The ESaveRelatedBehavior enables ActiveRecord models to save HAS_MANY relational active records and MANY_MANY relations along with the main model.

It provides two new methods:

saveWithRelated()
Saves the model and all specified related models
returns false if any model has not been saved

saveRelated()
Saves the specified related models only and
returns false if any model has not been saved

Features:
- handles many_many and has_many relations
- fills has_many relations with validated objects
- allows selection of scenario for has_many relations
- processes everything within a transaction
- relations can be set with data arrays or object arrays
- only specified relations are saved
- massive assignment works, since data is set on relation directly
- adds 2 methods to activeRecord, no use of beforeSave/afterSave,
therefore the behavior can be added to all activeRecord classes,
it will only do its work when the new methods are called
- uses standard SQL
- NO HANDLING OF COMPOSITE KEYS yet

So similar to many_many relations you can either pass an array of objects
or an array of data that represents the related objects.

You can also pass a single object:

$author->posts = newpost();
$author->saveWithRelated('posts');

If you are saving several relations you would do:

$author->saveWithRelated(array('relation1', 'relation2'));

If saving was not successful, then $author->posts will return
an array of validated post models which can be used to display
the tabular input form along with validation messages.
If saving was successful, then the relation will return the related models.

When updating a model then all records related to this model are first deleted.
(You can prevent deletion: see section "advanced usage")

You can also specify the scenario to be used for the insertion of the related models:

This can be useful if you have to validate aggregated values.
Say you have a field A in the model and field B in the related models.
Now you would like to check if the sum of values in field B equals the value in field A.
You can do this with an SQL statement in a validator which is active for the last scenario.

v1.4
Small code change to make it compatible to PHP versions < 5.3.
This has not been checked on all versions though, so I left requirements at PHP >= 5.3

v1.3
Corrected error: When using saveWithRelated for new models they can contain validation errors and hence will not be saved, so their id is not known. Therefore no models can be related yet. MANY_MANY-relations will now not be saved in this case. For HAS_MANY relations the models will only be validated (no saving attempt,because the foreign key would be missing).

v1.2
Corrected error: when checking for a model class for a mn-table
the autoloader caused an exception when the class file did not exist

v1.1
- added support for scenario selection when adding has_many relations
- changed the way options are specified when saving relations
(now an array is used so that 'append' and 'scenario' can both be specified

v1.0
- first version

Total 20 comments

I have a MANY_MANY relation, and when I call saveRelated(), on an object, sudden crash appear.
I start digging into ESaveRelatedBehavior code.
I have discovered that for a MANY_MANY relation, the following code is called:

$possibleModels = $model->findAll(newCDbCriteria(array(// find all models, that can be related (used to make sure only existing records are linked)'index' => $model->getMetaData()->tableSchema->primaryKey)));

As the comment said, this is used for make sure only existing records are linked.
That is correct, but with huge impact on the performance, because all the activeRecords form the related database would be instantiated.
I think it is sufficient to extract only the activeRecords that are candidate for update, to check if they exists:

@Anonymous Joe
Thank you for the bug report, today I got the same problem.

@Pasta
Thank you for your suggestion to fix the bug.
The code change does remove the bug but will also remove a feature:
On HAS_MANY relations the saved related objects should still be
validated even when the parent object had errors on saving.

@undsof: For this extension it actually was desired behavior to only save safe attributes for the related models. This data usually comes from a web form. But you are right, there could be situations in which you would like to set extra attributes on the array that do not come from user input. If I'll ever need that functionality I will include it in the extension. For the moment I will keep it simple.

If the linking table is a table name with prefix, say {{some_table}}, your regular expression with only retrieve the "some_table" without "{{}}" and result error in getting table name. I suggest to change the regex to this:

2.When you use append param you will have to make sure that the new records are not dublicates of existing records, the behavior does not handle that.
I've modified code, so now rows are not dublicated in nmTable.

Modified code (to be inserted into ESaveRelatedBehavior.php starting from line 209):

If you use the 'saveWithRelated' method, and the model you're saving has errors, the behaviour is going to crash in the dbcriteria construction. To avoid this, you've encapsulate the foreach just after the second if clause:

Thanx for the feedback.
I have not included delete methods, because many2many-relations and hasMany-related objects can be delete by passing array() to the relation.
For deleting the object itself and the related objects I say:
When using a relational database, then you can setup the foreign keys in a way that related objects are automatically deleted as well.

For many_many relations this extension only manages the data in the connecting mn-table. It does not handle creation of the related objects (if it did, then passing associative arrays could be useful). Personally I only ever pass an array of primary keys (like array(1,6,9) - usually coming from a checkboxlist) to this kind of relation.

You can set a single object for has_many relations.
Currently there is no way to check for having passed exactly one object.
You can also set the relation to array() to delete all related objects.

There are some reasons why I get all foreign models:
The extension does not throw an error, when an invalid object-id is passed, the id is just ignored. If you do insert a wrong id in a DBMS like Postgres, a foreign key error will be thrown and the transaction automatically rolls back,
which was not the kind of behaviour I wanted.
The extension is mainly build for handling user input, i.e. the user clicking a few checkboxes to select related objects. When handling many records, this could be a performance issue, the the extension would have to be changed. Instead of getting all related objects, a SQL statement could be generated to get all valid ids. I may actually do this, its just more lines of code. Or as you suggested, the DB FK checks could be used.

I am not working on composite keys support at the moment.
So far I never needed it, I have cases where it could be used, but I prefer to introduce an extra primary key (usualy an id column) and set a unique constraint on the other multi-column PK. This is then so much easier to handle.