View on GitHub

yii2-cookbook-chinese

Yii Application Development Cookbook(Third Edition)中文翻译

实现单表继承

关系数据库不支持继承。如果我们需要在数据库中存储继承,我们需要通过代码来支持它。这个代码应该是高效的,从而它应该生成尽量少的JOINs。一个常见的解决方法是Matrin Fowler提出的,叫做单表继承

当我们使用这个模式时,我们在一张表中存储所有的类树数据,并使用这个类型字段来决定模型的每一行。

作为一个例子,我们希望实现如下单表继承:

Car |- SportCar |- FamilyCar

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 创建并设置一个数据库,添加如下表格:
DROP TABLE IF EXISTS 'car';
CREATE TABLE 'car' (
  'id' int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  'name' varchar(255) NOT NULL,
  'type' varchar(100) NOT NULL,
  PRIMARY KEY ('id')
);
INSERT INTO 'car' ('name', 'type')
  VALUES ('Ford Focus', 'family'),
  ('Opel Astra', 'family'),
  ('Kia Ceed', 'family'),
  ('Porsche Boxster', 'sport'),
  ('Ferrari 550', 'sport');
  1. 使用Gii为car表创建一个Car模型,并未Car模型生成ActiveQuery。

如何做…

  1. 添加如下方法和属性到models/CarQuery.php
/**
 * @var
 */
public $type;
/**
 * @param \yii\db\QueryBuilder $builder
 *
 * @return \yii\db\Query
 */
public function prepare($builder)
{
    if ($this->type !== null) {
        $this->andWhere(['type' => $this->type]);
    }
    return parent::prepare($builder);
}
  1. 创建models/SportCar.php
<?php
namespace app\models;
use Yii;
/**
 * Class SportCar
 * @package app\models
 */
class SportCar extends Car
{
    const TYPE = 'sport';
    /**
     * @return CarQuery
     */
    public static function find()
    {
        return new CarQuery(get_called_class(), ['where' =>
            ['type' => self::TYPE]]);
    }
    /**
     * @param bool $insert
     *
     * @return bool
     */
    public function beforeSave($insert)
    {
        $this->type = self::TYPE;
        return parent::beforeSave($insert);
    }
}
  1. 创建models/FamilyCar.php
<?php
namespace app\models;
use Yii;
/**
 * Class FamilyCar
 * @package app\models
 */
class FamilyCar extends Car
{
    const TYPE = 'family';
    /**
     * @return CarQuery
     */
    public static function find()
    {
        return new CarQuery(get_called_class(), ['where' =>
            ['type' => self::TYPE]]);
    }
    /**
     * @param bool $insert
     *
     * @return bool
     */
    public function beforeSave($insert)
    {
        $this->type = self::TYPE;
        return parent::beforeSave($insert);
    }
}
  1. 添加如下方法到models/Car.php
/**
 * @param array $row
 *
 * @return Car|FamilyCar|SportCar
 */
public static function instantiate($row)
{
    switch ($row['type']) {
        case SportCar::TYPE:
            return new SportCar();
        case FamilyCar::TYPE:
            return new FamilyCar();
        default:
            return new self;
    }
}
  1. 添加TestController
<?php
namespace app\controllers;
use app\models\Car;
use app\models\FamilyCar;
use Yii;
use yii\helpers\Html;
use yii\web\Controller;
/**
 * Class TestController
 * @package app\controllers
 */
class TestController extends Controller
{
    public function actionIndex()
    {
        echo Html::tag('h1', 'All cars');
        $cars = Car::find()->all();
        foreach ($cars as $car) {
            // Each car can be of class Car, SportCar or FamilyCar
            echo get_class($car).' '.$car->name."<br />";
        }
        
        echo Html::tag('h1', 'Family cars');
        $familyCars = FamilyCar::find()->all();
        foreach($familyCars as $car)
        {
            // Each car should be FamilyCar
            echo get_class($car).' '.$car->name."<br />";
        }
    }
}
  1. 运行test/index,你将会得到如下输出:

工作原理…

基础模型Car是一个典型的Yii AR模型,除了他有两个额外的方法。tableName方法明确声明了模型使用的表名。单对于Car模型,这没有意义,单对于子模型,它将会返回相同的car表,这就是我们想要的——整个类树用一个表。instantiate方法被用于AR内部,当我们调用方法例如Car::find()->all(),用于创建一个模型的实例。我们使用一个switch来创建不同的类。

SportCarFamilyCar模型只是设置了缺省的AR作用域,所以当我们使用SportCar::model()->方法查询模型时,我们只会得到SportCar模型。

参考

参考如下地址,了解更多关于单表继承模式,和Yii Active Record实现: