实现单表继承

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

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

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

Car |- SportCar |- FamilyCar

准备

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

如何做…

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

实现单表继承 - 图1

工作原理…

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

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

参考

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