Create a relation of more than three tables in Yii - php

I have the following method that is causing me trouble as I try and create a relation of more than three tables in Yii:
public function relations()
{
return array(
'info'=>array(self::BELONGS_TO, 'Software', 'ITEM_ID'),
'categories'=>array(self::MANY_MANY, 'ItemCategory',
'item_cat_relation(item_id, cat_id)',
'condition'=>'categories.cat_of_type=item_meta1.item_type_id'),
);
}
This code gives an error at item_meta1.item_type_id

In general, Yii is not built to handle "three table relations". That being said, you should still be able to add a condition() to your relation query, I think the issue is just that you didn't JOIN the item_meta1 table. You can do this two ways:
1) Add a JOIN clause to your relation:
return array(
'categories'=>array(self::MANY_MANY, 'ItemCategory',
'item_cat_relation(item_id, cat_id)',
'join'=>'JOIN item_meta1 ON categories.cat_of_type=item_meta1.item_type_id'
),
);
1) Add a WITH clause to your relation (assuming you have a relation for the other table set up):
return array(
'itemMeta'=>array(self::HAS_MANY, 'ItemMeta','item_type_id'), // I probably don't have this quite right, but you should get the idea
'categories'=>array(self::MANY_MANY, 'ItemCategory',
'item_cat_relation(item_id, cat_id)',
'with'=>'itemMeta',
'condition'=>'categories.cat_of_type=itemMeta.item_type_id')
),
);
I did not test any of that code even a little bit, but I have done similar things so it should work, in principle. :) Good luck!

Related

Yii1 search() by many-many relation returns only one related model

I have 3 tables:
clients, traders and client_trader_relation
clients can have many traders, and traders can have many clients so this is a MANY-MANY relationship with a "pivot" table. The relation is defined in clients model like this:
$relations = array(
'traders' => array(self::MANY_MANY, 'traders', 'client_trader_relation(client_id, trader_id)'),
);
Now everything works correctly when displaying a listing of all clients in let's say CGridView, but I also want to be able to search for clients by a specific trader (so if one of the traders is let's say id 10, then return this client).
I have done it like this in model's search() function:
public function search()
{
$criteria=new CDbCriteria;
$criteria->with = 'traders';
$criteria->together = true;
$criteria->compare('traders.id', $this->search_trader);
}
search_trader is an additional variable added to the model & rules so it cna be used for searching.
While this works, it successfully returns all clients of specified trader, the result doesn't contain any other related traders, just the one I'm searching for. I can understand this behaviour, because that's the way the generated SQL works.
I'm curious though if there is any way to return all the traders from such search without having to make any additional queries/functions? If not, then what would be the correct way of doing such thing? As for now, I can only think of some function in the model like getAllTraders() that would manually query all the traders related to current client. That would work, I could use this function for displaying the list of traders, but it would produce additional query and additional code.
You can use this to disable eager loading:
$this->with(['traders' => ['select' => false]]);
But this will create separate query for each row, so with 20 clients in GridView you will get extra 20 queries. AFAIK there is no clean and easy way to do this efficiently. The easiest workaround is to define additional relation which will be used to get unfiltered traders using eager loading:
public function relations() {
return [
'traders' => [self::MANY_MANY, 'traders', 'client_trader_relation(client_id, trader_id)'],
'traders2' => [self::MANY_MANY, 'traders', 'client_trader_relation(client_id, trader_id)'],
];
}
And then define with settings for eager loading:
$this->with([
'traders' => ['select' => false],
'traders2',
]);
Then you can use $client->traders2 to get full list of traders.
You can also define this relation ad-hoc instead of in relations():
$this->getMetaData()->addRelation(
'traders2',
[self::MANY_MANY, 'traders', 'client_trader_relation(client_id, trader_id)']
);

Softdelete with Yii and relations

I have a simple DB with multiple tables and relationships, ie:
Article - Category
User - Group
etc...
I have implemented SoftDelete behavior where there is a Active column and if set to 0, it is considered deleted.
My question is simple.
How to i specify in as few places as possible that i only want load Articles that belong to Active categories.
I have specified relationships and default scopes (with Active = 1) condition.
However, when i do findAll(), it returns those Articles that have Active = 1, even if the category it belongs to is Active = 0....
Thank you
Implementation so far:
In base class
public function defaultScope()
{
return array('condition' => 'Active = 1');
}
in model:
'category' => array(self::BELONGS_TO, 'Category', 'CategoryID'),
'query':
$data = Article::model()->findAll();
MY SOLUTION
So i decided, that doing it in framework is:
inneficient
too much work
not good as it moves business logic away from database - this is fairly important to save work later on when working on interfaces/webservices and other customizations that should be part of the product.
Overall lesson: Try to keep all business logic as close to database as possible to prevent disrepancies.
First, i was thinking using triggers that would propagate soft delete down the hierarchy. However after thinking a bit more i decided not to do this. The reason is, that this way if I (or an interface or something) decided to reactivate the parent records, there would be no way to say which child record was chain-deleted and which one was deleted before:
CASE:
Lets say Category and Article.
First, one article is deleted.
Then the whole category is deleted.
Then you realize this was a mistake and you want to undelete the Category. How do you know which article was deleted by deleting category and which one should stay deleted? Yes there are solutions, ie timestamps but ...... too complex, too easy to break
So my solution in the end are:
VIEWS. I think i will move away from yii ORM to using views for anything more complex then basic things.
There are two advantages to this for me:
1) as a DBA i can do better SQL faster
2) logic stays in database, in case the application changes/another one is added, there is no need to implement the logic in more then one places
You need to specify condition when you are using findAll method. So You should use CDbCriteria for this purpose:
$criteria=new CDbCriteria;
$criteria->with = "category";
$criteria->condition = "category.Active = 1"; //OR $criteria->compare('category.active', 1 true);
$data = Article::model()->findAll($criteria);
You should also have a defaultScope in your Article model, condition there should add category.Active = 1 or whatever your relation is named.
public function defaultScope()
{
return array('condition' => 't.Active = 1 AND category.Active = 1');
}
I don't remember by now but it might be you have to specify the relation:
return array(
'with' => array("category" => array(
'condition'=> "t.Active = 1 AND category.Active = 1",
)
);

Yii CActiveRecord multi level (nested) joins

I'm fairly new to Yii and so far I've managed to get by on my own but now I'm stuck.
I have a complex relational db (implemented in MySQL). I have the models for them and it's working properly my only problem is that I can't figure out how to make complex queries with CDbCriteria
The application is like an issue tracker so a user can report some problem and someone in charge of that type of problem will contact him/her.
Main tables relevant to the problem:
roles (multiple roles can be assigned to a user, e.g. a role can be 'accountant' or 'developer' )
issuetypes (a user with a role /e.g. accountant/ can create a new issue with a set of issue types /e.g. "printer problem"/
issues (every issue can only have one issue type)
A developer can create an issue like "new PHP version needed" but the accountant can't do that so I need to query the database for all the available issuetypes for a set of roles.
If the user have multiple roles (developer, tester) then I need the union of the issuetypes available for those roles.
So far it's working but when I need to take it a step further and query all the issues submitted with these issuetypes...I'm stuck.
Roughly I need to get the following query:
SELECT DISTINCT i.* FROM `issue` i
LEFT JOIN issuetype ON issuetype.id=i.issuetype_id
RIGHT JOIN role_has_issuetype rit ON rit.issuetype_id=issuetype.id
RIGHT JOIN role ON role.id=rit.role_id
WHERE role.role IN ('developer','tester') AND i.id IS NOT NULL
I know I could use the SQL query directly but the db backend will change in the future (most probably to Oracle) so I'd like to keep the abstraction as far as I could to avoid changing any hard-coded SQL statement and be "backend-independent".
The relevant parts of the models:
class Role extends CActiveRecord
{
public function relations()
{
return array(
'issuetypes' => array(self::MANY_MANY, 'Issuetype', 'role_has_issuetype(role_id, issuetype_id)'),
'users' => array(self::MANY_MANY, 'User', 'user_has_role(role_id, user_id)'),
);
}
}
class Issuetype extends CActiveRecord
{
public function relations()
{
return array(
...
'issues' => array(self::HAS_MANY, 'Issue', 'issuetype_id'),
'roles' => array(self::MANY_MANY, 'Role', 'role_has_issuetype(issuetype_id, role_id)'),
);
}
class Issue extends CActiveRecord
{
public function relations()
{
...
'issuetype' => array(self::BELONGS_TO, 'Issuetype', 'issuetype_id'),
);
}
}
I've tried something like this:
Issue::model()->with(
array(
'issuetype'=>array(
'select'=>false,
'joinType'=>'INNER JOIN',
'condition'=>'issuetype.roles IN ("developer","tester")',
)))->findAll();
It doens't work because issuetype has no column roles it's just a relation.
I've tried to do it in two steps. First get the issuetypes associated with the roles then get the issues.
The first part is working with this code:
$crit = new CDbCriteria();
$crit->addInCondition('roles.role',array('developer','tester'));
$crit->select = array('id');
$res=Issuetype::model()->with('roles')->findAll($crit);
But I don't know how to use the $res in another criteria.
(I'm not even sure this approach will work and even if will it's far from optimal)
I've read about a dozen SO answers and read the Yii forum together with the Yii docs but the examples I've found were not sufficient to solve this (at least I couldn't adopt those codes
to my problem)
I'm quite sure I'm just overlooking some obvious stuff but unfortunately I can't figure it out on my own.
Thanks
Sleeping on the problem helped :)
I've realized my mistake. I should have written this:
return Issue::model()->with(array(
'issuetype.roles'=>array(
'select'=>false,
'joinType'=>'INNER JOIN',
'condition'=>"roles.role IN ('developer','tester')",
)))->findAll();
I hope this will help someone in the future.

Yii Framework - two relations via the same "through" table

Mine goal is to have possibility to search Documents via the Users Names and Surnames and also via Recrutation Year and Semester.
Documents are related only to Declarations in such a way that Document are connected to exatly one Declaration and Declaration can be connected to exatly One or none Documents.
Declarations are related to OutgoingStudent and Recrutation.
So when quering Documents I want to query also OutgoingStudent and Recrutations via the Declaration table.
My code for relations in Documents:
return array(
'declaration' => array(self::BELONGS_TO, 'Declaration', 'DeclarationID'),
'outgoingStudentUserIdUser' => array(self::HAS_ONE, 'OutgoingStudent', 'OutgoingStudent_User_idUser','through'=>'declaration',),
'Recrutation' => array(self::HAS_ONE, 'Recrutation', 'Recrutation_RecrutationID','through'=>'declaration'),
);
And now when in search() function I want to make a query ->with
'declaration','outgoingStudentUserIdUser' and 'Recrutation':
$criteria->with = array('declaration','Recrutation','outgoingStudentUserIdUser');
I'm getting this error:
CDbCommand nie zdołał wykonać instrukcji SQL: SQLSTATE[42000] [1066]
Not unique table/alias: 'declaration'. The SQL statement executed was:
SELECT COUNT(DISTINCT t.DeclarationID) FROM Documents t LEFT
OUTER JOIN Declarations declaration ON
(t.DeclarationID=declaration.idDeclarations) LEFT OUTER JOIN
Recrutation Recrutation ON
(declaration.Recrutation_RecrutationID=Recrutation.RecrutationID)
LEFT OUTER JOIN Declarations declaration ON
(t.DeclarationID=declaration.idDeclarations) LEFT OUTER JOIN
OutgoingStudent outgoingStudentUserIdUser ON
(declaration.OutgoingStudent_User_idUser=outgoingStudentUserIdUser.User_idUser)
When using only $criteria->with = array('declaration','Recrutation') or $criteria->with = array('declaration','outgoingStudentUserIdUser') there is no error only when using both.
So probably it should be done in some other way, but how?
I have so many things to tell you! Here they are:
I find your relations function declaration pretty messy, and I'm not sure if it is doing what you want it to do (in case it worked). Here are my suggestions to re-declare it:
First of all, 'outgoingStudentUserIdUser' looks like a terrible name for a relation. In the end, the relation will be to instances of outgoingStudentUser, not only to 'ids'. So allow me to name it just as outgoingStudentUser. Now, this is my code:
'outgoingStudentUser' => array(self::HAS_ONE, 'OutgoingStudent', array('idDocuments'=>'idOutgoingStudent'),'through'=>'declaration',),
where 'idDocuments' is Documents' model primary key, and idOutgoingStudent is OutgoingStudent's model primary key.
The second relation could be corrected in a very similar way:
'Recrutation' => array(self::HAS_ONE, 'Recrutation', array('idDocuments'=>'idRecrutation'),'through'=>'declaration'),
where 'idDocuments' is Documents' model primary key, and idRecrutation is Recrutation's model primary key.
You can find that this is the correct declaration here: http://www.yiiframework.com/doc/guide/1.1/en/database.arr#through-on-self
But that's not all. I have more to tell you! What you're doing with your code is senseless. 'with' is used to force eager loading on related objects. In the following code:
$criteria->with = array('declaration','Recrutation','outgoingStudentUserIdUser');
you're just specifying in $criteria that when you retrieve in DB an instance of Documents using this $criteria, it will also fetch the models linked to that instance by the relations passed as parameters to 'with'. That's eager loading. It is used to reduce the number of queries to database. Is like saying: "go to DB and get me this instance of Documents, but once you're there, bring to me once per all the instances of other tables related to this object".
Well, that's what you're declaring, but certainly that's not what you want to do. How I know? Because that declaration is useless inside a search() function. As you may see here: http://www.yiiframework.com/doc/api/1.1/CDbCriteria/#with-detail , 'with' is only useful in some functions, and search() is not one of them. Inside search(), eager loading is pointless, senseless and useless.
So I see myself forced to ask you what are you trying to do? You say: "Mine goal is to have possibility to search Documents via the Users Names and Surnames and also via Recrutation Year and Semester", but what do you mean by "search Documents via the Users Names and..."? Do you want something like this: $user->documents, to return all the documents associated with $user? I hope you could be more specific about that, but perhaps in another, more to-the-point, question.
You can also try this:
return array(
'declaration' => array(self::BELONGS_TO, 'Declaration', 'DeclarationID'),
'outgoingStudentUserIdUser' => array(self::HAS_ONE, 'OutgoingStudent', 'OutgoingStudent_User_idUser','through'=>'declaration',),
'Recrutation' => array(self::HAS_ONE, 'Recrutation', '', 'on'=>'declaration.id=Recrutation.Recrutation_RecrutationID'),
);

adding hasMany association causes find() to not work well

OK, I am a little bit lost...
I am pretty new to PHP, and I am trying to use CakePHP for my web-site.
My DB is composed of two tables:
users with user_id, name columns
copies with copy_id, copy_name, user_id (as foreign key to users) columns.
and I have the matching CakePHP elements:
User and Copy as a model
UserController as controller
I don't use a view since I just send the json from the controller.
I have added hasMany relation between the user model and the copy model see below.
var $hasMany = array(
'Copy' => array(
'className' => 'Friendship',
'foreignKey' => 'user_id'
)
);
Without the association every find() query on the users table works well, but after adding the hasMany to the model, the same find() queries on the users stop working (print_r doesn't show anything), and every find() query I am applying on the Copy model
$copy = $this->User->Copy->find('all', array(
'condition' => array('Copy.user_id' => '2')
));
ignores the condition part and just return the whole data base.
How can I debug the code execution? When I add debug($var) nothing happens.
I'm not an expert, but you can start with the following tips:
Try to follow the CakePHP database naming conventions. You don't have to, but it's so much easier to let the automagic happen... Change the primary keys in your tabel to 'id', e.g. users.user_is --> users.id, copies.copy_id -->copies.id.
Define a view, just for the sake of debugging. Pass whatever info from model to view with $this->set('users', $users); and display that in a <pre></pre> block
If this is your first php and/or CakePHP attempt, make sure you do at least the blog tutorial
Make CakePHP generate (bake) a working set of model/view/controllers for users and copies and examine the resulting code
There's good documentation about find: the multifunctional workhorseof all model data-retrieval functions
I think the main problem is this:
'condition' => array('Copy.user_id' => '2')
It should be "conditions".
Also, stick to the naming conventions. Thankfully Cake lets you override pretty much all its assumed names, but it's easier to just do what they expect by default.
The primary keys should be all named id
The controller should be pluralised: UsersController
First off, try as much as possible to follow CakePHP convention.
var $hasMany = array(
'Copy' => array(
'className' => 'Friendship',
'foreignKey' => 'user_id'
)
);
Your association name is 'Copy' which is a different table and model then on your classname, you have 'Friendship'.
Why not
var $hasMany = array(
'Copy' => array('className'=>'Copy')
);
or
var $hasMany = array(
'Friendship' => array('className'=>'Friendship')
);
or
var $hasMany = array(
'Copy' => array('className'=>'Copy'),
'Friendship' => array('className'=>'Friendship')
);
Also, check typo errors like conditions instead of condition
Your table name might be the problem too. I had a table named "Class" and that gave cake fits. I changed it to something like Myclass and it worked. Class was a reserved word and Copy might be one too.

Categories