使用Yii框架,最快地将设计在MySQL上的ActiveRecord迁移到MongoDB上
前提 (Qian ti)
现代的Web应用程序由于不知道何时会产生大量访问,所以需要从初期阶段就意识到负载均衡的设计。应用服务器通过扩展规模以实现负载均衡,而数据库则通常通过读取副本模式(使用一个主服务器进行写入,从多个从服务器进行读取)来分担负载。
尽管如此,我们仍然困扰于主数据库写入事务成为瓶颈的问题。因此,对于不需要ACID特性但有大量写入的数据,我们会选择将其与主数据库分开,并使用NoSQL数据库。如果使用MongoDB,可以通过使用分片功能来分散处理写入负载。
然而,在尚未完成全面环境建立的原型阶段,我们会希望能在简单的环境下尽快实现朴素的设计思想。从一开始就有意识地进行性能调优建模并不容易。最好的方法是暂时忘记性能问题,尽快制作出原型。但即使这样,如果没有对这种技术债务(为了快速启动而借入的债务)的后续解决保证,我们无法安心地进行工作。
我们将看一下在Yii框架中是如何解决这个问题的。
创造一个名为“原型”的负债
为了更容易理解,我将举一个稍微夸张的例子。
投稿記事需要一个有点不同的页面浏览计数。它可以动态地从过去任意日期开始计算页面浏览次数。
所以,为了记录所有页面浏览次数,我们决定设置具有has-many关系的AccessLog。每当浏览网页时,我们将创建一条记录。我们考虑用MySQL来实现这个功能。
public function up()
{
$this->createTable('{{%access_log}}', [
'id' => Schema::TYPE_BIGPK,
'post_id' => Schema::TYPE_INTEGER . ' NOT NULL',
'recorded_at' => Schema::TYPE_INTEGER . ' NOT NULL',
]);
$this->addForeignKey('fk_access_log_post_id', '{{%access_log}}', 'post_id', '{{%post}}', 'id', 'CASCADE');
}
在Gii中生成并添加方法。
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "{{%post}}".
*
* @property integer $id
* @property string $title
* @property string $body
*
* @property AccessLog[] $accessLogs
*/
class Post extends \yii\db\ActiveRecord
{
public static function tableName()
{
return '{{%post}}';
}
// 中略
/**
* @return \yii\db\ActiveQuery
*/
public function getAccessLogs()
{
return $this->hasMany(AccessLog::className(), ['post_id' => 'id']);
}
/**
* 投稿記事のページが表示されたときにレコードを作成する
*/
public function recordPageViewed()
{
$log = new AccessLog();
$log->post_id = $this->id;
$log->recorded_at = time();
$log->save(false);
// $this->refresh();
}
/**
* ある日時以降のページビュー数を得る
*
* @param integer $timestamp
* @return integer
*/
public function getPageViewAfter($timestamp=null)
{
$query = $this->getrelation('accessLogs');
if ($timestamp) {
$query->where(['>=', 'recorded_at', $timestamp]);
}
return $query->count();
}
}
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "{{%access_log}}".
*
* @property integer $id
* @property integer $post_id
* @property integer $recorded_at
*
* @property Post $post
*/
class AccessLog extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return '{{%access_log}}';
}
// 中略
/**
* @return \yii\db\ActiveQuery
*/
public function getPost()
{
return $this->hasOne(Post::className(), ['id' => 'post_id']);
}
}
在视图处理结束时调用Post::recordPageViewed()方法将数据插入access_log表,以达到所需的功能。
在开发阶段,它可以正常运行,但是不能直接发布出去。对于开发环境,只需要一个人进行测试,但是在Web上存在着成千上万潜在用户。仅仅通过页面显示就导致主数据库写入操作,从而造成负载过重是一个严重的问题。还有就是如何偿还这种技术债务才是关键。
将MySQL的ActiveRecord转换为MongoDB。
Yii Framework 2.0 在开发过程中同时为关系型数据库和NoSQL设计了 ActiveRecord,并且共享了大部分抽象类和特性。这样,即使是不同类型的 ActiveRecord,也具有相当大的兼容性。
要使用MongoDB,需要在Composer中添加安装库。
$ composer require yiisoft/yii2-mongodb
在应用程序的设置文件中添加mongodb组件。
$config = [
'components' => [
// ...
'mongodb' => [
'class' => 'yii\mongodb\Connection',
'dsn' => 'mongodb://localhost:27017/mydb',
],
],
在这之后,将AccessLog转换为MongoDB的ActiveRecord,那么之前提到的这两个类的实现会变成这样。
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "{{%post}}".
*
* @property integer $id
* @property string $title
* @property string $body
*
* @property AccessLog[] $accessLogs
*/
class Post extends \yii\db\ActiveRecord
{
/**
* @inheritdoc
*/
public static function tableName()
{
return '{{%post}}';
}
// 中略
/**
* @return \yii\mongodb\ActiveQuery
*/
public function getAccessLogs()
{
return $this->hasMany(AccessLog::className(), ['post_id' => 'id']);
}
/**
* 投稿記事のページが表示されたときにレコードを作成する
*/
public function recordPageViewed()
{
$log = new AccessLog();
$log->post_id = $this->id;
$log->recorded_at = time();
$log->save(false);
// $this->refresh();
}
/**
* ある日時以降のページビュー数を得る
*
* @param integer $timestamp
* @return integer
*/
public function getPageViewAfter($timestamp=null)
{
$query = $this->getrelation('accessLogs');
if ($timestamp) {
$query->where(['gte' => ['recorded_at' => $timestamp]]);
}
return $query->count();
}
}
<?php
namespace app\models;
/**
* This is the model class for collection "access_log".
*
* @property integer $_id
* @property integer $post_id
* @property integer $recorded_at
*
* @property Post $post
*/
class AccessLog extends \yii\mongodb\ActiveRecord
{
/**
* @inheritdoc
*/
public static function collectionName()
{
return 'access_log';
}
/**
* @inheritdoc
*/
public function attributes()
{
return ['_id', 'post_id', 'recorded_at'];
}
// 中略、RDBのときと全く同じ
/**
* @return \yii\db\ActiveQuery
*/
public function getPost()
{
return $this->hasOne(Post::className(), ['id' => 'post_id']);
}
}
我写下了没有改变的部分,没有省略。您能够理解,独特功能的部分和较为复杂的部分变化较少吗?
以下是具体的更改点:
-
- 名前空間の違う yii\mongodb\ActiveRecord と yii\mongodb\ActiveQuery が登場
tableName() は collectionName() になった
MongoDB にはスキーマがないので attributes() でフィールドを明示
MongoDB の習慣に合わせて id を _id に変更
>= を使った条件式は MongoDB の大小比較 gte を使った構文に
这些都是只需要机械操作就能完成的任务。
/**
- * @return \yii\db\ActiveQuery
+ * @return \yii\mongodb\ActiveQuery
*/
public function getAccessLogs()
public function getPageViewAfter($timestamp=null)
{
$query = $this->getrelation('accessLogs');
if ($timestamp) {
- $query->where(['>=', 'recorded_at', $timestamp]);
+ $query->where(['gte' => ['recorded_at' => $timestamp]]);
}
- * @property integer $id
+ * @property integer $_id
*/
-class AccessLog extends \yii\db\ActiveRecord
+class AccessLog extends \yii\mongodb\ActiveRecord
{
/**
* @inheritdoc
*/
- public static function tableName()
+ public static function collectionName()
{
- return '{{%access_log}}';
+ return 'access_log';
}
+ /**
+ * @inheritdoc
+ */
+ public function attributes()
+ {
+ return ['_id', 'post_id', 'recorded_at'];
+ }
为什么更改点如此之少呢?当使用Yii的查询构建器时,如果想要指定某个字段的值为如此,且另一个字段的值为如此来进行搜索时,可以按照以下方式编写。
$query->where([
'field_a' => $value1,
'field_b' => $value2,
]);
当事情变得有点复杂时,我们可以使用基于键值对的关联数组进行嵌套。这种风格非常类似于MongoDB的查询语法(也与ElasticSearch相似)。虽然不能完全以同样的方式编写任何查询,但我们仍然可以看出它具有相当一致性意识的NoSQL查询构建器。
如果使用更简单或者没有 KVS 的话,可以说它在伪代码中等价于 ActiveRecord::find()->where([‘id’ => $key])->one();。可以用 ActiveRecord::findOne($key); 替换这种模式。
Yii框架在改变ActiveRecord类型时的影响非常小,它成功地进行了良好的抽象化,使得无论使用哪种类型都能保持一致的思维方式。
值得一提的是,MongoDB和RDB之间的关联关系得以保持。即使经过更改,仍然可以使用像 $post->accessLogs 或 $accessLog->post 这样的代码。
当Yii加载相关记录时,不使用JOIN,而是使用使用关联键对应的查询来实现。因此,即使在不同的存储之间的关系中,也能正确地建立关联。(只要能够正确编写没有冲突的关系键的程序)
在Yii中,我们可以创建不受存储实体影响的接口不变的模型。因此,当应用程序的其他部分发生突然的重大变化并导致崩溃时,这种情况将不会发生。我们可以同时保持原型的运行状态,并逐步进行优化。