Yii связь многие ко многим

от автора

many-to-many

Вступление

Привет, хабр! Многие, наверное, сталкивались с необходимостью реализовать функционал связи многие-ко-многим в Yii. Казалось бы, тут нет ничего сложного, и скорее всего разработчики фреймворка уже постарались за нас реализовать необходимый функционал связи, и нам остаёться только прописать необходимые связи в модели, и, пользуясь привычными методами, сохранить данные.

Но такого функционала Yii нам не предоставляет, видимо такая возможность появиться в более поздних версиях или будет расширяться только за счет дополнительных расширений. В итоге нам приходиться реализовывать связь самим.
Конечно, все выкручиваються как могут, кто-то использует расширения, которые хорошо показали себя в бою. Например: with-related-behavior репозиторий проекта
Кто-то пишет связь прямо в методе при сохранении или обновлении данных. Но у такого подхода большой минус. Когда связей между сущностями в проекте много приходиться дублировать код в методах, что в дальнейшем отразиться на падении читаемости и его поддержке.
Но что делать, если сущностей много, и нужно связывать много данных?
Мы попробуем написать простую реализацию, которую в случае необходимости, можно будет легко переписать и в дальнейшем поддерживать в своем проекте.

Немного теории

При типе связи многие-ко-многим запись из одной таблицы связывается с несколькими записями другой таблицы, а запись из второй таблицы связывается с несколькими записями в первой таблице.
Этот тип связи требует создания третьей таблицы, называемой сводной таблицей. В сводной таблице в качестве внешних ключей содержатся первичные ключи двух первых таблиц.

Реализация

Возьмем за пример задачу, в которой товар может быть сделан из нескольких материалов:

many-to-many
Вот так выглядит миграция сводной сущности ProductMaterial, в которую добавлены внешние ключи на сущности Product и Material:

#protected/migrations/m140314_091505_addProductMaterialTable.php 	class m140314_091505_addProductMaterialTable extends CDbMigration 	{ 		public function safeUp() 		{ 			$prefix = $this->getDbConnection()->tablePrefix; 			$this->createTable('{{productMaterial}}', array( 					'id' => 'int(10) unsigned NOT NULL AUTO_INCREMENT', 					'productId' => 'int(10) unsigned NOT NULL', 					'materialId' => 'int(10) unsigned NOT NULL', 					'PRIMARY KEY (`id`)', 			), 'ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=\'style (to which productMaterial should belong)\';'); 	 			$this->addForeignKey($prefix.'productMaterial_product_fk_constraint', '{{productMaterial}}', 'productId', '{{product}}', 'id', 'RESTRICT', 'CASCADE'); 			$this->addForeignKey($prefix.'productMaterial_material_fk_constraint', '{{productMaterial}}', 'materialId', '{{material}}', 'id', 'RESTRICT', 'CASCADE'); 		} 	 	} 

Теперь опишем поведение, которое будет сохранять, и добавлять, и изменять наши связи:

#protected/extensions/ManyToManyRelationBehavior.php class ManyToManyRelationBehavior extends CBehavior{ 	/** 	 * @var string name model Relation  	 */ 	public $modelNameRelation; 	/** 	 * @var string field name for the relationship with the current models 	 */ 	public $fieldNameModelCurrent = null; 	/** 	 * @var string field name for the relationship with the external models 	 */ 	public $fieldNameModelRelation = null; 	/** 	 * @var array list of values ​​of the current model 	 */ 	public $relationList = array();  	public function events() { 		return array_merge(parent::events(), array( 				'onAfterSave' => 'afterSave', 		)); 	}  	public function afterSave($event){ 		if (is_array($this->relationList)){ 			$model = $this->modelNameRelation; 			$delete = $model::model()->deleteAll("{$this->fieldNameModelCurrent} =:param", array( 					":param" => $this->owner->id, 			)); 			foreach ($this->relationList as $value){ 				$model = new $this->modelNameRelation; 				$model->{$this->fieldNameModelRelation} = intval($value); 				$model->{$this->fieldNameModelCurrent} = $this->owner->id; 				if (!$model->save()){ 					Yii::log('Unable to save relation: '.serialize($model->getErrors()), 'warning'); 					return false; 				} 			} 		} 		return true; 	} } 

Пример использования поведения при создании нового продукта:

public function actionCreate() { 	$model=new Product; 	$materialList = $_POST['Product']['materialId'];         $model->attachBehavior('ManyToManyRelationBehavior', array( 					'class' => 'ext.ManyToManyRelationBehavior', 					'modelNameRelation' => 'ProductMaterial', // Наша модель для связи 					'fieldNameModelCurrent' =>  'productId',// Название поля для связи и Product 					'fieldNameModelRelation' => 'materialId', // Название поля для связи и Material 					'relationList' => $materialList, // array id material 				)); 

Чтобы быстро получать материалы товаров через модель добавим нужные Relation в модель Product, связав их через параметр through.

	#protected/models/Product.php 	public function relations() 	{ 		return $this->moderationRelations(array( 		'materialAll' => array(self::HAS_MANY, 'ProductMaterial', 'productId'), 		'materialRelation' => array(self::HAS_MANY, 'Material', 'materialId', 'through' => 'materialAll'), 			 		)); 	} 

Теперь материалы товара можно получить просто:

$model = Material::model()->findByPk($pk); $model->materialAll; 

Заключение

Понятно, что разработчики Yii не могут включить в фреймворк весь функционал, который может потребоваться. Но они дали нам прекрасную возможность легко писать расширения с прозрачным доступом к ним, которые можно переносить из одного проекта в другой. Так же не всегда удобно брать существующие расширения. Можно потратить много времени на их изучение и доработку функционала. За это время вы можете написать свою реализацию, которую в дальнейшем легко поддерживать и дорабатывать.

Репозиторий поведения

ссылка на оригинал статьи http://habrahabr.ru/post/216605/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *