Yii Framework Forum

[EXTENSION] CSaveRelationsBehavior


(Juban_) #1

Welcome to the CSaveRelationsBehavior discussion topic.

The CSaveRelationsBehavior enables ActiveRecord model to save HasMany and ManyMany relational active records along with the main model.

This component and its documentation are available from the official Yii Extension repository here.

I hope this extension will be helpfull for some of you.

Feel free to give your feedback or enhancements requests here.

Regards.


(Juban_) #2

You can now see this behavior in action by downloading the demo application from the download section.

It is designed to run under Yii 1.0 but a Yii 1.1 compatible version is coming soon.

Demos for Yii 1.0 and 1.1 are now both available.


(Chris De Kok) #3

:) Thanks it looks very nice…


(Juban_) #4

Thank you for your feedback :)

Stay tuned for the next release coming soon including:

  • possiblity to define a validation scenario for relations

  • onDelete handling


(Chris De Kok) #5

I am having a problem setting it up… I tried the demo and there it works fine so I must be doing something wrong…

I keep getting:

It always throws it after:

$relobj = $relModel->findByPk($criteria);

on line 151:

criteria looks like this:

array(1) {

["id"]=>

string(1) "3"

}

MY controller:



$pages = $_POST['Pages'];


            $model->setRelationRecords('pages',is_array(@$_POST['Pages']) ? $_POST['Pages'] : array());


            $model->save();

Model:



public function relations() {


        // NOTE: you may need to adjust the relation name and the related


        // class name for the relations automatically generated below.


        return array(


            'pages' => array(self::HAS_MANY, 'Pages', 'book_id',


                'order' => 'pages.page_id ASC',


            ),


            'user' => array(self::BELONGS_TO, 'User', 'user_id'),


            'comments' => array(self::HAS_MANY, 'Comments', 'book_id',


                'order' => 'comments.created DESC',


            ),


            'commentCount' => array(self::STAT, 'Comments', 'book_id',


                'condition'=> 'status='.Comments::STATUS_APPROVED),


        );


    }

and view:



<?php echo CHtml::beginForm(null, 'POST', array()); ?>


    <div class="pages clear">


        <?php foreach($model->pages as $idx => $page):?>


                <div class="page">


                    <label>Title page: <?php echo $idx + 1;?></label>


                    <?php echo CHtml::activeTextField($page, "[$idx]title"); ?><br />


                    <?php echo CHtml::error($page,"title"); ?>


                    <?php echo CHtml::image( Yii::app()->request->baseUrl.'/'.$page->image, 'page');?>


                </div>


        <?php endforeach;?>


    </div>


    <div class="clear"></div>


    <?php echo CHtml::submitButton();?>


<?php echo CHtml::endForm();?>

Any idea what could cause this error ? :(


(Juban_) #6

Hi,

I’m trying to reproduce that and will come back to you as soon as possible.

Regards.

Alban.


(Juban_) #7

OK. My guess is that the "id" field in your Pages table is not set as a Primary Key.

Could you confirm?


(Juban_) #8

Hi,

I have updated the behavior (1.0.2).

Could you try it and let me know if the issue still occurs?

Regards.

Alban.


(Juban_) #9

Hi,

A new version of the behavior (1.0.3) is available for download that should deal better with HAS_MANY relations.

Please, download the enhanced demonstration application to see how to deal with related primary keys (look at the comments exemple in the library form).

Regards.

Alban.


(A) #10

Hi!

I’ve modified it to support more complicated models.

Please let me know whether you changed my code or not IF you accept my changes.




<?php

/**


 * http://www.yiiframework.com/extension/save-relations-ar-behavior/


 * CSaveRelationsBehavior class file.

 *

 * @author Alban Jubert <alban.jubert@trinidev.fr>

 * @link http://www.trinidev.fr/

 * @version 1.0.3

 */


/*

 * The CSaveRelationBehavior enables ActiveRecord model to save HasMany and ManyMany 

 * relational active records along with the main model

 * 

 * Requirements:

 * Yii Framework 1.0.4 or later

 * 

 * Installation:

 * Extract the release file under `protected/components`

 * 

 * Usage:

 * - Add the following code to the models you wish to add this behavior to:

 * public function behaviors(){

 * 		return array('CSaveRelationsBehavior' => array('class' => 'application.components.CSaveRelationsBehavior'));

 * }

 * In your controler, to save the relations data, simply call

 * $model->setRelationRecords('relationName',$data);

 * $model->save();

 * - For ManyMany relations, $data is either an array of foreign key values (ie. array(2,5,43)) or

 * an array of associative arrays giving the composite foreign keys values of the related model

 * (ie. array(array('pk1'=>2,'pk2'=>'fr'),array('pk1'=>5,'pk2'=>'en'))

 * You will typically get this data from some checkboxes values listing the ids of the related model

 * - For HasMany relations, $data should be set as an array of associative arrays giving the attributes values

 * of the related model (ie. array(array('id'=>123, 'name'=>'someValue', 'visible'=>true),

 * array('id'=>456, 'name'=>'someOtherValue', 'visible'=>false));

 * You can get this data by using the tabular input technique within the form of the main model 

 * (http://www.yiiframework.com/doc/guide/form.table)

 * 

 * In both cases, the foreign keys related to the main model will automatically be populated

 * with its primary key(s) value(s).

 * Most of the time, you will call the setRelationRecords that way:

 * $model->setRelationRecords('relationName',is_array(@$_POST['ModelName']) ? $_POST['ModelName'] : array());

 * 

 * By default, the behavior will handle the save operation in a transactional way

 * so that if anything goes wrong during the save of some related data

 * your relational integrity will not be affected.

 * If you prefer to handle this yourself, you can set the 'transactional' property of the behavior to false.

 * Also, if any error occurs during the save process, the hasError property will be set to true.

 * 

 * Additional features:

 * - $model->addSaveRelation('relationName'[,'customErrorMessage'])

 * You can use this method to force the save of some relation. 

 * You can also set the error message of the relation by using the second parameter (see setSaveRelationMessage below)

 * - $model->removeSaveRelation('relationName')

 * Simply do the oposite

 * - $model->setSaveRelationMessage('relationName','customErrorMessage')

 * Set the message to be shown in the error summary of the main model

 */


class CSaveRelationsBehavior extends CActiveRecordBehavior {

	

	public $relations = array();

	public $transactional = true;

	public $hasError = false;

	public $deleteRelatedRecords = true;

	private $transaction;

	

	private function initSaveRelation($relation){

		$model = $this->owner;

		if(!array_key_exists($relation,$model->relations())) 

			throw new CDbException('CSaveRelatedBehavior could not find the "'.$relation.'" relation in the model.');

		if(!array_key_exists($relation,$this->relations)) {

			Yii::trace("Init {$relation} relation",'application.components.CSaveRelatedBehavior');

			$this->relations[$relation]=array();

		}

	}

	

	public function setRelationRecords($relation,$data=null,$merge=false) {

		// TODO - Make fewer SQL requests to validate and load related models data

		$this->addSaveRelation($relation);

		$model = $this->owner;

		$activeRelation = $model->getActiveRelation($relation);

		if($activeRelation instanceOf CHasManyRelation || $activeRelation instanceOf CManyManyRelation) {

			if(!$merge) $model->{$relation} = array();

			$relationClassName = $activeRelation->className;

			$relationForeignKey = $activeRelation->foreignKey;

			$criteria = array();

			if($activeRelation instanceOf CManyManyRelation) {

				$schema = $model->getCommandBuilder()->getSchema();

				preg_match('/^\s*(.*?)\((.*)\)\s*$/',$relationForeignKey,$matches);

				$joinTable=$schema->getTable($matches[1]);

				$fks=preg_split('/[\s,]+/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);

				$relModel = new $relationClassName;

				$pks = array();

				$fkDefined=true;

				foreach($fks as $i=>$fk) {

					if(isset($joinTable->foreignKeys[$fk])) {

						list($tableName,$pk)=$joinTable->foreignKeys[$fk];

						if($schema->compareTableNames($relModel->tableSchema->rawName,$tableName)) {

							$pks[] = $pk;

						}

					}

					else {

						$fkDefined=false;

						break;

					}

				}

				if(!$fkDefined) {

					$pks = array();

					foreach($fks as $i=>$fk)

					{

						if($i<count($model->tableSchema->primaryKey))

						{

							$pks[] = is_array($model->tableSchema->primaryKey) ? $model->tableSchema->primaryKey[$i] : $model->tableSchema->primaryKey;

						}

					}

				}

				if(!is_null($data)) {

					foreach($data as $key=>$value) {

						$relobj = null;

						$relModel = new $relationClassName;

						if(is_array($value)) {

							foreach($pks as $pk) {

								$criteria[$pk] = $value[$pk];

							}

						}

						else {

							$criteria[$pks[0]] = $value;

						}

						$relobj = $relModel->findByAttributes($criteria);

						if(!($relobj instanceof $relationClassName)) $relobj = new $relationClassName;

						$relobj->attributes = $value;

						$model->addRelatedRecord($relation,$relobj,$key);

					}

				}

			}

			else 

			{

				$fks=preg_split('/[\s,]+/',$relationForeignKey,-1,PREG_SPLIT_NO_EMPTY);

				if(!is_null($data)) {

					foreach($data as $key=>$value) {

						$relobj = null;

						if(!$model->isNewRecord) {

							$criteria = array();

							$relModel = new $relationClassName;

							$relationPrimaryKeys = $relModel->tableSchema->primaryKey;

							if(is_array($value)) {

								if(is_array($relationPrimaryKeys)) {

									foreach($relationPrimaryKeys as $relationPrimaryKey){

										if(!in_array($relationPrimaryKey,$fks)) {

											if(isset($value[$relationPrimaryKey])) $criteria[$relationPrimaryKey] = $value[$relationPrimaryKey];

										}

										else {

											$criteria[$relationPrimaryKey] = $model->primaryKey;

										}

									}

								}

								else{

									if(!in_array($relationPrimaryKeys,$fks)) {

										if(isset($value[$relationPrimaryKeys])) $criteria[$relationPrimaryKeys] = $value[$relationPrimaryKeys];

									}

									else {

										$criteria[$relationPrimaryKeys] = $model->primaryKey;

									}

								}

							}

							else {

								$criteria = array($relationPrimaryKeys=>$value);

							}

							if(count($criteria)) $relobj = $relModel->findByAttributes($criteria);

						}

						if(!($relobj instanceof $relationClassName)) $relobj = new $relationClassName;

						foreach($value as $prop=>$val) $relobj->{$prop} = $val;

						$model->addRelatedRecord($relation,$relobj,$key);

					}

				}

			}

		}

	}

	

	public function addSaveRelation($relation,$message=null){

		$this->initSaveRelation($relation);

		$this->relations[$relation] = CMap::mergeArray($this->relations[$relation],array('save'=>true));

		if(!is_null($message)) $this->setSaveRelationMessage($relation,$message);

	}

	

	public function removeSaveRelation($relation){

		$model = $this->owner;

		if(!array_key_exists($relation,$model->relations())) 

			throw new CDbException('CSaveRelatedBehavior could not find the "'.$relation.'" relation in the model.');

		if(array_key_exists($relation,$this->relations)) {

			Yii::trace("Removing {$relation} relation to save",'application.components.CSaveRelatedBehavior');

			$this->relations[$relation] = CMap::mergeArray($this->relations[$relation],array('save'=>false));

		}

	}

	

	public function setRelationScenario($relation,$scenario){

		$this->initSaveRelation($relation);

		$this->relations[$relation] = CMap::mergeArray($this->relations[$relation],array('scenario'=>$scenario));	

	}

	

	public function setSaveRelationMessage($relation,$message) {

		$this->initSaveRelation($relation);

		$this->relations[$relation] = CMap::mergeArray($this->relations[$relation],array('message'=>$message));

	}

	

	public function beforeValidate($event) {

		$model = $this->owner;

		foreach($this->relations as $relation=>$params) {

			if(isset($params['save']) && $params['save']==true) {

				$activeRelation = $model->getActiveRelation($relation);

				$validRelation = true;

				if(!($activeRelation instanceOf CManyManyRelation)) {

					foreach($model->{$relation} as $relatedRecord) {

						if(isset($params['scenario'])) $relatedRecord->scenario = $params['scenario'];

						$validRelation = $validRelation && $relatedRecord->validate();

					}

					if(!$validRelation) 

						$model->addError($relation,isset($params['message']) ? $params['message'] : "An error occured during the save of {$relation}");				

				}

				$this->relations[$relation]['valid'] = $validRelation;

			}

		}

	}

	

	public function beforeSave($event) {

		$model = $this->owner;

		$valid =  true;

		foreach($this->relations as $relation=>$params) {

			if(isset($params['save']) && $params['save']==true) {

				$valid = $valid && $this->relations[$relation]['valid'];

			}

		}

		if($valid && $this->transactional && !$model->dbConnection->currentTransaction) {

			Yii::trace("beforeSave start transaction",'application.components.CSaveRelatedBehavior');

			$this->transaction=$model->dbConnection->beginTransaction();

		}

		$event->isValid = $valid;

	}

	

	public function afterSave($event) {

		$model = $this->owner;

		try{

			foreach($this->relations as $relation=>$params) {

				if(isset($params['save']) && $params['save']==true) {

					Yii::trace("saving {$relation} related records.",'application.components.CSaveRelatedBehavior');

					$activeRelation = $model->getActiveRelation($relation);

					$relationClassName = $activeRelation->className;

					$relationForeignKey = $activeRelation->foreignKey;

					$keysToKeep = array();

					if($activeRelation instanceOf CManyManyRelation) {

						// ManyMany relation : save relation to the many to many relation table

						$schema = $model->getCommandBuilder()->getSchema();

						preg_match('/^\s*(.*?)\((.*)\)\s*$/',$relationForeignKey,$matches);

						$joinTable=$schema->getTable($matches[1]);

						$fks=preg_split('/[\s,]+/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);

						$fksFieldNames = array();

						$fksParamNames = array();

						foreach($fks as $fk) {

							$fksFieldNames[] = $schema->quoteColumnName($fk);

							$fksParamNames[] = ':'.$fk;

						}

						$sql="INSERT IGNORE INTO ".$joinTable->rawName." (".implode(', ',$fksFieldNames).") VALUES(".implode(', ',$fksParamNames).")";

						$baseParams = array();

						$baseCriteriaCondition = array();

						reset($fks);

						foreach($fks as $i=>$fk) {

							if(isset($joinTable->foreignKeys[$fk])) {

								list($tableName,$pk)=$joinTable->foreignKeys[$fk];

								if($schema->compareTableNames($model->tableSchema->rawName,$tableName)) {

									$baseCriteriaCondition[$fk] = $baseParams[':'.$fk] = $model->{$pk};

								}

							}

						}

						$relModel = new $relationClassName;

						foreach($model->{$relation} as $idx=>$relatedRecord) {

							$relParams = array();

							reset($fks);

							foreach($fks as $i=>$fk) {

								if(isset($joinTable->foreignKeys[$fk])) {

									list($tableName,$pk)=$joinTable->foreignKeys[$fk];

									if($schema->compareTableNames($relModel->tableSchema->rawName,$tableName)) {

										$keysToKeep[$fk][] = $relParams[':'.$fk] = $relatedRecord->{$pk};

									}

								}

							}

							$model->getCommandBuilder()->createSqlCommand($sql,$baseParams+$relParams)->execute();

						}

						// Delete removed records

						$criteria = new CDbCriteria;

						$criteria->addColumnCondition($baseCriteriaCondition);

						foreach($keysToKeep as $fk=>$values)

							$criteria->addInCondition($fk,$values,'AND NOT');

						$model->getCommandBuilder()->createDeleteCommand($joinTable->name,$criteria)->execute();

					}

					else {

						// HasMany relation : save related models

						foreach($model->{$relation} as $relatedRecord) {

							if($relatedRecord->isNewRecord) {

								if(is_array($relationForeignKey)) {

									foreach($relationForeignKey as $fk) {

										$relatedRecord->{$fk} = $model->primaryKey[$fk];

									}

								}

								else {

									$relatedRecord->{$relationForeignKey} = $model->primaryKey;

								}

							}

							if($relatedRecord->save()) {

								$relationPrimaryKeys = $relatedRecord->tableSchema->primaryKey;

								if(is_array($relationPrimaryKeys)) {

									foreach($relationPrimaryKeys as $relationPrimaryKey){

										if($relationPrimaryKey!=$relationForeignKey) $keysToKeep[$relationPrimaryKey][] = $relatedRecord->{$relationPrimaryKey};

									}

								}

								else{

									$keysToKeep[$relationPrimaryKeys][] = $relatedRecord->{$relationPrimaryKeys};

								}

							}

							else {

								throw new CException("Invalid related record");

							}

						}




						


/*						foreach($keysToKeep as $key)

                                                {

                                                    $relatedRecord = new $relationClassName;

                                                    $criteria = new CDbCriteria;

                                                    $criteria->addColumnCondition(array($relationForeignKey=>$model->primaryKey));

                                                    foreach ($key as $fk=>$value)

                                                    {

                                                        $criteria->addInCondition($fk,array($value), 'AND NOT');

                                                    }

                                                    //$criteria->addColumnCondition($key, 'AND', 'AND NOT');

                                                    Yii::log('123'.print_r($criteria->toArray(), true),'error');

                                                    $relatedRecord->deleteAll($criteria);

                                                }

*/





						$relatedRecord = new $relationClassName;

						$criteria = new CDbCriteria;

						$criteria->addColumnCondition(array($relationForeignKey=>$model->primaryKey));

						if (count($keysToKeep)===1)

						{

							foreach ( $keysToKeep as $fk => $values )

								$criteria->addInCondition ( $fk, $values, 'AND NOT' );

						} else {

							$_keysToKeep = array();

							foreach ( $keysToKeep as $field=>$fValues )

							{

								foreach ( $fValues as $key=>$value )

								{

									$_keysToKeep[$key][$field] = $value;

								}								

							}

							

							foreach($_keysToKeep as $record)

							{

								$criteria->addColumnCondition ($record, 'AND', 'AND NOT' );

							}

						}

						

						$relatedRecord->deleteAll ( $criteria );




					}

				}

			}

			unset($relation);

			if($this->transactional && $this->transaction) $this->transaction->commit();

		}

		catch(Exception $e)

		{

			Yii::trace("An error occured during the save operation for related records : ".$e->getMessage(),'application.components.CSaveRelatedBehavior');

			$this->hasError = true;

			if(isset($relation)) $model->addError($relation,isset($this->relations[$relation]['message']) ? $this->relations[$relation]['message'] : "An error occured during the save of {$relation}");

			if($this->transactional && $this->transaction) $this->transaction->rollBack();

		}

	}

	

	public function beforeDelete($event) {

		$model = $this->owner;

		if($this->transactional && !$model->dbConnection->currentTransaction) {

			Yii::trace("beforeDelete start transaction",'application.components.CSaveRelatedBehavior');

			$this->transaction=$model->dbConnection->beginTransaction();

		}

	}

	

	public function afterDelete($event) {

		if($this->deleteRelatedRecords) {

			$model = $this->owner;

			try{

				foreach($model->relations() as $relation=>$params) {

					$activeRelation = $model->getActiveRelation($relation);

					if(is_object($activeRelation) && ($activeRelation instanceOf CManyManyRelation || $activeRelation instanceOf CHasManyRelation || $activeRelation instanceOf CHasOneRelation)) {

						Yii::trace("deleting {$relation} related records.",'application.components.CSaveRelatedBehavior');

						$relationClassName = $activeRelation->className;

						$relationForeignKey = $activeRelation->foreignKey;

						if($activeRelation instanceOf CManyManyRelation) {

							// ManyMany relation : delete related records from the many to many relation table

							$schema = $model->getCommandBuilder()->getSchema();

							preg_match('/^\s*(.*?)\((.*)\)\s*$/',$relationForeignKey,$matches);

							$joinTable=$schema->getTable($matches[1]);

							$fks=preg_split('/[\s,]+/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);

							$baseParams = array();

							$baseCriteriaCondition = array();

							reset($fks);

							foreach($fks as $i=>$fk) {

								if(isset($joinTable->foreignKeys[$fk])) {

									list($tableName,$pk)=$joinTable->foreignKeys[$fk];

									if($schema->compareTableNames($model->tableSchema->rawName,$tableName)) {

										$baseCriteriaCondition[$fk] = $baseParams[':'.$fk] = $model->{$pk};

									}

								}

							}

							// Delete records

							$criteria = new CDbCriteria;

							$criteria->addColumnCondition($baseCriteriaCondition);

							$model->getCommandBuilder()->createDeleteCommand($joinTable->name,$criteria)->execute();

						}

						else {

							// HasMany & HasOne relation : delete related records

							$relatedRecord = new $relationClassName;

							$criteria = new CDbCriteria;

							$criteria->addColumnCondition(array($relationForeignKey=>$model->primaryKey));

							$relatedRecord->deleteAll($criteria);

						}

					}

				}

				unset($relation);

				if($this->transactional && $this->transaction) $this->transaction->commit();

			}

			catch(Exception $e)

			{

				Yii::trace("An error occured during the delete operation for related records : ".$e->getMessage(),'application.components.CSaveRelatedBehavior');

				$this->hasError = true;

				if(isset($relation)) $model->addError($relation,"An error occured during the delete operation of {$relation}");

				if($this->transactional && $this->transaction) $this->transaction->rollBack();

			}

		}

	}

}




(Chris De Kok) #11

Is there anyway to just delete one relation? I could not really find it… other then unsetting the relation array.




public function deleteBook($idx) {

		$libraryBookArray = $this->libraryBooks;

		unset($libraryBookArray[$idx]);

		$this->libraryBooks = $libraryBookArray;

}



Also how does this exactly work? :)


(A) #12

Modified it a liitle bit more:

-added back-quotes to column names




<?php

/**


 * http://www.yiiframework.com/extension/save-relations-ar-behavior/


 * CSaveRelationsBehavior class file.

 *

 * @author Alban Jubert <alban.jubert@trinidev.fr>

 * @link http://www.trinidev.fr/

 * @version 1.0.3

 */


/*

 * The CSaveRelationBehavior enables ActiveRecord model to save HasMany and ManyMany 

 * relational active records along with the main model

 * 

 * Requirements:

 * Yii Framework 1.0.4 or later

 * 

 * Installation:

 * Extract the release file under `protected/components`

 * 

 * Usage:

 * - Add the following code to the models you wish to add this behavior to:

 * public function behaviors(){

 * 		return array('CSaveRelationsBehavior' => array('class' => 'application.components.CSaveRelationsBehavior'));

 * }

 * In your controler, to save the relations data, simply call

 * $model->setRelationRecords('relationName',$data);

 * $model->save();

 * - For ManyMany relations, $data is either an array of foreign key values (ie. array(2,5,43)) or

 * an array of associative arrays giving the composite foreign keys values of the related model

 * (ie. array(array('pk1'=>2,'pk2'=>'fr'),array('pk1'=>5,'pk2'=>'en'))

 * You will typically get this data from some checkboxes values listing the ids of the related model

 * - For HasMany relations, $data should be set as an array of associative arrays giving the attributes values

 * of the related model (ie. array(array('id'=>123, 'name'=>'someValue', 'visible'=>true),

 * array('id'=>456, 'name'=>'someOtherValue', 'visible'=>false));

 * You can get this data by using the tabular input technique within the form of the main model 

 * (http://www.yiiframework.com/doc/guide/form.table)

 * 

 * In both cases, the foreign keys related to the main model will automatically be populated

 * with its primary key(s) value(s).

 * Most of the time, you will call the setRelationRecords that way:

 * $model->setRelationRecords('relationName',is_array(@$_POST['ModelName']) ? $_POST['ModelName'] : array());

 * 

 * By default, the behavior will handle the save operation in a transactional way

 * so that if anything goes wrong during the save of some related data

 * your relational integrity will not be affected.

 * If you prefer to handle this yourself, you can set the 'transactional' property of the behavior to false.

 * Also, if any error occurs during the save process, the hasError property will be set to true.

 * 

 * Additional features:

 * - $model->addSaveRelation('relationName'[,'customErrorMessage'])

 * You can use this method to force the save of some relation. 

 * You can also set the error message of the relation by using the second parameter (see setSaveRelationMessage below)

 * - $model->removeSaveRelation('relationName')

 * Simply do the oposite

 * - $model->setSaveRelationMessage('relationName','customErrorMessage')

 * Set the message to be shown in the error summary of the main model

 */


class CSaveRelationsBehavior extends CActiveRecordBehavior {

	

	public $relations = array();

	public $transactional = true;

	public $hasError = false;

	public $deleteRelatedRecords = true;

	private $transaction;

	

	private function initSaveRelation($relation){

		$model = $this->owner;

		if(!array_key_exists($relation,$model->relations())) 

			throw new CDbException('CSaveRelatedBehavior could not find the "'.$relation.'" relation in the model.');

		if(!array_key_exists($relation,$this->relations)) {

			Yii::trace("Init {$relation} relation",'application.components.CSaveRelatedBehavior');

			$this->relations[$relation]=array();

		}

	}

	

	public function setRelationRecords($relation,$data=null,$merge=false) {

		// TODO - Make fewer SQL requests to validate and load related models data

		$this->addSaveRelation($relation);

		$model = $this->owner;

		$activeRelation = $model->getActiveRelation($relation);

		if($activeRelation instanceOf CHasManyRelation || $activeRelation instanceOf CManyManyRelation) {

			if(!$merge) $model->{$relation} = array();

			$relationClassName = $activeRelation->className;

			$relationForeignKey = $activeRelation->foreignKey;

			$criteria = array();

			if($activeRelation instanceOf CManyManyRelation) {

				$schema = $model->getCommandBuilder()->getSchema();

				preg_match('/^\s*(.*?)\((.*)\)\s*$/',$relationForeignKey,$matches);

				$joinTable=$schema->getTable($matches[1]);

				$fks=preg_split('/[\s,]+/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);

				$relModel = new $relationClassName;

				$pks = array();

				$fkDefined=true;

				foreach($fks as $i=>$fk) {

					if(isset($joinTable->foreignKeys[$fk])) {

						list($tableName,$pk)=$joinTable->foreignKeys[$fk];

						if($schema->compareTableNames($relModel->tableSchema->rawName,$tableName)) {

							$pks[] = $pk;

						}

					}

					else {

						$fkDefined=false;

						break;

					}

				}

				if(!$fkDefined) {

					$pks = array();

					foreach($fks as $i=>$fk)

					{

						if($i<count($model->tableSchema->primaryKey))

						{

							$pks[] = is_array($model->tableSchema->primaryKey) ? $model->tableSchema->primaryKey[$i] : $model->tableSchema->primaryKey;

						}

					}

				}

				if(!is_null($data)) {

					foreach($data as $key=>$value) {

						$relobj = null;

						$relModel = new $relationClassName;

						if(is_array($value)) {

							foreach($pks as $pk) {

								$criteria[$pk] = $value[$pk];

							}

						}

						else {

							$criteria[$pks[0]] = $value;

						}

						$relobj = $relModel->findByAttributes($criteria);

						if(!($relobj instanceof $relationClassName)) $relobj = new $relationClassName;

						$relobj->attributes = $value;

						$model->addRelatedRecord($relation,$relobj,$key);

					}

				}

			}

			else 

			{

				$fks=preg_split('/[\s,]+/',$relationForeignKey,-1,PREG_SPLIT_NO_EMPTY);

				if(!is_null($data)) {

					foreach($data as $key=>$value) {

						$relobj = null;

						if(!$model->isNewRecord) {

							$criteria = array();

							$relModel = new $relationClassName;

							$relationPrimaryKeys = $relModel->tableSchema->primaryKey;

							if(is_array($value)) {

								if(is_array($relationPrimaryKeys)) {

									foreach($relationPrimaryKeys as $relationPrimaryKey){

										if(!in_array($relationPrimaryKey,$fks)) {

											if(isset($value[$relationPrimaryKey])) $criteria[$relationPrimaryKey] = $value[$relationPrimaryKey];

										}

										else {

											$criteria[$relationPrimaryKey] = $model->primaryKey;

										}

									}

								}

								else{

									if(!in_array($relationPrimaryKeys,$fks)) {

										if(isset($value[$relationPrimaryKeys])) $criteria[$relationPrimaryKeys] = $value[$relationPrimaryKeys];

									}

									else {

										$criteria[$relationPrimaryKeys] = $model->primaryKey;

									}

								}

							}

							else {

								$criteria = array($relationPrimaryKeys=>$value);

							}

							if(count($criteria)) $relobj = $relModel->findByAttributes($criteria);

						}

						if(!($relobj instanceof $relationClassName)) $relobj = new $relationClassName;

						foreach($value as $prop=>$val) $relobj->{$prop} = $val;

						$model->addRelatedRecord($relation,$relobj,$key);

					}

				}

			}

		}

	}

	

	public function addSaveRelation($relation,$message=null){

		$this->initSaveRelation($relation);

		$this->relations[$relation] = CMap::mergeArray($this->relations[$relation],array('save'=>true));

		if(!is_null($message)) $this->setSaveRelationMessage($relation,$message);

	}

	

	public function removeSaveRelation($relation){

		$model = $this->owner;

		if(!array_key_exists($relation,$model->relations())) 

			throw new CDbException('CSaveRelatedBehavior could not find the "'.$relation.'" relation in the model.');

		if(array_key_exists($relation,$this->relations)) {

			Yii::trace("Removing {$relation} relation to save",'application.components.CSaveRelatedBehavior');

			$this->relations[$relation] = CMap::mergeArray($this->relations[$relation],array('save'=>false));

		}

	}

	

	public function setRelationScenario($relation,$scenario){

		$this->initSaveRelation($relation);

		$this->relations[$relation] = CMap::mergeArray($this->relations[$relation],array('scenario'=>$scenario));	

	}

	

	public function setSaveRelationMessage($relation,$message) {

		$this->initSaveRelation($relation);

		$this->relations[$relation] = CMap::mergeArray($this->relations[$relation],array('message'=>$message));

	}

	

	public function beforeValidate($event) {

		$model = $this->owner;

		foreach($this->relations as $relation=>$params) {

			if(isset($params['save']) && $params['save']==true) {

				$activeRelation = $model->getActiveRelation($relation);

				$validRelation = true;

				if(!($activeRelation instanceOf CManyManyRelation)) {

					foreach($model->{$relation} as $relatedRecord) {

						if(isset($params['scenario'])) $relatedRecord->scenario = $params['scenario'];

						$validRelation = $validRelation && $relatedRecord->validate();

					}

					if(!$validRelation) 

						$model->addError($relation,isset($params['message']) ? $params['message'] : "An error occured during the save of {$relation}");				

				}

				$this->relations[$relation]['valid'] = $validRelation;

			}

		}

	}

	

	public function beforeSave($event) {

		$model = $this->owner;

		$valid =  true;

		foreach($this->relations as $relation=>$params) {

			if(isset($params['save']) && $params['save']==true) {

				$valid = $valid && $this->relations[$relation]['valid'];

			}

		}

		if($valid && $this->transactional && !$model->dbConnection->currentTransaction) {

			Yii::trace("beforeSave start transaction",'application.components.CSaveRelatedBehavior');

			$this->transaction=$model->dbConnection->beginTransaction();

		}

		$event->isValid = $valid;

	}

	

	public function afterSave($event) {

		$model = $this->owner;

		try{

			foreach($this->relations as $relation=>$params) {

				if(isset($params['save']) && $params['save']==true) {

					Yii::trace("saving {$relation} related records.",'application.components.CSaveRelatedBehavior');

					$activeRelation = $model->getActiveRelation($relation);

					$relationClassName = $activeRelation->className;

					$relationForeignKey = $activeRelation->foreignKey;

					$keysToKeep = array();

					if($activeRelation instanceOf CManyManyRelation) {

						// ManyMany relation : save relation to the many to many relation table

						$schema = $model->getCommandBuilder()->getSchema();

						preg_match('/^\s*(.*?)\((.*)\)\s*$/',$relationForeignKey,$matches);

						$joinTable=$schema->getTable($matches[1]);

						$fks=preg_split('/[\s,]+/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);

						$fksFieldNames = array();

						$fksParamNames = array();

						foreach($fks as $fk) {

							$fksFieldNames[] = $schema->quoteColumnName($fk);

							$fksParamNames[] = ':'.$fk;

						}

						$sql="INSERT IGNORE INTO ".$joinTable->rawName." (".implode(', ',$fksFieldNames).") VALUES(".implode(', ',$fksParamNames).")";

						$baseParams = array();

						$baseCriteriaCondition = array();

						reset($fks);

						foreach($fks as $i=>$fk) {

							if(isset($joinTable->foreignKeys[$fk])) {

								list($tableName,$pk)=$joinTable->foreignKeys[$fk];

								if($schema->compareTableNames($model->tableSchema->rawName,$tableName)) {

									$baseCriteriaCondition["`$fk`"] = $baseParams[':'.$fk] = $model->{$pk};

								}

							}

						}

						$relModel = new $relationClassName;

						foreach($model->{$relation} as $idx=>$relatedRecord) {

							$relParams = array();

							reset($fks);

							foreach($fks as $i=>$fk) {

								if(isset($joinTable->foreignKeys[$fk])) {

									list($tableName,$pk)=$joinTable->foreignKeys[$fk];

									if($schema->compareTableNames($relModel->tableSchema->rawName,$tableName)) {

										$keysToKeep[$fk][] = $relParams[':'.$fk] = $relatedRecord->{$pk};

									}

								}

							}

							$model->getCommandBuilder()->createSqlCommand($sql,$baseParams+$relParams)->execute();

						}

						// Delete removed records

						$criteria = new CDbCriteria;

						$criteria->addColumnCondition($baseCriteriaCondition);

						foreach($keysToKeep as $fk=>$values)

							$criteria->addInCondition("`$fk`",$values,'AND NOT');

						$model->getCommandBuilder()->createDeleteCommand($joinTable->name,$criteria)->execute();

					}

					else {

						// HasMany relation : save related models

						foreach($model->{$relation} as $relatedRecord) {

							if($relatedRecord->isNewRecord) {

								if(is_array($relationForeignKey)) {

									foreach($relationForeignKey as $fk) {

										$relatedRecord->{$fk} = $model->primaryKey[$fk];

									}

								}

								else {

									$relatedRecord->{$relationForeignKey} = $model->primaryKey;

								}

							}

							if($relatedRecord->save()) {

								$relationPrimaryKeys = $relatedRecord->tableSchema->primaryKey;

								if(is_array($relationPrimaryKeys)) {

									foreach($relationPrimaryKeys as $relationPrimaryKey){

										if($relationPrimaryKey!=$relationForeignKey) $keysToKeep[$relationPrimaryKey][] = $relatedRecord->{$relationPrimaryKey};

									}

								}

								else{

									$keysToKeep[$relationPrimaryKeys][] = $relatedRecord->{$relationPrimaryKeys};

								}

							}

							else {

								throw new CException("Invalid related record");

							}

						}




						$relatedRecord = new $relationClassName;

						$criteria = new CDbCriteria;

						$criteria->addColumnCondition(array("`$relationForeignKey`"=>$model->primaryKey));

						if (count($keysToKeep)===1)

						{

							foreach ( $keysToKeep as $fk => $values )

								$criteria->addInCondition ( "`$fk`", $values, 'AND NOT' );

						} else {

							$_keysToKeep = array();

							foreach ( $keysToKeep as $field=>$fValues )

							{

								foreach ( $fValues as $key=>$value )

								{

									$_keysToKeep[$key]["`$field`"] = $value;

								}								

							}

							

							foreach($_keysToKeep as $record)

							{

								$criteria->addColumnCondition ($record, 'AND', 'AND NOT' );

							}

						}

						

						$relatedRecord->deleteAll ( $criteria );




					}

				}

			}

			unset($relation);

			if($this->transactional && $this->transaction) $this->transaction->commit();

		}

		catch(Exception $e)

		{

			Yii::trace("An error occured during the save operation for related records : ".$e->getMessage(),'application.components.CSaveRelatedBehavior');

			$this->hasError = true;

			if(isset($relation)) $model->addError($relation,isset($this->relations[$relation]['message']) ? $this->relations[$relation]['message'] : "An error occured during the save of {$relation}");

			if($this->transactional && $this->transaction) $this->transaction->rollBack();

		}

	}

	

	public function beforeDelete($event) {

		$model = $this->owner;

		if($this->transactional && !$model->dbConnection->currentTransaction) {

			Yii::trace("beforeDelete start transaction",'application.components.CSaveRelatedBehavior');

			$this->transaction=$model->dbConnection->beginTransaction();

		}

	}

	

	public function afterDelete($event) {

		if($this->deleteRelatedRecords) {

			$model = $this->owner;

			try{

				foreach($model->relations() as $relation=>$params) {

					$activeRelation = $model->getActiveRelation($relation);

					if(is_object($activeRelation) && ($activeRelation instanceOf CManyManyRelation || $activeRelation instanceOf CHasManyRelation || $activeRelation instanceOf CHasOneRelation)) {

						Yii::trace("deleting {$relation} related records.",'application.components.CSaveRelatedBehavior');

						$relationClassName = $activeRelation->className;

						$relationForeignKey = $activeRelation->foreignKey;

						if($activeRelation instanceOf CManyManyRelation) {

							// ManyMany relation : delete related records from the many to many relation table

							$schema = $model->getCommandBuilder()->getSchema();

							preg_match('/^\s*(.*?)\((.*)\)\s*$/',$relationForeignKey,$matches);

							$joinTable=$schema->getTable($matches[1]);

							$fks=preg_split('/[\s,]+/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);

							$baseParams = array();

							$baseCriteriaCondition = array();

							reset($fks);

							foreach($fks as $i=>$fk) {

								if(isset($joinTable->foreignKeys[$fk])) {

									list($tableName,$pk)=$joinTable->foreignKeys[$fk];

									if($schema->compareTableNames($model->tableSchema->rawName,$tableName)) {

										$baseCriteriaCondition[$fk] = $baseParams[':'.$fk] = $model->{$pk};

									}

								}

							}

							// Delete records

							$criteria = new CDbCriteria;

							$criteria->addColumnCondition($baseCriteriaCondition);

							$model->getCommandBuilder()->createDeleteCommand($joinTable->name,$criteria)->execute();

						}

						else {

							// HasMany & HasOne relation : delete related records

							$relatedRecord = new $relationClassName;

							$criteria = new CDbCriteria;

							$criteria->addColumnCondition(array($relationForeignKey=>$model->primaryKey));

							$relatedRecord->deleteAll($criteria);

						}

					}

				}

				unset($relation);

				if($this->transactional && $this->transaction) $this->transaction->commit();

			}

			catch(Exception $e)

			{

				Yii::trace("An error occured during the delete operation for related records : ".$e->getMessage(),'application.components.CSaveRelatedBehavior');

				$this->hasError = true;

				if(isset($relation)) $model->addError($relation,"An error occured during the delete operation of {$relation}");

				if($this->transactional && $this->transaction) $this->transaction->rollBack();

			}

		}

	}

}




(Juban_) #13

Hi, and thank you for your contribution.

Could you explain a bit more what the "more complicated model" is for you?

Anyway, I’m going to check the modifications you made and figure out what it does and come back to you as soon as possible.

Regards.

Alban.


(Juban_) #14

Hi,

This code simply removes one related model object (at a specific index) from a HAS_MANY relation array.

As you can’t directly delete the objects from the property, it first transfer the content of the property (an array of objects) to a temporary array, remove a entry from that array then reset the value of the property with the resulting array.

It does not actually delete it from the database. This will be achieve during the save operation.

Regards.

Alban.


(A) #15

It generated wrong DELETE queries for related models having several fields (I found it on a model having 3 fields (composite PK) + Foreign Key). Now it generates them better.

Just try to use previous version with such a model and you’ll understand.

The second modification adds back-quotes, so now it’s possible to create columns even with names from ‘reserved words’ list (like FROM, TO etc).

Anyway, you understand your code better so I would be happy if you review my changes and adapt them to your style.


(Sanderb) #16

I’m wondering why my code won’t work:

I have two model, ‘Bedrijf’ and ‘Perceel’ with a MANY_MANY relation. Working from the ‘Bedrijf’ model it looks like this:




	public function relations() {

		return array(

			...

			'percelen' => array(self::MANY_MANY, 'Perceel', 'BedrijfPerceel(bedrijf_id, perceel_id)',

				'index' => 'perceel_id',

			),

		);

	}



My form generates a checkboxlist of Perceel models and saves the selected elements to a public variable called $perceel, which is validated by this function. All validated ids are assigned to the ‘percelen’ relation afterwards:




	public function validatePerceel() {

		$clean = array();

		foreach($this->perceel as $perceel) {

			$criteria = new CDbCriteria;

			$criteria->compare('perceel_id', $perceel);

			if(Perceel::model()->exists($criteria)) {

				$clean[] = $perceel;

			} else {

				return false;

			}

		}

		$this->setRelationRecords('percelen', $clean);

		return true;

	}



This all works, $clean contains something like array(2, 3).

However, if I try to save my model afterwards I get the following CDbException:

Looks like it’s selecting the wrong primary key for the joined table. I tried figuring out what exactly was going on, but the code looks pretty complicated so I could use some help with this!


(Juban_) #17

Hi,

Sorry for the late answer.

For what I can guess, I think that you didn’t set any foreign key constrains in your MANY_MANY table definition.

If this information is missing from the table schema, the behavior will consider the latest keys provided in the relation keys property as the primary keys of the joined table (the same way Yii Framework does).

So, you can try by setting the relation like that:




       public function relations() {

                return array(

                        ...

                        'percelen' => array(self::MANY_MANY, 'Perceel', 'BedrijfPerceel(perceel_id, bedrijf_id)',

                                'index' => 'perceel_id',

                        ),

                );

        }



Let me know if it works for you.

Regards.

Alban.


(Sanderb) #18

The way I posted it was working fine for Yii when I manually added some records to the BedrijfPerceel table in my DB. Just to be clear, this code is from the Bedrijf model, so bedrijf_id its own primary key and perceel_id is the primary key of the joined table (the Perceel model). Switching the keys around results in incorrect behavior when I request related Perceel models, and it still gives me the same error when saving. You are correct in that there are no foreign keys defined explicitly, my app uses SQLite, which didn’t support foreign keys until recently and still has FK functionality disabled by default…

Thanks for any help you can give :)


(Juban_) #19

OK. I understand.

I will try to reproduce this and come back to you with a fix as soon as possible.


(Juban_) #20

I just did a few tests using a MySQL table with no foreign keys constrains and a "flipped" keys definition for the relation and I confirm that Yii does not use the right foreign key value for the joined table either.

So. To get it work for you, I think you should define an extra relation in your model using the right definition keys order and use that relation to save the related data with the behavior.

What do you think about it ?

Regards.

Alban.