В большинстве реляционных баз данных, к сожалению, нет поддержки наследования, так что приходится реализовывать это вручную. В этой статье я хочу кратко показать, как реализовать такой подход к наследованию, как «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/
Добавить комментарий