Yii 1.1: activerecord-relation-behavior

Inspired by and put together the awesomeness of many yii extensions that aim to improve saving of related records.
44 followers

ActiveRecord Relation Behavior

This extension is inspired by all the yii extensions that aim to improve saving of related records. It allows you to assign related records especially for MANY_MANY relations more easily. It puts together the awesomeness of all the extensions mentionend below (see headline "Feature comparison"). It comes with 100% test coverage and well structured and clean code so it can savely be used in enterprise production enviroment.

Requirements

  • Yii 1.1.6 or above
  • As Yii Framework this behavior is compatible with all PHP versions above 5.1.0. I try to be at least as backwards compatible as Yii is, which is PHP 5.1.0, if there are any problems with php versions, please report it!
  • Behavior will only work for ActiveRecord classes that have primary key defined. Make sure to override primaryKey() method when your table does not define a primary key.
  • You need php 5.3.x or higher to run the unit tests.

Resources

How to install

  1. Get the source in one of the following ways:
    • Download the latest version and place the files under extensions/yiiext/behaviors/activerecord-relation/ under your application root directory.
    • Add this repository as a git submodule to your repository by calling git submodule add https://github.com/yiiext/activerecord-relation-behavior.git extensions/yiiext/behaviors/activerecord-relation
  2. Add it to the models you want to use it with, by adding it to the behaviors() method.
public function behaviors()
{
    return array(
        'activerecord-relation'=>array(
            'class'=>'ext.yiiext.behaviors.activerecord-relation.EActiveRecordRelationBehavior',
    );
}

Let the magic begin...

We have two ActiveRecord classes (the ones from Yii definitive guide):

class Post extends CActiveRecord
{
    // ...
    public function relations()
    {
        return array(
            'author'     => array(self::BELONGS_TO, 'User',     'author_id'),
            'categories' => array(self::MANY_MANY,  'Category', 'tbl_post_category(post_id, category_id)'),
        );
    }
}
 
class User extends CActiveRecord
{
    // ...
    public function relations()
    {
        return array(
            'posts'   => array(self::HAS_MANY, 'Post',    'author_id'),
            'profile' => array(self::HAS_ONE,  'Profile', 'owner_id'),
        );
    }
}

Somewhere in our application code we can do:

$user = new User();
    $user->posts = array(1,2,3);
    $user->save();
    // user is now author of posts 1,2,3
 
    // this is equivalent to the last example:
    $user = new User();
    $user->posts = Post::model()->findAllByPk(array(1,2,3));
    $user->save();
    // user is now author of posts 1,2,3
 
    $user->posts = array_merge($user->posts, array(4));
    $user->save();
    // user is now also author of post 4
 
    $user->posts = array();
    $user->save();
    // user is not related to any post anymore
 
    $post = Post::model()->findByPk(2);
    $post->author = User::model()->findByPk(1);
    $post->categories = array(2, Category::model()->findByPk(5));
    $post->save();
    // post 2 has now author 1 and belongs to categories 1 and 5
 
    // adding a profile to a user:
    $user->profile = new Profile();
    $user->profile->save(); // need this to ensure profile got a primary key
    $user->save();

Some things you should care about...

  • once you use this behavior you can not set relations by setting the foreign key attributes anymore. For example if you set $model->author_id it will have no effect since ARRelationBehavior will overwrite it with null if there is no related record or set it to related records primary key. Instead simply assign the value to the relation itself: $model->author = 1; / $model->author = null;
  • relations will not be refreshed after saving, so if you only set primary keys there are no objects yet. Call $model->reload() to force reloading of related records. Or load related records with forcing reload: $model->getRelated('relationName',true).
  • This behavior will only work for relations that do not have additional conditions, joins, groups or the like defined since the expected result after setting and saving them is not always clear.
  • if you assigned a record to a BELONGS_TO relation, for example $post->author = $user;, $user->posts will not be updated automatically (might add this as a feature later).

Exceptions explained

"You can not save a record that has new related records!"

You have assigned a record to a relation which has not been saved (it is not in the database yet). Since ActiveRecord Relation Behavior needs its primary key to save it to a relation table, this will not work. You have to call ->save() on all new records before saving the related record.

"A HAS_MANY/MANY_MANY relation needs to be an array of records or primary keys!"

You can only assing arrays to HAS_MANY and MANY_MANY relations, assigning a single record to a ..._MANY relation is not possible.

"Related record with primary key "X" does not exist!"

You tried to assign primary key value X to a relation, but X does not exist in your database.

Feature comparison

Inspired by and put together the awesomeness of the following yii extensions:

  • can save MANY_MANY relations like cadvancedarbehavior, eadvancedarbehavior, esaverelatedbehavior and advancedrelationsbehavior
  • cares about relations when records get deleted like eadvancedarbehavior (not yet implemented, see github issue #7)
  • can save BELONGS_TO, HAS_MANY, HAS_ONE like eadvancedarbehavior, esaverelatedbehavior and advancedrelationsbehavior
  • saves with transaction and can handle external transactions like with-related-behavior, esaverelatedbehavior and saverbehavior
  • does not touch additional data in MANY_MANY table (cadvancedarbehavior deleted it)
  • validates for array on HAS_MANY and MANY_MANY relation to have more clear semantic

these are the extensions mentioned above - cadvancedarbehavior http://www.yiiframework.com/extension/cadvancedarbehavior/ - eadvancedarbehavior http://www.yiiframework.com/extension/eadvancedarbehavior - advancedrelationsbehavior http://www.yiiframework.com/extension/advancedrelationsbehavior - saverbehavior http://www.yiiframework.com/extension/saverbehavior - with-related-behavior https://github.com/yiiext/with-related-behavior - CSaveRelationsBehavior http://code.google.com/p/yii-save-relations-ar-behavior/ - esaverelatedbehavior http://www.yiiframework.com/extension/esaverelatedbehavior

reviewed but did not take something out: - xrelationbehavior http://www.yiiframework.com/extension/xrelationbehavior - save-relations-ar-behavior http://www.yiiframework.com/extension/save-relations-ar-behavior

Many thanks to the authors of these extensions for inpiration and ideas.

Run the unit test

This behavior is covered by unit tests with 100% code coverage (ECompositeDbCriteria is currently not covered since composite pks are not fully supported yet). To run the unit tests you need phpunit installed and the test class requires php 5.3 or above.

  1. make sure yii framework is available under ./yii/framework you can do this by
    • cloning the yii git repo with git clone https://github.com/yiisoft/yii.git yii
    • or linking existing yii directory here with ln -s ../../path/to/yii yii
  2. run phpunit EActiveRecordRelationBehaviorTest.php or if you want coverage information in html, run phpunit --coverage-html tmp/coverage EActiveRecordRelationBehaviorTest.php

FAQ

When using a MANY_MANY relation, not changing it in any way and doing save() does it re-save relations or not?

It uses CActiveRecord::hasRelated() to check if a relation has been loaded or set and will only save if this is the case. It will re-save if you loaded and did not change, since it is not able to detect this. But re-saving does not mean entries in MANY_MANY table get deleted and re-inserted. It will only run a delete query, that does not match any rows if you did not touch records, so no row in db will be touched.

is it possible to save only related links (n-m table records) without re-saving model?

Currently not, will add this feature in the future: issue #16.

how can I delete a particular id from many-many relation? do I need to load all related records for this?

Currently you have to load all and re-assign the array. Will add an api for this; issue #16.

Total 10 comments

#12593 report it
CeBe at 2013/03/30 06:54pm
Thanks!

Thanks for your comment! Created an issue on github: https://github.com/yiiext/activerecord-relation-behavior/issues/36

#12592 report it
Andres Felipe Diaz at 2013/03/30 06:37pm
Does not work with relations defined in the metadata of the Active Record

Before I start, let me express that I am grateful this extension was done in the first place. It is something needed and I hope it is added to the core as it will accelerate the development proccess. That being said..

I have a behavior that is called categorizable. Basically I add the behavior to any model that I want to be able to receive categories. In the behavior, I add the relationship with categories through the metaData of the Active Record.

This extension takes the relations from the Active Record from the relations() method which is an array containing a configuration. However, this configuration does not refresh when the metadata of relations is updated.

Therefore this plugin does not work if you add a relationship via the metadata of the active record.

There are two possible solutions for this.

  1. Modify (Directly or Extending) the EActiveRecordRelationBehavior so that it reads the relations from the metadata and not from the relations() method.

  2. Modify the relations method so that it is updated when the metadata is updated....however this solution gives me the sense that will break other things :$ so I am going for the first one.

#10096 report it
zloypacifist at 2012/10/04 08:01am
problem

I have table object with PK field ID, and related tables objectTechparam and objectLocation with foreign keys FK_object. I need to add objects to my database, and when I create record to Object table I need to create records to objectTechparam and objectLocation with FK_object=Object.ID , but when I used this extension I have Exception "You can not save a record that has new related records!". How can I create record to table Object with creation records to related tables with this extension?

#9590 report it
CeBe at 2012/08/26 07:21pm
yii does not seem to work with MARS transactions

@josez https://github.com/yiisoft/yii/issues/112#issuecomment-8031565

#9587 report it
josez at 2012/08/25 03:58pm
Error on SQL server

Hi, i am getting following error when i try to use your extension (on saving a record). any clues what to do with it?

SQLSTATE[42000]: [Microsoft][SQL Server Native Client 11.0][SQL Server]A transaction that was started in a MARS batch is still active at the end of the batch. The transaction is rolled back.

#8596 report it
darkheir at 2012/06/13 02:12pm
Congratulations

It's a really good idea to take the better from all those extensions and create an ultimate one!

Works like a charm in my Yii application, congrats!

#8000 report it
Vitalets at 2012/05/02 09:47am
ok

@CeBe: ok! Also have a look on my comment to esaverelatedbehavior.

#7999 report it
CeBe at 2012/05/02 08:45am
added a FAQ

@vitalets: Thanks, added a FAQ section and answered your questions there.

Links to the other behaviors are clickable at (same content as here built with github pages): http://yiiext.github.com/activerecord-relation-behavior/

#7994 report it
Vitalets at 2012/05/02 06:24am
seems good

hi CeBe, great work!

some questions: 1. is it possible to save only related links (n-m table records) without re-saving model?
2. how can I delete a particular id from many-many relation? do I need to load all related records for this?
3. could you make clickable links to mentioned extensions in your post?

#7992 report it
phreak at 2012/05/02 02:16am
should be built

10x , very nice indeed. I think this functionality should be built in the framework.

Leave a comment

Please to leave your comment.

Create extension