I have three models: Person, Feature and PersonFeature. PersonFeature is a junction table with two foreign keys, person_id referencing id in the person table and feature_id referencing id in the feature table.
My question is does Gii in Yii2 generate all the relevant relations. These are the relation in each of the three models
Person:
public function getPersonfeatures()
{
return $this->hasMany(Personfeature::className(), ['personid' => 'id']);
}
Feature:
public function getPersonfeatures()
{
return $this->hasMany(Personfeature::className(), ['featureid' => 'id']);
}
PersonFeature:
public function getPerson()
{
return $this->hasOne(Person::className(), ['id' => 'personid']);
}
public function getFeature()
{
return $this->hasOne(Feature::className(), ['id' => 'featureid']);
}
But when I browse other posts I see that there exists a 'viaTable' operation for example:
public function getPerson() {
return $this->hasMany(Person::className(), ['id' => 'personid'])
->viaTable('personfeature', ['featureid' => 'id']);}
So basically my question is, is Yii supposed to generate this for me? Or can I manually add it in?
Cheers
The last function (with viaTable) is a many to many relationship, that function can be used just as any other relational function (for instance in a ->with() query).
You don't need a model for your join table, unless you want to use it for something else.
Hope this helps.
Related
I have 3 tables in my database :
users (id);
interests (id);
users_interests (user_id, interests_id);
I want to be able to fetch all the user's interests in this way :
$interests = $user->interests
This is what I wrote in the User.php model, following laravel's doc:
public function interests() {
return $this->hasManyThrough(
'App\Interest', 'App\UserInterest',
'user_id', 'id', 'interest_id'
);
}
but it returns empty even though the user has a game. So there has to be something I'm doing wrong
Anyone to help me ?
I think a belongs to many would do the job:
public function interests() {
return $this->belongsToMany(
'App\Interest',
'users_interests',
'user_id',
'interests_id'
);
}
Quite similar to the example in the docs
If you were to rename users_interests table to interest_user and the column
interests_id to the singular form you would just need the first parameter:
public function interests() {
return $this->belongsToMany(App\Interest::class);
}
From my understanding the hasManyThrough is used to jump forward within a relation (also described in the docs):
The "has-many-through" relationship provides a convenient shortcut for
accessing distant relations via an intermediate relation.
I am new to yii2. I have read the documentation and some answers on sof but still I cant get to work with relations in yii2. I am able to create raw mysql query for the problem but I dont know how to create the same query using yii2 relations. I am confused with via, joinWith and some key concepts. I will make the problem as descriptive as possible.
I have four models.
Category, CategoryNews, NewsTags, Tags
category table - cat_id, cat_name
news_category table - nc_id, nc_cat_id, nc_news_id
news_tags table - nt_id, nt_news_id, nt_tag_id
tags table - tag_id, tag_name
What I need is tags model object for each category, that is for each category i need all news tags belonging to that category. Request is from gridview.
The generated relations are:
Category Model:
public function getNewsCategory()
{
return $this->hasMany(NewsCategory::className(), ['nc_cat_id' => 'cat_id']);
}
NewsCategory Model:
public function getNcNews()
{
return $this->hasOne(News::className(), ['news_id' => 'nc_news_id']);
}
public function getNcCat()
{
return $this->hasOne(Category::className(), ['cat_id' => 'nc_cat_id']);
}
NewsTags Model:
public function getNtNews()
{
return $this->hasOne(News::className(), ['news_id' => 'nt_news_id']);
}
public function getNtTag()
{
return $this->hasOne(Tags::className(), ['tag_id' => 'nt_tag_id']);
}
News Model:
public function getNewsCategory()
{
return $this->hasMany(NewsCategory::className(), ['nc_news_id' => 'news_id']);
}
public function getNewsTags()
{
return $this->hasMany(NewsTags::className(), ['nt_news_id' => 'news_id']);
}
Tags Model:
public function getNewsTags()
{
return $this->hasMany(NewsTags::className(), ['nt_tag_id' => 'tag_id']);
}
ie. each category contains multiple news and each news contain mutiple tags and I need all tags related to each category.
More precisely, on the gridview I need all categories and a column displaying all tags related to these categories.
Please help!!
You can avoid declaration of models for junction tables, using viaTable syntax for many-to-many relations. Then your code will contain only three models (Category, News and Tag) and everything will be much simplier.
Your code for AR models and relations could looks as follows:
public class Category extends ActiveRecord
{
public function getNews()
{
return $this->hasMany(News::className(), ['id' => 'news_id'])
->viaTable('news_category_table', ['category_id' => 'id']);
}
}
public class News extends ActiveRecord
{
public function getCategories()
{
return $this->hasMany(Category::className(), ['id' => 'category_id'])
->viaTable('news_category_table', ['news_id' => 'id']);
}
public function getTags()
{
return $this->hasMany(Tags::className(), ['id' => 'tag_id'])
->viaTable('news_tags_table', ['news_id' => 'id']);
}
}
public class Tag extends ActiveRecord
{
public function getNews()
{
return $this->hasMany(News::className(), ['id' => 'news_id'])
->viaTable('news_tags_table', ['tag_id' => 'id']);
}
}
These relations you can use in link and unlink functions (rows in junction tables will be managed by Yii in backround). But keep in mind that you should use TRUE as second param in unlink() to remove row in junction table:
$article = new News();
$tag = new Tag();
$tag->save();
$article->link('tags', $tag);
$article->link('caterories', $category);
OR vice versa
$tag->link('news', $article);
$category->link('news', $article);
To get all tags in given category you can declare following function in Category class:
public function getTags()
{
return Tags::find()
->joinWith(['news', 'news.categories C'])
->where(['C.id' => $this->id])
->distinct();
}
This will work as relation query and you can use it as $category->tags or as $category->getTags()->count() or any other way (but not in link and unlink functions).
P.S. To use provided example in your code You should first change names, because I used singular form for AR classes names (Tag) and short notation for primary and foreign keys (id, tag_id etc). And I'd recommend you also to use such naming approach in your code and DB structure.
P.P.S. This example code wasn't tested so be careful :)
I have 3 models: Items, Serials and SerialsCategories. When I show Item form (to create or update) I need to show the serials which belongs to a categoryId selected in a previous step. A serial can belong to more than one category.
Right now I have on my Item model:
public function getSerialsTypeByCategory() {
return (new SerialType)->getByCategory($this->itemCategoryId);
}
On my SerialType model:
public function getByCategory($itemCategoryId) {
return SerialTypeItemCategory::find()->select(['serialTypeId'])->where(['itemCategoryId' => $itemCategoryId])->all();
}
This is working, it does what I need but ... Is this the proper way? is there a better way?
it's not wrong what you are doing. but there is something more -
check this link:
Working with Relational Data
if you use ->hasOne and ->hasMany to define relations, your model gains some extra benefits, like joining with lazy or eager loading:
Item::findOne($id)->with(['categories'])->all();
with a relation, you can also use ->link and ->unlink, to add/delete related data without having to think about linked fields.
Further, it is easy to define relations via junction table:
class Order extends ActiveRecord
{
public function getItems()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->viaTable('order_item', ['order_id' => 'id']);
}
}
I can get the results using this query in mysql.
SELECT group_concat(im.instrument_name) AS instrument
FROM ot_note otn
LEFT JOIN ot_instrument_entry oie ON otn.id=oie.ot_note_id
LEFT JOIN instrument_master im ON oie.instrument_name=im.id
GROUP BY otn.id
Now I have a model OTNote, InstrumentMaster and OTInstrumentEntry.
I have tried to use a relation using via table, but it looks like I am missing something.
I have tried to create the relation like this, but it is throwing error;
public function getInstrumentMaster()
{
return $this
->hasMany(OtInstrumentEntry::className(), ['ot_note_id' => 'id'])
->viaTable('instrument_master', ['id'=>'instrument_name']);
}
How can I can create a relation so I can access the column instrument_master.instrument_name column which is related to ot_instrument_entry?
ot_instrument_entry is the linking table. In class OTNote you should have:
// class OTNote
public function getInstrumentMaster()
{
return $this
->hasMany(InstrumentMaster::className(), ['id' => 'instrument_name'])
->viaTable('ot_instrument_entry', ['ot_note_id' => 'id']);
}
In class InstrumentMaster you may want to have this for vice versa access:
// class InstrumentMaster
public function getOTNotes()
{
return $this
->hasMany(OTNote::className(), ['id' => 'ot_note_id'])
->viaTable('ot_instrument_entry', ['instrument_name' => 'id']);
}
Details can be found here.
I'm trying to get data from a join table in Yii2 without an additional query. I have 2 models (User, Group) associated via the junction table (user_group). In the user_group table, I want to store extra data (admin flag, ...) for this relation.
What's the best way to add data to the junction table? The link method accepts a parameter extraColumns but I can't figure out how this works.
What's the best way to retrieve this data? I wrote an additional query to get the values out of the junction table. There must be a cleaner way to do this?!
FYI, this is how I defined the relation in the models:
Group.php
public function getUsers() {
return $this->hasMany(User::className(), ['id' => 'user_id'])
->viaTable('user_group', ['group_id' => 'id']);
}
User.php
public function getGroups() {
return $this->hasMany(Group::className(), ['id' => 'group_id'])
->viaTable('user_group', ['user_id' => 'id']);
}
In short: Using an ActiveRecord for the junction table like you suggested is IMHO the right way because you can set up via() to use that existing ActiveRecord. This allows you to use Yii's link() method to create items in the junction table while adding data (like your admin flag) at the same time.
The official Yii Guide 2.0 states two ways of using a junction table: using viaTable() and using via() (see here). While the former expects the name of the junction table as parameter the latter expects a relation name as parameter.
If you need access to the data inside the junction table I would use an ActiveRecord for the junction table as you suggested and use via():
class User extends ActiveRecord
{
public function getUserGroups() {
// one-to-many
return $this->hasMany(UserGroup::className(), ['user_id' => 'id']);
}
}
class Group extends ActiveRecord
{
public function getUserGroups() {
// one-to-many
return $this->hasMany(UserGroup::className(), ['group_id' => 'id']);
}
public function getUsers()
{
// many-to-many: uses userGroups relation above which uses an ActiveRecord class
return $this->hasMany(User::className(), ['id' => 'user_id'])
->via('userGroups');
}
}
class UserGroup extends ActiveRecord
{
public function getUser() {
// one-to-one
return $this->hasOne(User::className(), ['id' => 'user_id']);
}
public function getGroup() {
// one-to-one
return $this->hasOne(Group::className(), ['id' => 'userh_id']);
}
}
This way you can get the data of the junction table without additional queries using the userGroups relation (like with any other one-to-many relation):
$group = Group::find()->where(['id' => $id])->with('userGroups.user')->one();
// --> 3 queries: find group, find user_group, find user
// $group->userGroups contains data of the junction table, for example:
$isAdmin = $group->userGroups[0]->adminFlag
// and the user is also fetched:
$userName = $group->userGroups[0]->user->name
This all can be done using the hasMany relation. So you may ask why you should declare the many-to-many relation using via(): Because you can use Yii's link() method to create items in the junction table:
$userGroup = new UserGroup();
// load data from form into $userGroup and validate
if ($userGroup->load(Yii::$app->request->post()) && $userGroup->validate()) {
// all data in $userGroup is valid
// --> create item in junction table incl. additional data
$group->link('users', $user, $userGroup->getDirtyAttributes())
}
I don't know for sure it is best solution. But for my project it will be good for now :)
1) Left join
Add new class attribute in User model public $flag;.
Append two lines to your basic relation but don't remove viaTable this can (and should) stay.
public function getUsers()
{
return $this->hasMany(User::className(), ['id' => 'user_id'])
->viaTable('user_group', ['group_id' => 'id'])
->leftJoin('user_group', '{{user}}.id=user_id')
->select('{{user}}.*, flag') //or all ->select('*');
}
leftJoin makes possible to select data from junction table and with select to customize your return columns.
Remember that viaTable must stay because link() relies on it.
2) sub-select query
Add new class attribute in User model public $flag;
And in Group model modified getUsers() relation:
public function getUsers()
{
return $this->hasMany(User::className(), ['id' => 'user_id'])
->viaTable('user_group', ['group_id' => 'id'])
->select('*, (SELECT flag FROM user_group WHERE group_id='.$this->id.' AND user_id=user.id LIMIT 1) as flag');
}
As you can see i added sub-select for default select list. This select is for users not group model. Yes, i agree this is litle bit ugly but does the job.
3) Condition relations
Different option is to create one more relation for admins only:
// Select all users
public function getUsers() { .. }
// Select only admins (users with specific flag )
public function getAdmins()
{
return $this->hasMany(User::className(), ['id' => 'user_id'])
->viaTable('user_group', ['group_id' => 'id'],
function($q){
return $q->andWhere([ 'flag' => 'ADMIN' ]);
});
}
$Group->admins - get users with specific admin flag. But this solution doesn't add attribute $flag. You need to know when you select only admins and when all users. Downside: you need to create separate relation for every flag value.
Your solution with using separate model UserGroup still is more flexible and universal for all cases. Like you can add validation and basic ActiveRecord stuff. These solutions are more one way direction - to get stuff out.
Since I have received no answer for almost 14 days, I'll post how I solved this problem. This is not exactly what I had in mind but it works, that's enough for now. So... this is what I did:
Added a model UserGroup for the junction table
Added a relation to Group
public function getUserGroups()
{
return $this->hasMany(UserGroup::className(), ['user_id' => 'id']);
}
Joined UserGroup in my search model function
$query = Group::find()->where('id =' . $id)->with('users')->with('userGroups');
This get's me what I wanted, the Group with all Users and, represented by my new model UserGroup, the data from the junction table.
I thought about extending the query building Yii2 function first - this might be a better way to solve this. But since I don't know Yii2 very well yet, I decided not to do for now.
Please let me know if you have a better solution.
For that purpose I've created a simple extension, that allows to attach columns in junction table to child model in relation as properties.
So after setting up this extension you will be able to access junction table attributes like
foreach ($parentModel->relatedModels as $childModel)
{
$childModel->junction_table_column1;
$childModel->junction_table_column2;
....
}
For more info please have look at
Yii2 junction table attributes extension
Thanks.