Наследование с одной таблицей в Yii
18 августа 2010
Реляционные БД не поддерживают наследование, поэтому при отображении объекта на БД приходится как-то это обходить. При этом необходимо минимизировать количество JOIN. Решается данная проблема довольно простым способом при помощи паттерна наследование с одной таблицей. При этом, в таблице хранятся столбцы для всей ветки классов, наследуемых от заданного. Для определения типа модели обычно используется поле type.
В Yii этот паттерн реализуется достаточно красиво.
Наша схема с единственной таблицей:
CREATE TABLE `car` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `type` varchar(100) NOT NULL, PRIMARY KEY (`id`) );
Базовая модель AR с перекрытым методом instantiate:
class Car extends CActiveRecord { static function model($className=__CLASS__) { return parent::model($className); } function tableName() { return 'car'; } /** * Перекрываем метод для заполнения результатов findAll() и подобных * методов нужными нам моделями. * * @param array $attributes * @return Car */ protected function instantiate($attributes){ switch($attributes['type']){ case 'sport': $class='SportCar'; break; case 'family': $class='FamilyCar'; break; default: $class=get_class($this); } $model=new $class(null); return $model; } }
Ну и наши наследники:
class SportCar extends Car { static function model($className=__CLASS__) { return parent::model($className); } function defaultScope(){ return array( 'condition'=>"type='sport'", ); } } class FamilyCar extends Car { static function model($className=__CLASS__) { return parent::model($className); } function defaultScope(){ return array( 'condition'=>"type='family'", ); } }
Для удобства в них переопределены defaultScope, то есть, например, при поиске с использованием FamilyCar будут найдены только семейные автомобили.
Комментарии RSS по email OK
Правильно ли я понял:
Мы храним все поля всех классов в одной таблице - и базового класса, и всех наследников. Неиспользуемые в каждом конкретном случае поля просто оставляем пустыми;
Мы также имеем служебное поле type в той же таблице для указания принадлежности конкретному классу;
С помощью defaultScope() мы определяем условие по умолчанию для выбора записей, соответствующих объектам данного конкретного класса, из общей таблицы.
Тогда, насколько я понимаю, для добавления нового нетривиального класса (такого, у которого есть новые поля) нам придется расширять общую таблицу, так? Это выглядит не очень-то гибким решением, если есть потребность добавлять новые классы объектов. Есть какие-нибудь способы это обойти?
Почти так.
Да.
Да.
Для добавления новых классов «на лету» без изменения структуры таблиц лучше посмотреть в сторону Entity-Attribute-Value, но тут будут как раз проблемы с JOIN (хотя на не очень крупных проектах сойдёт).
Спасибо за ссылку, общую идею понял.
По мне так это антипаттерн, плодить классы. какие плюсы? нежели просто отличать тип авто по атрибуту type?
Например, так можно заменить какой-нибудь if в методе для подсчёта стоимости модели на полиморфизм.
допустим нам нужно создать объект авто по параметру type. мне для этого фабрику тогда придется делать? а в ней тот же if или switch? или в контроллере условную логику сразу if ($type == Car::TYPE_SPORT) { $car = new SportCar; } else if ....
Ну да, внутри метода
instantiate
будет всё тот жеswitch
. Но это лучше, чемswitch
в жирном методе с логикой.Чтобы избавиться от defaultScopes в каждом наследнике достаточно в родителе добавить
из-за get_called_class() работает только PHP > 5.3
Еще одно дополнение, лучше всего установить type для модели по умолчанию.
Нужно сделать это: 1) в методе model() 2) и в методе init()
Я бы хотел сделать Car abstract. Так и сделал у себя в коде пока не понадобилось сделать поиск типа:
В таком случае при abstract Car, PHP быстрее выстреливает ошибку о попытке создать объект асбтрактного класса.
Итого, есть ли способ сделать Car абстракным? :)
Нет, нету.
а как на щет yii2, можно как то портировать?
Готово: github.com/samdark/yii2-cookbook/blob/master/book/ar-single-table-inheritance.md