第3章 ActiveRecord, 模型, 数据库

在本章中,我们将会讨论如下话题:

  • 从数据库中获取数据
  • 定义和使用多个数据库连接
  • 自定义ActiveQuery类
  • 使用AR event-like方法处理model fields
  • 自动化时间戳
  • 自动设置一个作者
  • 自动设置一个slug
  • 事务
  • 复制和读写分离
  • 实现单表继承

介绍

在本章中,你将学习如何高效使用数据库,什么时候应该使用models而什么时候不应该,如何使用多个数据库,如何自动预处理Active Record fields,如何使用事务,等等。

从数据库中获取数据

今天大多数应用都在使用数据库。不论是一个小网站,还是一个大型社交网站,至少其中一部分功能是由数据库驱动的。

Yii引入了三种方法来允许你使用数据库。他们是:

  • Active Record
  • Query Builder
  • SQL via DAO

我们将使用这三种方法从filmfilm_actoractor表中获取数据,并将他们展示在一个列表中。同时,我们将会比较它们的执行时间和内存占用情况,来决定这些方法的使用场景。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. http://dev.mysql.com/doc/index-other.html下载Sakila数据库。
  3. 执行下载好的SQLs;首先是schema,然后是数据。
  4. config/main.php中配置数据库连接,使用Sakila数据库。
  5. 使用Gii为actor和film表创建模型。

如何做…

  1. 创建app/controllers/DbController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Actor;
  4. use Yii;
  5. use yii\db\Query;
  6. use yii\helpers\ArrayHelper;
  7. use yii\helpers\Html;
  8. use yii\web\Controller;
  9. /**
  10. * Class DbController
  11. * @package app\controllers
  12. */
  13. class DbController extends Controller
  14. {
  15. /**
  16. * Example of Active Record usage.
  17. *
  18. * @return string
  19. */
  20. public function actionAr()
  21. {
  22. $records = Actor::find()
  23. ->joinWith('films')
  24. ->orderBy('actor.first_name,
  25. actor.last_name, film.title')
  26. ->all();
  27. return $this->renderRecords($records);
  28. }
  29. /**
  30. * Example of Query class usage.
  31. *
  32. * @return string
  33. */
  34. public function actionQuery()
  35. {
  36. $rows = (new Query())
  37. ->from('actor')
  38. ->innerJoin('film_actor',
  39. 'actor.actor_id=film_actor.actor_id')
  40. ->leftJoin('film',
  41. 'film.film_id=film_actor.film_id')
  42. ->orderBy('actor.first_name, actor.last_name,
  43. actor.actor_id, film.title')
  44. ->all();
  45. return $this->renderRows($rows);
  46. }
  47. /**
  48. * Example of SQL execution usage.
  49. *
  50. * @return string
  51. */
  52. public function actionSql()
  53. {
  54. $sql = 'SELECT *
  55. FROM actor a
  56. JOIN film_actor fa ON fa.actor_id = a.actor_id
  57. JOIN film f ON fa.film_id = f.film_id
  58. ORDER BY a.first_name, a.last_name, a.actor_id,
  59. f.title';
  60. $rows = Yii::$app->db->createCommand($sql)->queryAll();
  61. return $this->renderRows($rows);
  62. }
  63. /**
  64. * Render records for Active Record array.
  65. *
  66. * @param array $records
  67. *
  68. * @return string
  69. */
  70. protected function renderRecords(array $records = [])
  71. {
  72. if (!$records) {
  73. return $this->renderContent('Actor list is empty.');
  74. }
  75. $items = [];
  76. foreach ($records as $record) {
  77. $actorFilms = $record->films
  78. ?
  79. Html::ol(ArrayHelper::getColumn($record->films, 'title')): null;
  80. $actorName = $record->first_name.'
  81. '.$record->last_name;
  82. $items[] = $actorName.$actorFilms;
  83. }
  84. return $this->renderContent(Html::ol($items, [
  85. 'encode' => false,
  86. ]));
  87. }
  88. /**
  89. * Render rows for result of query.
  90. *
  91. * @param array $rows
  92. *
  93. * @return string
  94. */
  95. protected function renderRows(array $rows = [])
  96. {
  97. if (!$rows) {
  98. return $this->renderContent('Actor list is empty.');
  99. }
  100. $items = [];
  101. $films = [];
  102. $actorId = null;
  103. $actorName = null;
  104. $actorFilms = null;
  105. $lastActorId = $rows[0]['actor_id'];
  106. foreach ($rows as $row) {
  107. $actorId = $row['actor_id'];
  108. $films[] = $row['title'];
  109. if ($actorId != $lastActorId) {
  110. $actorName = $row['first_name'].'
  111. '.$row['last_name'];
  112. $actorFilms = $films ? Html::ol($films) : null;
  113. $items[] = $actorName.$actorFilms;
  114. $films = [];
  115. $lastActorId = $actorId;
  116. }
  117. }
  118. if ($actorId == $lastActorId) {
  119. $actorFilms = $films ? Html::ol($films) : null;
  120. $items[] = $actorName.$actorFilms;
  121. }
  122. return $this->renderContent(Html::ol($items, [
  123. 'encode' => false,
  124. ]));
  125. }
  126. }
  1. 这里,我们有三个actions分别对应于三种不同的方法。
  2. 运行上面的db/ardb/querydb/sql三个actions之后,你应该得到了一个展示200个演员和他们演过的1000个电影的树,截图如下:

第3章 ActiveRecord, 模型, 数据库 - 图1

  1. 在页面底部,提供了关于内存使用和执行时间的信息。运行这段代码的绝对时间可能不同,但相对大小应该是一致的:
方法 内存使用(MB) 执行时间(秒)
Active Record 21.4 2.398
Query Builder 28.3 0.477
SQL(DAO) 27.6 0.481

工作原理…

actionAr方法使用Active Record方法获取了模型的实例。我们使用Gii生成的Actor模型来获取所有的演员,并指定joinWith=>'films'来获取对应的电影,它使用一个简单的查询或者通过关系预先加载,这是由Gii从InnoDB表外键为我们创建的。然后迭代所有的演员和电影,打印出他们的名字。

actionQuery函数使用Query Builder。首先我们使用\yii\db\Query为当前数据库连接创建了一个查询。然后依次加入查询部分fromjoinInnerleftJoin。这些方法自动escape值、表和field名称。\yii\db\Query的函数all()返回了原始数据库的行数组。每一行也是一个数组,索引是field名称。我们将结果传给了renderRows,它负责渲染。

actionSql是一样的,不同的是我们直接传递SQL,而不是一个接着一个。值得一提的是,我们应该使用Yii::app()->db->quoteValue手动escape参数值:

renderRows方法渲染了Query Builder。

renderRecords方法渲染了active records。

方法 Active Record Query Builder SQL(DAO)
语法 能为你处理SQL。
Gii会为你创建模型和关系。
使用完全面向对象风格的模型和整洁的API。
生成一个适当嵌套的模型的数组作为结果。
整洁的API,适于一步步创建查询。
生成原始数据数组作为结果。
适用于复杂的SQL。
手动qoute值和关键字。
不太适用于一步步创建查询。
生成原始数据数组作为结果。
性能 相对于SQL和Query Builder,内存占用率高,执行时间长。 Okay Okay
更多特性 自动quote值和名称。
Behaviors. Before/after hook.
校验。Prototyping select.
自动quote值和名称
适用于 为单个模型更新、删除和创建(当使用form时尤为便利) 适用于大量的数据,并能一步步创建查询。 使用纯SQL进行复杂的查询,并有尽可能好的性能。

更多…

欲了解更多有关Yii操作数据库,参考如下资源:

定义和使用多个数据库连接

对于新的单机web应用,多数据库连接并不常用。但是,当你为一个已经存在的系统附加一个应用是,你很可能需要另外一个数据库连接。

在本节中,你将会学习如何定义多个数据库连接并利用DAO、Query Builder和Active Record模型使用它们。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 创建两个MySQL数据库,名字分别叫db1db2
  3. db1中创建一个名叫post的表:
  1. DROP TABLE IF EXISTS 'post';
  2. CREATE TABLE IF NOT EXISTS 'post' (
  3. 'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4. 'title' VARCHAR(255) NOT NULL,
  5. 'text' TEXT NOT NULL,
  6. PRIMARY KEY ('id')
  7. );
  1. db2中创建一个名叫comment的表:
  1. DROP TABLE IF EXISTS 'comment';
  2. CREATE TABLE IF NOT EXISTS 'comment' (
  3. 'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4. 'text' TEXT NOT NULL,
  5. 'post_id' INT(10) UNSIGNED NOT NULL,
  6. PRIMARY KEY ('id')
  7. );

如何做…

  1. 首先配置数据库连接。打开config/main.php文件,按照官方指南中的描述,定义一个主连接:
  1. 'db' => [
  2. 'connectionString' =>'mysql:host=localhost;dbname=db1',
  3. 'username' => 'root',
  4. 'password' => '',
  5. 'charset' => 'utf8',
  6. ],
  1. 复制它,重命名db组件为db2,并相应修改connectionString。同时,你需要按照如下方式添加class
  1. 'db2' => [
  2. 'class'=>'yii\db\Connection',
  3. 'connectionString' => 'mysql:host=localhost;dbname=db2',
  4. 'username' => 'root',
  5. 'password' => '',
  6. 'charset' => 'utf8',
  7. ],
  1. 现在你有两个数据库连接,你可以按如下方式利用DAO和Query Builder使用它们:
  1. $rows1 = Yii::$app->db->createCommand($sql)->queryAll();
  2. $rows2 = Yii::$app->db2->createCommand($sql)->queryAll();
  1. 现在,如何我们需要使用Active Record模型,首先我们需要使用Gii创建Post和Comment模型。你可以为每一个模型选择一个合适的连接。当你创建Comment模型时,将数据库连接ID设置为db2,如下截图所示:

第3章 ActiveRecord, 模型, 数据库 - 图2

  1. 现在你可以按往常一样使用Comment模型,并创建controllers/DbController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Post;
  4. use app\models\Comment;
  5. use yii\helpers\ArrayHelper;
  6. use yii\helpers\Html;
  7. use yii\web\Controller;
  8. /**
  9. * Class DbController.
  10. * @package app\controllers
  11. */
  12. class DbController extends Controller
  13. {
  14. public function actionIndex()
  15. {
  16. $post = new Post();
  17. $post->title = 'Post #'.rand(1, 1000);
  18. $post->text = 'text';
  19. $post->save();
  20. $posts = Post::find()->all();
  21. echo Html::tag('h1', 'Posts');
  22. echo Html::ul(ArrayHelper::getColumn($posts, 'title'));
  23. $comment = new Comment();
  24. $comment->post_id = $post->id;
  25. $comment->text = 'comment #'.rand(1, 1000);
  26. $comment->save();
  27. $comments = Comment::find()->all();
  28. echo Html::tag('h1', 'Comments');
  29. echo Html::ul(ArrayHelper::getColumn($comments,
  30. 'text'));
  31. }
  32. }
  1. 运行db/index多次,然后你将会看到记录保存到了两个数据库中,如下截图所示:

第3章 ActiveRecord, 模型, 数据库 - 图3

工作原理…

在Yii中,你可以通过配置文件添加和配置你自己的组件。对于非标准的组件,例如db2,你必须指定组件类。类似地,你可以添加db3db4或者其他组件,例如facebookApi。剩余的数组键值对分别赋值给了组件的公共属性。

更多…

依赖于使用的RDBMS,有一个额外的事情可以做,能让我们更方便的使用多个数据库。

跨数据库关系

如果你使用的是MySQL,你可以为你的模型创建跨数据库的关系。为了做到这一步,你应该为Comment模型的表名添加数据库名称:

  1. class Comment extends \yii\db\ActiveRecord
  2. {
  3. //...
  4. public function tableName()
  5. {
  6. return 'db2.comment';
  7. }
  8. //...
  9. }

现在,如果在Post模型中你定义了一个评论关系,你可以按如下方式使用:

  1. $posts = Post::find()->joinWith('comments')->all();

参考

欲了解更多信息,参考http://www.yiiframework.com/doc-2.0/guide-db-dao.html#creating-dbconnections

自定义ActiveQuery类

默认情况下,所有的Active Record查询是由\yii\db\ActiveQuery支持的。为了在一个Active Record中使用一个自定义的查询类,你可以重写\yii\db\ActiveRecord::find()方法,并返回一个你的自定义查询类的实例。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 设置数据库连接,创建一个名为post的表:
  1. DROP TABLE IF EXISTS ''post'';
  2. CREATE TABLE IF NOT EXISTS ''post'' (
  3. ''id'' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4. ''lang'' VARCHAR(5) NOT NULL DEFAULT ''en'',
  5. ''title'' VARCHAR(255) NOT NULL,
  6. ''text'' TEXT NOT NULL,
  7. PRIMARY KEY (''id'')
  8. );
  9. INSERT INTO ''post''(''id'',''lang'',''title'',''text'')
  10. VALUES (1,''en_us'',''Yii news'',''Text in English''),
  11. (2,''de'',''Yii Nachrichten'',''Text in Deutsch'');
  1. 使用Gii生成一个Post模型,并选中Generate ActiveQuery选项,这会生成PostQuery类。

如何做…

  1. models/PostQuery.php添加如下方法:
  1. <?php
  2. namespace app\models;
  3. /**
  4. * This is the ActiveQuery class for [[Post]].
  5. *
  6. * @see Post
  7. */
  8. class PostQuery extends \yii\db\ActiveQuery
  9. {
  10. /**
  11. * @param $lang
  12. *
  13. * @return $this
  14. */
  15. public function lang($lang)
  16. {
  17. return $this->where([ 'lang' => $lang ]);
  18. }
  19. }
  1. 现在,我们可以使用我们的模型。创建controllers/DbController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Post;
  4. use yii\helpers\Html;
  5. use yii\web\Controller;
  6. /**
  7. * Class DbController.
  8. * @package app\controllers
  9. */
  10. class DbController extends Controller
  11. {
  12. public function actionIndex()
  13. {
  14. // Get posts written in default application language
  15. $posts = Post::find()->all();
  16. echo Html::tag('h1', 'Default language');
  17. foreach ($posts as $post) {
  18. echo Html::tag('h2', $post->title);
  19. echo $post->text;
  20. }
  21. // Get posts written in German
  22. $posts = Post::find()->lang('de')->all();
  23. echo Html::tag('h1', 'German');
  24. foreach ($posts as $post) {
  25. echo Html::tag('h2', $post->title);
  26. echo $post->text;
  27. }
  28. }
  29. }
  1. 现在运行db/index你会得到类似如下截图所示的输出:

第3章 ActiveRecord, 模型, 数据库 - 图4

工作原理…

我们在Post模型中重写了find方法,并扩展了ActiveQuery类。lang方法返回指定语言值的ActiveQuery。为了支持链式调用,lang返回自身模型的实例。

参考

欲了解更多信息,参考如下地址:

使用AR event-like方法处理model fields

Yii中实现的Active Record非常强大,并有很多特性。其中一个特性就是event-like方法,你可以在将存入数据库之前或者从数据库中取出来时,利用它预处理模型字段,也可以删除和模型相关的数据等等。

在本节中,我们将会链接post文本中所有的URL,并列出所有存在的Active Record event-like方法。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 设置数据库连接并创建一个名叫post的表:
  1. DROP TABLE IF EXISTS 'post';
  2. CREATE TABLE IF NOT EXISTS 'post' (
  3. 'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4. 'title' VARCHAR(255) NOT NULL,
  5. 'text' TEXT NOT NULL,
  6. PRIMARY KEY ('id')
  7. );
  1. 使用Gii生成Post模型。

如何做…

  1. 添加如下方法到models/Post.php
  1. /**
  2. * @param bool $insert
  3. *
  4. * @return bool
  5. */
  6. public function beforeSave($insert)
  7. {
  8. $this->text = preg_replace(
  9. '~((?:https?|ftps?)://.*?)(|$)~iu',
  10. '<a href="\1">\1</a>\2',
  11. $this->text
  12. );
  13. return parent::beforeSave($insert);
  14. }
  1. 现在尝试保存一个包含链接的帖子,创建controllers/TestController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Post;
  4. use yii\helpers\Html;
  5. use yii\helpers\VarDumper;
  6. use yii\web\Controller;
  7. /**
  8. * Class TestController.
  9. * @package app\controllers
  10. */
  11. class TestController extends Controller
  12. {
  13. public function actionIndex()
  14. {
  15. $post = new Post();
  16. $post->title = 'links test';
  17. $post->text = 'before http://www.yiiframework.com/
  18. after';
  19. $post->save();
  20. return $this->renderContent(Html::tag('pre',
  21. VarDumper::dumpAsString(
  22. $post->attributes
  23. )));
  24. }
  25. }
  1. 现在,运行test/index。你会得到如下结果:

第3章 ActiveRecord, 模型, 数据库 - 图5

工作原理…

ActiveRecord类中实现的方法beforeSave是在保存之前执行的。使用一个正则表达式,我们将所有的URL替换成链接。为了防止保存,你可以返回false。

参考

自动化时间戳

举个例子,我们有一个简单的博客应用。在任何一个博客中,有帖子、评论等等。我们希望在创建或者更新帖子时,生成时间戳。假设我们的帖子模型是BlogPost

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 设置数据库连接并创建一个表名叫blog_post
  1. DROP TABLE IF EXISTS 'blog_post';
  2. CREATE TABLE IF NOT EXISTS 'blog_post' (
  3. 'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4. 'title' VARCHAR(255) NOT NULL,
  5. 'text' TEXT NOT NULL,
  6. 'created_date' INTEGER,
  7. 'modified_date'INTEGER,
  8. PRIMARY KEY ('id')
  9. );
  1. 使用Gii为blog_post表创建一个模型。

如何做…

  1. 将如下方法添加到models/BlogPost.php
  1. /**
  2. * @return array
  3. */
  4. public function behaviors()
  5. {
  6. return [
  7. 'timestamp'=> [
  8. 'class' => 'yii\behaviors\TimestampBehavior',
  9. 'createdAtAttribute' => 'creation_date',
  10. 'updatedAtAttribute' => 'modified_date'
  11. ]
  12. ];
  13. }
  1. 创建controllers/TestController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\BlogPost;
  4. use yii\helpers\Html;
  5. use yii\helpers\VarDumper;
  6. use yii\web\Controller;
  7. /**
  8. * Class TestController.
  9. * @package app\controllers
  10. */
  11. class TestController extends Controller
  12. {
  13. public function actionIndex()
  14. {
  15. $blogPost = new BlogPost();
  16. $blogPost->title = 'Gotcha!';
  17. $blogPost->text = 'We need some laughter to ease the
  18. tension of holiday shopping.';
  19. $blogPost->save();
  20. return $this->renderContent(Html::tag('pre',
  21. VarDumper::dumpAsString($blogPost->attributes)
  22. ));
  23. }
  24. }
  1. 现在运行test/index,你将会得到如下结果:

第3章 ActiveRecord, 模型, 数据库 - 图6

工作原理…

默认情况下,Timestamp behavior填充created_at(创建模型时的时间戳)和updated_at(更新模型时的时间戳)。这样命名这些字段是标准经验,但也可以根据实际需求进行修改。

更多…

例如我们的字段名称是creation_datemodified_date

根据这些字段使用behavior来配置我们的模型。此外,我们应该添加我们的behavior的代码到我们的Post模型:

  1. <?php
  2. namespace app\models;
  3. use Yii;
  4. use yii\db\BaseActiveRecord;
  5. class Post extends \yii\db\ActiveRecord
  6. {
  7. // ..
  8. public function behaviors()
  9. {
  10. return [
  11. [
  12. 'class' => 'yii\behaviors\TimestampBehavior',
  13. 'attributes' => [
  14. BaseActiveRecord::EVENT_BEFORE_INSERT =>
  15. 'creation_date',
  16. BaseActiveRecord::EVENT_BEFORE_UPDATE =>
  17. 'modified_date',
  18. ]
  19. ]
  20. ];
  21. }
  22. // ..
  23. }

在这个例子中,我们设定了creation_datemodified_date,在创建和更新时分别使用如下事件:EVENT_BEFORE_INSERTEVENT_BEFORE_UPDATE

其它…

在一些场景中,你可能希望保存时间戳。例如你希望给一个特定的控制器动作更新last_login字段。在这种情况下,你可以使用如下方式触发时间戳更新:

  1. $model->touch('last_login');

注意touch()不能用于新模型,否则你得到InvalidCallException异常:

  1. $model = new Post();
  2. $model->touch('creation_date');

touch()方法在它内部调用模型保存,所以你不需要再次调用。

参考

欲了解更多信息,参考http://www.yiiframework.com/doc-2.0/guide-conceptbehaviors.html#using-timestampbehavior

自动设置一个作者

Blameable behavior允许你自动更新一个或多个作者的字段。它主要用于生成created_byupdate_by字段的数据。和时间戳行为类似,你可以很容易为这个行为指定一些特殊的参数和必要的事件。

回顾上节中的例子。在我们的博客应用中也有帖子。例如,假设我们的博客模型叫做BlogPost。模型有author_id字段,它标明创建这个帖子的人,以及updater_id,它标明更新这个帖子的人。我们希望在创建或者更新模型事件时自动填写这些字段。现在你将学习如何做到这个。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 设置数据库连接并创建一个名叫blog_post的表:
  1. DROP TABLE IF EXISTS 'blog_post';
  2. CREATE TABLE IF NOT EXISTS 'blog_post' (
  3. 'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4. 'author_id' INT(10) UNSIGNED DEFAULT NULL,
  5. 'updater_id' INT(10) UNSIGNED DEFAULT NULL,
  6. 'title' VARCHAR(255) NOT NULL,
  7. 'text' TEXT NOT NULL,
  8. PRIMARY KEY ('id')
  9. );
  1. 使用Gii为blog_post表生成BlogPost模型。

如何做…

  1. 添加如下behaviors方法到models/BlogPost.php
  1. <?php
  2. namespace app\models;
  3. use Yii;
  4. use yii\db\BaseActiveRecord;
  5. /**
  6. * This is the model class for table "blog_post".
  7. *
  8. * @property integer $id
  9. * @property integer $author_id
  10. * @property integer $updater_id
  11. * @property string $title
  12. * @property string $text
  13. */
  14. class BlogPost extends \yii\db\ActiveRecord
  15. {
  16. /**
  17. * @return array
  18. */
  19. public function behaviors()
  20. {
  21. return [
  22. [
  23. 'class' => 'yii\behaviors\BlameableBehavior',
  24. 'attributes' => [
  25. BaseActiveRecord::EVENT_BEFORE_INSERT =>
  26. 'author_id',
  27. BaseActiveRecord::EVENT_BEFORE_UPDATE =>
  28. 'updater_id'
  29. ]
  30. ]
  31. ];
  32. }
  33. }
  1. 创建controllers/TestController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\BlogPost;
  4. use app\models\User;
  5. use Yii;
  6. use yii\helpers\Html;
  7. use yii\helpers\VarDumper;
  8. use yii\web\Controller;
  9. /**
  10. * Class TestController.
  11. * @package app\controllers
  12. */
  13. class TestController extends Controller
  14. {
  15. public function actionIndex()
  16. {
  17. $users = new User();
  18. $identity = $users->findIdentity(100);
  19. Yii::$app->user->setIdentity($identity);
  20. $blogPost = new BlogPost();
  21. $blogPost->title = 'Very pretty title';
  22. $blogPost->text = 'Success is not final, failure is not fatal...';
  23. $blogPost->save();
  24. return $this->renderContent(Html::tag('pre',
  25. VarDumper::dumpAsString(
  26. $blogPost->attributes
  27. )));
  28. }
  29. }
  1. 运行test/index,你将会得到如下结果: 第3章 ActiveRecord, 模型, 数据库 - 图7

工作原理…

默认情况下,Blameable行为填充created_byupdated_by字段,但是我们根据需求改变。

我们也指定了模型事件和字段,所以在模型创建时,author_id将会填充。类似地,在模型更新时,我们将会填充updater_id

Blameable所做的就是当创建或者更新模型事件发生时,将当前的用户ID值插入到created_byupdated_by字段。这是非常便利的一个方法。每次创建或者更新模型时,我们将自动填充必要的字段。

这对于一些项目非常有用,例如对于大型系统,当多个用户是管理员,你需要知道谁都做了些什么。你也可以为前端实现使用这个。例如,如果你有一个blog_comment表,你希望使用这个方法来记录评论的作者。此外,你可以在控制器中设置作者的字段,但这个行为帮助你避免写不必要的和额外的代码。这将非常有效并且简单。

更多…

有时我们希望使用一个id填充author_idupdater_id,而不是当前用户。在这种情况下,我们需要拆除这个行为:

  1. $model->detachBehavior('blammable');

参考

欲了解更多信息,参考http://www.yiiframework.com/doc-2.0/yii-behaviorsblameablebehavior.html

自动设置一个slug

在web中,slug是一个用于URL中的短文本,用来标识和描述一个资源。slug是URL的一部分,它使用可读的关键词标定一个网页。Sluggable行为是Yii2模型行为,它能为我们生成唯一的slugs。

在本节中,我们将会指导你修改Yii默认URL,修改成一个用户友好和搜索引擎友好的格式。Yii通过一个sluggable行为为这个提供内置支持。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 设置数据库连接,并创建一个名为blog_post的表:
  1. DROP TABLE IF EXISTS 'blog_post';
  2. CREATE TABLE IF NOT EXISTS 'blog_post' (
  3. 'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4. 'title' VARCHAR(255) NOT NULL,
  5. 'slug' VARCHAR(255) NOT NULL,
  6. 'text' TEXT NOT NULL,
  7. PRIMARY KEY ('id')
  8. );
  1. 使用Gii为帖子表创建一个模型。

如何做…

  1. models/BlogPost.php添加如下behaviors方法:
  1. <?php
  2. namespace app\models;
  3. use Yii;
  4. use yii\db\BaseActiveRecord;
  5. class BlogPost extends \yii\db\ActiveRecord
  6. {
  7. // ..
  8. public function behaviors()
  9. {
  10. return [
  11. [
  12. 'class' => 'yii\behaviors\SluggableBehavior',
  13. 'attribute' => 'title',
  14. 'slugAttribute' => 'slug',
  15. 'immutable'=> false,
  16. 'ensureUnique' => true
  17. ]
  18. ];
  19. }
  20. // ..
  21. }
  1. 创建controllers/TestController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\BlogPost;
  4. use Yii;
  5. use yii\helpers\Html;
  6. use yii\helpers\VarDumper;
  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. $blogPostA = new BlogPost();
  17. $blogPostA->title = 'Super Quote title 1';
  18. $blogPostA->text = 'The price of success is hard work, dedication to the job at hand';
  19. $blogPostA->save();
  20. $blogPostB = new BlogPost();
  21. $blogPostB->title = 'Super Quote title 2';
  22. $blogPostB->text = 'Happiness lies in the joy of achievement...';
  23. $blogPostB->save();
  24. return $this->renderContent(
  25. '<pre>' .
  26. VarDumper::dumpAsString(
  27. $blogPostA->attributes
  28. ).
  29. VarDumper::dumpAsString(
  30. $blogPostB->attributes
  31. ) .
  32. '</pre>'
  33. );
  34. }
  35. }
  1. 结果如下:

第3章 ActiveRecord, 模型, 数据库 - 图8

工作原理…

  • Yii为SluggableBehavior提供了一些友好的增强功能。
  • 例如,一旦一个搜索引擎记录了一个slug,你不要再修改页面的URL。
  • 不可变的属性告诉Yii在首次创建后保持slug不变——尽管标题被修改了。
  • 如果用户输入消息覆盖了内容,ensureUnique属性将会自动附加一个唯一的后缀到复件中。这保证了每一个消息都有一个唯一的URL,即时是消息是唯一的。
  • 如果你创建了另一个帖子,和之前有相同的内容,你将会看到它的slug自动增加为hot-update-for-ios-devices-2。

注意

注意:如果你收到了一个关于这个不可变属性的错误,也许是因为你需要运行Composer来更新你的Yii到最新版本。

更多…

  1. 使用Gii为模型app\models\Post生成CRUD和控制器app\controllers\BlogPostController
  2. 添加如下动作到app\controllers\BlogPostController
  1. /**
  2. * @param $slug
  3. *
  4. * @return string
  5. * @throws NotFoundHttpException
  6. */
  7. public function actionSlug($slug)
  8. {
  9. $model = BlogPost::findOne(['slug'=>$slug]);
  10. if ($model === null) {
  11. throw new NotFoundHttpException('The requested page does not exist.');
  12. }
  13. return $this->render('view', [
  14. 'model' => $model,
  15. ]);
  16. }
  1. 如果你使用slug值sluggablebehavior-test运行blogpost/slug,你将会得到如下结果:

第3章 ActiveRecord, 模型, 数据库 - 图9

  1. 建议使用一个Post模型的实例完成先前的slug小节。
  2. 为了美化URL,在config/web.php中添加如下urlManager组件:
  1. //..
  2. 'urlManager' => [
  3. 'enablePrettyUrl' => true,
  4. 'rules' => [
  5. 'blog-post' => 'blog-post/index',
  6. 'blog-post/index' => 'blog-post/index',
  7. 'blog-post/create' => 'blog-post/create',
  8. 'blog-post/view/<id:\d+>' => 'blog-post/view',
  9. 'blog-post/update/<id:\d+>' => 'blog-post/update',
  10. 'blog-post/delete/<id:\d+>' => 'blog-post/delete',
  11. 'blog-post/<slug>' => 'blog-post/slug',
  12. 'defaultRoute' => '/site/index',
  13. ],
  14. ]
  15. //..
  1. 注意blog-post/<slug>' => 'blog-post/slug规则。
  2. 如果你使用你的slug URL访问网页,例如index.php/blog-post/super-quote-title-1/,你将会得到类似步骤3中的结果:

第3章 ActiveRecord, 模型, 数据库 - 图10

参考

欲了解更多信息,参考:

事务

在现代数据库中,事务可以做一些别的事情,例如保证在别人写数据的时候你没有访问数据的权限。但是,基本的思想是一样的——事务能保证无论发生什么事情,你使用的数据都是可感知的。他们保证不存在这样这样一种情况,钱从一个账户中拿走了,但没有进入另外一个账户中。

Yii2支持强大的带有保存点的事务机制。

一个经典的例子是,将钱从一个银行账户转移到另外一个银行账户中。为了做到这一点,首先你需要从源账户中取出钱,然后转移到目标账户中。这个操作必须全部成功。如果半道终止了,钱将会丢失。例如,我们有一个接收账户和一个发送账户。我们需要将钱从发送账户转移到接收账户中。假设我们有一个账户模型。

准备…

账户模型非常简单,只包含idbalance两个字段。

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 创建一个migration,它会使用如下命令添加一个账户表:
  1. ./yii migrate/create create_account_table
  1. 同时,使用如下代码更新刚刚创建的migration:
  1. <?php
  2. use yii\db\Schema;
  3. use yii\db\Migration;
  4. class m150620_062034_create_account_table extends Migration
  5. {
  6. const TABLE_NAME = '{{%account}}';
  7. public function up()
  8. {
  9. $tableOptions = null;
  10. if ($this->db->driverName === 'mysql') {
  11. $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
  12. }
  13. $this->createTable(self::TABLE_NAME, [
  14. 'id' => Schema::TYPE_PK,
  15. 'balance' => ' NUMERIC(15,2) DEFAULT NULL',
  16. ], $tableOptions);
  17. }
  18. public function down()
  19. {
  20. $this->dropTable(self::TABLE_NAME);
  21. }
  22. }
  1. 然后使用如下命令安装migration:
  1. ./yii migrate up
  1. 使用Gii为账户表创建模型。
  2. 创建一个migration,他会添加一些测试Account模型到表中:
  1. ./yii migrate/create add_account_records
  1. 同时,使用如下代码更新刚刚创建的migration:
  1. <?php
  2. use yii\db\Migration;
  3. use app\models\Account;
  4. class m150620_063252_add_account_records extends Migration
  5. {
  6. public function up()
  7. {
  8. $accountFirst = new Account();
  9. $accountFirst->balance = 1110;
  10. $accountFirst->save();
  11. $accountSecond = new Account();
  12. $accountSecond->balance = 779;
  13. $accountSecond->save();
  14. $accountThird = new Account();
  15. $accountThird->balance = 568;
  16. $accountThird->save();
  17. return true;
  18. }
  19. public function down()
  20. {
  21. $this->truncateTable('{{%account}}');
  22. return false;
  23. }
  24. }

如何做…

  1. 添加如下规则到models/Account.php中的rules方法:
  1. public function rules()
  2. {
  3. return [
  4. //..
  5. [['balance'], 'number', 'min' => 0],
  6. //..
  7. ];
  8. }
  1. 假设balance只能是正的,不能是负值。
  2. TestController创建success和error动作:
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Account;
  4. use Yii;
  5. use yii\db\Exception;
  6. use yii\helpers\Html;
  7. use yii\helpers\VarDumper;
  8. use yii\web\Controller;
  9. class TestController extends Controller
  10. {
  11. public function actionSuccess()
  12. {
  13. $transaction = Yii::$app->db->beginTransaction();
  14. try {
  15. $recipient = Account::findOne(1);
  16. $sender = Account::findOne(2);
  17. $transferAmount = 177;
  18. $recipient->balance += $transferAmount;
  19. $sender->balance -= $transferAmount;
  20. if ($sender->save() && $recipient->save()) {
  21. $transaction->commit();
  22. return $this->renderContent(
  23. Html::tag('h1', 'Money transfer was successfully')
  24. );
  25. } else {
  26. $transaction->rollBack();
  27. throw new Exception('Money transfer failed:' .
  28. VarDumper::dumpAsString($sender->getErrors()) .
  29. VarDumper::dumpAsString($recipient->getErrors())
  30. );
  31. }
  32. } catch ( Exception $e ) {
  33. $transaction->rollBack();
  34. throw $e;
  35. }
  36. }
  37. public function actionError()
  38. {
  39. $transaction = Yii::$app->db->beginTransaction();
  40. try {
  41. $recipient = Account::findOne(1);
  42. $sender = Account::findOne(3);
  43. $transferAmount = 1000;
  44. $recipient->balance += $transferAmount;
  45. $sender->balance -= $transferAmount;
  46. if ($sender->save() && $recipient->save()) {
  47. $transaction->commit();
  48. return $this->renderContent(
  49. Html::tag('h1', 'Money transfer was successfully')
  50. );
  51. } else {
  52. $transaction->rollBack();
  53. throw new Exception('Money transfer failed: ' .
  54. VarDumper::dumpAsString($sender->getErrors()) .
  55. VarDumper::dumpAsString($recipient->getErrors())
  56. );
  57. }
  58. } catch ( Exception $e ) {
  59. $transaction->rollBack();
  60. throw $e;
  61. }
  62. }
  63. }
  1. 运行test/success你会得到如下输出:

第3章 ActiveRecord, 模型, 数据库 - 图11

  1. 在这中情况下,如果发生了一些错误,事务机制不会更新接收者账户和发送者账户。
  2. 运行test/error你将会得到如下输出:

第3章 ActiveRecord, 模型, 数据库 - 图12

如果你记得,我们给Account模型添加了一条规则,所以我们的账户只能是正的。在这种情况下,事务将会回滚,它阻止了从发送者账户中取出钱,但没有钱到接收者账户中的情况。

参考

欲了解更多信息,参考:

复制和读写分离

在本节中,我们将学习如何复制和读写分离。我们将会看到slave和master服务器如何帮助我们做到这些。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 设置数据库连接并创建一个名叫post的表:
  1. DROP TABLE IF EXISTS 'blog_post';
  2. CREATE TABLE IF NOT EXISTS 'blog_post' (
  3. 'id' INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  4. 'title' VARCHAR(255) NOT NULL,
  5. 'text' TEXT NOT NULL,
  6. 'created_at' INTEGER,
  7. 'modified_at'INTEGER,
  8. PRIMARY KEY ('id')
  9. );
  1. 为表blog_post创建BlogPost模型。
  2. 按照文章https://www.digitalocean.com/community/tutorials/how-to-set-up-master-slave-replication-inmysql/中的描述,在你的数据库服务器之间,配置主从复制。
  3. config/main.php中配置db组件,下面是个例子:
  1. 'components' => [
  2. // ..
  3. 'db' => [
  4. 'class' => 'yii\db\Connection',
  5. 'dsn' => 'mysql:host=4.4.4.4;dbname=masterdb',
  6. 'username' => 'master',
  7. 'password' => 'pass',
  8. 'charset' => 'utf8',
  9. 'slaveConfig' => [
  10. 'username' => 'slave',
  11. 'password' => 'pass',
  12. ],
  13. // list of slave configurations
  14. 'slaves' => [
  15. ['dsn' => 'mysql:host=5.5.5.5;dbname=slavedb']
  16. ]
  17. ],
  18. //..
  19. ]

如何做…

  1. 创建TestController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\BlogPost;
  4. use Yii;
  5. use yii\helpers\Html;
  6. use yii\helpers\VarDumper;
  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. $masterModel = new BlogPost();
  16. $masterModel->title = 'Awesome';
  17. $masterModel->text = 'Something is going on..';
  18. $masterModel->save();
  19. $postId = $masterModel->id;
  20. $replModel = BlogPost::findOne($postId);
  21. return $this->renderContent(
  22. Html::tag('h2', 'Master') .
  23. Html::tag('pre', VarDumper::dumpAsString(
  24. $masterModel
  25. ? $masterModel->attributes
  26. : null
  27. )) .
  28. Html::tag('h2', 'Slave') .
  29. Html::tag('pre', VarDumper::dumpAsString(
  30. $replModel
  31. ? $replModel->attributes
  32. : null
  33. ))
  34. );
  35. }
  36. }
  1. 运行test/index,你将会得到如下输出:

第3章 ActiveRecord, 模型, 数据库 - 图13

工作原理…

slave服务器用于数据读取,master服务器用于数据写入。ActiveRecord模型保存到master服务器中,数据复制到slave服务器中,然后$replModel在它上边找到。

更多…

\yii\db\Connection组件支持负载均衡和slaves之间的失效转移。当首都执行一个读查询时,\yii\db\Connection组件将会随机挑选一个slave并进行连接。如果这个slave死掉了,它将会尝试另外一个。如果所有的slaves都不可用,它将会连接到master。通过配置一个服务器状态缓存,死掉的服务器将会被记住,它将会在一段时间内不会被使用。

参考

欲了解更多信息,参考如下链接:

实现单表继承

关系数据库不支持继承。如果我们需要在数据库中存储继承,我们需要通过代码来支持它。这个代码应该是高效的,从而它应该生成尽量少的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,你将会得到如下输出:

第3章 ActiveRecord, 模型, 数据库 - 图14

工作原理…

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

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

参考

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