<rmcreative>

RSS

Наследование с одной таблицей в 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

  1. №2890
    Scriptin
    Scriptin 18 авг. 2010 г., 14:01:19

    Правильно ли я понял:

    1. Мы храним все поля всех классов в одной таблице - и базового класса, и всех наследников. Неиспользуемые в каждом конкретном случае поля просто оставляем пустыми;

    2. Мы также имеем служебное поле type в той же таблице для указания принадлежности конкретному классу;

    3. С помощью defaultScope() мы определяем условие по умолчанию для выбора записей, соответствующих объектам данного конкретного класса, из общей таблицы.

    Тогда, насколько я понимаю, для добавления нового нетривиального класса (такого, у которого есть новые поля) нам придется расширять общую таблицу, так? Это выглядит не очень-то гибким решением, если есть потребность добавлять новые классы объектов. Есть какие-нибудь способы это обойти?

  2. №2891
    Sam
    Sam 18 авг. 2010 г., 15:01:50
    1. Почти так.

    2. Да.

    3. Да.

    Для добавления новых классов «на лету» без изменения структуры таблиц лучше посмотреть в сторону Entity-Attribute-Value, но тут будут как раз проблемы с JOIN (хотя на не очень крупных проектах сойдёт).

  3. №2893
    Scriptin
    Scriptin 18 авг. 2010 г., 17:11:18

    Спасибо за ссылку, общую идею понял.

  4. №5048
    Артем
    Артем 25 июля 2011 г., 20:38:34

    По мне так это антипаттерн, плодить классы. какие плюсы? нежели просто отличать тип авто по атрибуту type?

  5. №5049
    Sam
    Sam 25 июля 2011 г., 20:54:17

    Например, так можно заменить какой-нибудь if в методе для подсчёта стоимости модели на полиморфизм.

  6. №5249
    Артем
    Артем 30 авг. 2011 г., 20:04:22

    допустим нам нужно создать объект авто по параметру type. мне для этого фабрику тогда придется делать? а в ней тот же if или switch? или в контроллере условную логику сразу if ($type == Car::TYPE_SPORT) { $car = new SportCar; } else if ....

  7. №5250
    Sam
    Sam 30 авг. 2011 г., 22:23:58

    Ну да, внутри метода instantiate будет всё тот же switch. Но это лучше, чем switch в жирном методе с логикой.

  8. №6350
    mc-bear
    mc-bear 15 июня 2012 г., 17:36:52

    Чтобы избавиться от defaultScopes в каждом наследнике достаточно в родителе добавить

    public function getDbCriteria($createIfNull = true)
        {
            // Проверим создана ли уже критерия
            $criteria = parent::getDbCriteria(false);
            // Если еще нет, создаем и добавляем обязательное условие по типу
            if($criteria === null && ($class = get_called_class()) !== __CLASS__) {
                $criteria = new CDbCriteria($this->defaultScope());
                $criteria->compare('type', $class);
                $this->setDbCriteria($criteria);
            }
            return parent::getDbCriteria($createIfNull);
        }

    из-за get_called_class() работает только PHP > 5.3

  9. №6405
    mc-bear
    mc-bear 03 июля 2012 г., 21:39:34

    Еще одно дополнение, лучше всего установить type для модели по умолчанию.

    Нужно сделать это: 1) в методе model() 2) и в методе init()

  10. №6517
    Denis T
    Denis T 31 июля 2012 г., 17:24:46

    Я бы хотел сделать Car abstract. Так и сделал у себя в коде пока не понадобилось сделать поиск типа:

    Manufacturer::model()->with('Car')->findByPk($manufacturerId);

    В таком случае при abstract Car, PHP быстрее выстреливает ошибку о попытке создать объект асбтрактного класса.

    Итого, есть ли способ сделать Car абстракным? :)

  11. №6521
    Sam
    Sam 31 июля 2012 г., 19:25:57

    Нет, нету.

  12. №9279
    sharp
    sharp 15 окт. 2014 г., 14:52:28

    а как на щет yii2, можно как то портировать?

  13. №9281
    Sam
    Sam 15 окт. 2014 г., 17:00:47
  1. Почта опубликована не будет.

  2. Можно использовать синтаксис Markdown или HTML.

  3. Введите ответ в поле. Щёлкните, чтобы получить другую задачу.