This behavior helps me to update and delete models with HAS_ONE, HAS_MANY and MANY_MANY relations.
Just give it a try)
Requirements ¶
Yii 1.0.2 or above
Usage ¶
To use this extension, just copy behavior class file to your /components/ directory, add this behavior to each model you would like to extend with new possibilities:
/**
* This is the model class for table "blog_category".
*
* The followings are the available columns in table 'blog_category':
* @property integer $id
* @property string $parent_id
* @property integer $sort
*/
class BlogCategory extends CActiveRecord
{
...
public function behaviors()
{
return array(
'AdvancedRelationsBehavior' => array(
'class' => 'AdvancedRelationsBehavior',
'relations' => array(
// HAS_MANY relations
'categories',
// MANY_MANY relations
'posts',
),
),
);
}
...
}
/**
* This is the model class for table "blog_post_to_category".
*
* The followings are the available columns in table 'blog_post_to_category':
* @property integer $post_id
* @property integer $category_id
* @property integer $sort
*/
class BlogPostToCategory extends CActiveRecord
{
...
}
/**
* This is the model class for table "blog_post".
*
* The followings are the available columns in table 'blog_post':
* @property integer $id
*/
class BlogPost extends CActiveRecord
{
...
public function behaviors()
{
return array(
'AdvancedRelationsBehavior' => array(
'class' => 'AdvancedRelationsBehavior',
'autoUpdate' => false, // disable automatic update of related records
'relations' => array(
// MANY_MANY relations
'categories',
'tags', // same structure as `blog_post_to_category`
),
),
);
}
...
}
After you've attached this behavior to models, you can use it. For example, to attach category model with PK 3 to categories with PK 1 and 2 now you can use code like this:
$model = BlogCategory::model()->findByPk(3);
$model->categories = array(1, 2);
$model->save();
Or to move all posts to category with PK 1:
$model = BlogCategory::model()->findByPk(1);
$model->posts = BlogPost::model()->findAll();
$model->save();
Also you can use manual update of relations. In the BlogPost model we disable auto-update, so we must call update method updateRelated() to update relations. For example, let's create BlogPost and attach it to all categories:
$model = new BlogPost;
$model->categories = BlogCategory::model()->findAll();
// update only `categories` relation, `tags` relation is ignored
if($model->save())
$model->updateRelations('categories');
If sort order attribute was specified in the configuration, behavior will update it before save model. To disable this feature, just assign NULL as attribute name.
Advanced usage example ¶
Note: saveWithRelated() and deleteWithRelated() methods added in version 1.0.5
/**
* Task: build this categories tree, add "Welcome" post to each "Posts" sub-category
* - News
* - Business
* - Posts
* - Sci/Tech
* - Posts
* - Entertainment
* - Posts
* - Sports
* - Posts
* - Health
* - Posts
*/
// the root category model
$rootCategory = new BlogCategory;
$rootCategory->title = "News";
// we cannot make direct modifications to the CActiveRecord relations,
// so we need to make a temporary buffer for this
$categories = array();
foreach(array(
'Business',
'Sci/Tech',
'Entertainment',
'Sports',
'Health'
) as $categoryTitle)
{
$category = new BlogCategory;
$category->title = $categoryTitle;
// add sub-category
$postsCategory = new BlogCategory;
$postsCategory->title = "Posts";
// add "Welcome" post to the "Posts"
$post = new BlogPost;
$post->title = "Welcome";
$postsCategory->posts = $post;
// save "Posts" category with "Welcome" post
$postsCategory->saveWithRelated('posts');
$category->categories = $postsCategory;
$categories[] = $category;
}
$rootCategory->categories = $categories;
// save the categories tree
$rootCategory->saveWithRelated('categories');
Update ¶
Version 1.0.9 saveWithRelated was fixed (update related models even if no changes in the owner model attributes)
Version 1.0.8 update doxygen comments for getRelations and updateRelated methods
Version 1.0.7 restore HAS_ONE relation data format after saving
Version 1.0.6 added support for both types of HAS_ONE and HAS_MANY relations
Version 1.0.5 added saveWithRelated() and deleteWithRelated() methods
Version 1.0.4 algorithm of updating HAS_MANY relations was fixed
Version 1.0.3 {{table}} parser RegExp fixed
Version 1.0.2 {{table}} parser RegExp updated
Version 1.0.1 HAS_MANY related records will be removed before saving.
inconsistencies
There are a few numeric inconsistencies in the article. Please check the ID numbers for clarity. An example should be perfect and errorless.
fixed
Thanks for you notice, fixed
active records with prefixed table names
when we use prefix (e.g. 'tbl_'), our relations will look like {{table_name}}(primary_key, foreign_key) and the expression below will go wrong.
preg_match('/^(.+)\((.+)\s*,\s*(.+)\)$/s', $relation[2], $m)
RegExp fixed
Thanks for your notice, RegExp fixed.
New expression:
if(preg_match('/^\s*[\{]*(.+)[\}]*\((.+)\s*,\s*(.+)\)\s*$/s', $relation[2], $m))
regexp still not ok
Hi alexjay, the regexp is buggy (the first group actually - it includes the two closing }} ), i think the following will be correct:
if(preg_match('/^\s*[\{]*([^}]+)[\}]*\((.+)\s*,\s*(.+)\)\s*$/s', $relation[2], $m))
Nice extension!
RegExp fixed ( I want to believe ;) )
Thanks, nlac. Shame on me)
Current RegExp is:
if(preg_match('/^\s*[\{]*([^\}]+?)[\}]*\s*\(\s*([^,\s]+)\s*,\s*([^\)]+?)\s*\)\s*$/s', $relation[2], $m))
data losing in updateRelated()
Hi alexjay,
i tested your extension again (actually i decided to use it in my module, i like it). I'm not sure that the proper action is done by updateRelated() method in case of HAS_MANY relations.
If i'm correct, for that case the method does:
The problem with this approach is, the method does NOT clone the RELATED data when making the clones in step 1 - i mean the relation data of the cloned records! All the relation info will be lost in step 2, the clones will be saved in step 3 without ANY relation info, except the setting the proper PKs through which they relates to the owner.
I suggest not to make any clones in step 1, delete only the necessary related records in step 2 and just set the proper FKs in step 3.
Another small thing: in line 80, there's a condition if(is_integer($model)) ...
That will fail if the given variable is a string eg. "2", better to use something like this if (!@$model->attributes) ...
important addition to the previous line
sorry, i forgot to write to my previous line: when i tested, the "autoDelete" AdvancedRelationsBehavior property was set for the related models, the current updateRelated() method produces the data losing when that is set.
fix and enhancement
I've fix an algorithm of updating HAS_MANY relations and added saveWithRelated() and deleteWithRelated() methods for better usage
recursive deleting
Hi me again:)
i have an another idea for your extension: extending the deleteRelated() with a parameter "$recursive" so this function would be able to delete all records along the HAS_MANY relationship tree, not just the immediate childs. It is simple and cool, the code is:
line 172: public function deleteRelated($relations = null, $bRecursive = false) right after line 203: if ($bRecursive) $success &= $model->deleteRelated(null,true); ...
The function deleteWithRelated() can also be extend with that parameter as well.
about deleteRelated method
Hi, nlac! Nice to see that AdvancedRelationsBehavior is used not only by me!)))
Thanks for your idea, I've also thought about this before and found that if we add this behavior to models from relations this will be done automaticaly.
This will be done here:
if(!$model->delete()) $success = false;
When we delete a particular model, all models from relations with behavior AdvancedRelationsBehavior will be handled automaticaly.
/* Example data structure: - category PK#1 - category PK#2 - blog posts */ BlogCategory::model()->findByPk(1)->delete();
So if we delete category PK#1, will be also deleted category PK#2 with all related blog posts.
Minor points from reading your code
Reviewing the code, I found these minor points:
The doxygen comment for getRelations indicate that it will return HAS_MANY and MANY_MANY relations but the code also includes HAS_ONE.
An undocumented side effect of updateRelated on a MANY_MANY-relation is that if the attribute value is a non-array it will be converted to an array. Also HAS_ONE is forced to not be an array at the end of updateRelated even if it was an array when updateRelated was called.
For MANY_MANY, updateRelated will call save() on relations to new foreign models (models where $model->isNewRecord == true). This is good as often PK is an incrementing number assigned by the database, but it may cause trouble to some if they don't know about this. So perhaps add a note about this in the doxygen comment.
doxygen comments
Hi, Leffe! Thanks for your review! I've updated doxygen comments for these methods.
doxygen
Thanks for the quick response on my feedback.
In your update, I noticed that you forgot a '*' on line 63 to connect the doxygen block. I don't know if this is still valid doxygen format or not may at least give a warning if anyone runs the doxygen program on your code.
I must add that I like your extension and the fact that it is possible to turn off automatic saving of relations. This allow me to make my code a bit more explicit. Also I like that you don't just erase all relations and add them back as the older similar extension does.
re: doxygen
Thanks, Leffe! I've uploaded a fixed file.
Erasing all relations and adding them back on every update is not good for database load, so I decided that it is better to update only what really needs to be updated. I'm always trying minimize the possible load from my code.
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.