Наследование ActiveRecord’s, описывающих одну таблицу (паттерн single table inheritance) в Yii2

от автора

В большинстве реляционных баз данных, к сожалению, нет поддержки наследования, так что приходится реализовывать это вручную. В этой статье я хочу кратко показать, как реализовать такой подход к наследованию, как «single table inheritance», описанный в книге «Patterns of Enterprise Application Architecture» by Martin Fowler.

В соответствии с этим паттерном, нужно использовать общую таблицу для наследуемых моделей и в этой таблице добавить поле type, которое будет определять класс-наследника этой записи.

В этой статье будет использоваться следующая структура наследования моделей:

Car |- SportCar |- HeavyCar 

Таблица `car` имеет следующую структуру:

CREATE TABLE `car` (     `id` int NOT NULL AUTO_INCREMENT,     `name` varchar(255) NOT NULL,     `type` varchar(255) DEFAULT NULL,     PRIMARY KEY (`id`) );  INSERT INTO `car` (`id`, `name`, `type`) VALUES (1, 'Kamaz', 'heavy'), (2, 'Ferrari', 'sport'), (3, 'BMW', 'city'); 

Модель Car можно сгенерировать с помощью Gii.

Как это работает

Нам понадобится простой класс запроса CarQuery, который автоматически будет подставлять тип автомобиля.

namespace app\models;  use yii\db\ActiveQuery;  class CarQuery extends ActiveQuery {     public $type;      public function prepare($builder)     {         if ($this->type !== null) {             $this->andWhere(['type' => $this->type]);         }         return parent::prepare($builder);     } } 

И теперь мы можем создать классы-наследники от Car. В них мы определим константу TYPE которая будет хранить тип автомобиля для записи в поле type модели, и переопределим ActiveRecord-методы init, find и beforeSave, в которых этот тип будет автоматически подставляться в модель и в запрос CarQuery. TYPE не обязательно должен быть строкой (разумнее использовать unsigned int) и даже не обязательно константой, но для простоты сделаем так. Таким будет SportCar:

namespace app\models;  class SportCar extends Car {     const TYPE = 'sport';      public function init()     {         $this->type = self::TYPE;         parent::init();     }      public static function find()     {         return new CarQuery(get_called_class(), ['type' => self::TYPE]);     }      public function beforeSave($insert)     {         $this->type = self::TYPE;         return parent::beforeSave($insert);     } } 

И таким HeavyCar:

namespace app\models;  class HeavyCar extends Car {     const TYPE = 'heavy';      public function init()     {         $this->type = self::TYPE;         parent::init();     }      public static function find()     {         return new CarQuery(get_called_class(), ['type' => self::TYPE]);     }      public function beforeSave($insert)     {         $this->type = self::TYPE;         return parent::beforeSave($insert);     } } 

Дублирования кода, можно избежать, вынеся эти методы в класс Car и используя вместо константы protected метод Car::getType, но сейчас я не буду на этом останавливаться для простоты.

Теперь нам нужно переопределить метод Car:instantiate: для автоматического создания модели нужного класса, в зависимости от типа:

public static function instantiate($row) {     switch ($row['type']) {         case SportCar::TYPE:             return new SportCar();         case HeavyCar::TYPE:             return new HeavyCar();         default:            return new self;     } } 

Знающий о всех наследниках switch case в коде модели-родителя — на самом деле не слишком удачное решение, но, опять же, это сделано только для простоты понимания подхода и от этого несложно избавиться чуть усложнив код.

Теперь для single table inheritance всё готово. Вот простой пример его прозрачного использования в контроллере:

// finding all cars we have $cars = Car::find()->all(); foreach ($cars as $car) {     echo "$car->id $car->name " . get_class($car) . "<br />"; }  // finding any sport car $sportCar = SportCar::find()->limit(1)->one(); echo "$sportCar->id $sportCar->name " . get_class($sportCar) . "<br />"; 

Этот код выведет следующее:

1 Kamaz app\models\HeavyCar 2 Ferrari app\models\SportCar 3 BMW app\models\Car 2 Ferrari app\models\SportCar 

Как можно заметить, модели получают класс в соответствии с указанным у них типом.

Обработка уникальных значений

Если в таблице есть поля, отмеченные в модели как уникальные, для того чтобы UniqueValidator пропускал их у разных классов, можно использовать такую приятную фишку Yii как targetClass:

 public function rules()     {         return [             [['MyUniqueColumnName'], 'unique', 'targetClass' => Car::classname()],         ];     } 

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


Комментарии

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

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