В большинстве реляционных баз данных, к сожалению, нет поддержки наследования, так что приходится реализовывать это вручную. В этой статье я хочу кратко показать, как реализовать такой подход к наследованию, как «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 appmodels;
use yiidbActiveQuery;
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 appmodels;
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 appmodels;
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 appmodelsHeavyCar
2 Ferrari appmodelsSportCar
3 BMW appmodelsCar
2 Ferrari appmodelsSportCar
Как можно заметить, модели получают класс в соответствии с указанным у них типом.
Обработка уникальных значений
Если в таблице есть поля, отмеченные в модели как уникальные, для того чтобы UniqueValidator
пропускал их у разных классов, можно использовать такую приятную фишку Yii как targetClass
:
public function rules()
{
return [
[['MyUniqueColumnName'], 'unique', 'targetClass' => Car::classname()],
];
}
Автор: Tairesh