В большинстве реляционных баз данных, к сожалению, нет поддержки наследования, так что приходится реализовывать это вручную. В этой статье я хочу кратко показать, как реализовать такой подход к наследованию, как «single table inheritance», описанный в книге «Patterns of Enterprise Application Architecture» by Martin Fowler.
В соответствии с этим паттерном, нужно использовать общую таблицу для наследуемых моделей и в этой таблице добавить поле
В этой статье будет использоваться следующая структура наследования моделей:
Таблица
Модель
Нам понадобится простой класс запроса
И теперь мы можем создать классы-наследники от
И таким
Дублирования кода, можно избежать, вынеся эти методы в класс
Теперь нам нужно переопределить метод
Знающий о всех наследниках
Теперь для
Этот код выведет следующее:
Как можно заметить, модели получают класс в соответствии с указанным у них типом.
Если в таблице есть поля, отмеченные в модели как уникальные, для того чтобы
Это вольный перевод одного из полезных «рецептов» для Yii2, написанных хабравчанином SamDark здесь — https://github.com/samdark/yii2-cookbook, так что если эта статья чем-то вам помогла, отправляйте лучики добра — ему, а если не понравилась, то лучи зла мне.
В соответствии с этим паттерном, нужно использовать общую таблицу для наследуемых моделей и в этой таблице добавить поле
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()], ]; }
Это вольный перевод одного из полезных «рецептов» для Yii2, написанных хабравчанином SamDark здесь — https://github.com/samdark/yii2-cookbook, так что если эта статья чем-то вам помогла, отправляйте лучики добра — ему, а если не понравилась, то лучи зла мне.
