16 关注者

Active Record

Active Record 为访问和操作存储在数据库中的数据提供了一个面向对象的接口。Active Record 类与数据库表相关联,Active Record 实例对应于该表的一行,Active Record 实例的属性表示该行中特定列的值。您无需编写原始 SQL 语句,而是可以访问 Active Record 属性并调用 Active Record 方法来访问和操作存储在数据库表中的数据。

例如,假设 Customer 是与 customer 表相关联的 Active Record 类,而 namecustomer 表的一列。您可以编写以下代码将新行插入 customer

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();

以上代码等同于使用以下原始 SQL 语句(针对 MySQL),该语句直观性较差,更容易出错,甚至可能在您使用不同类型的数据库时出现兼容性问题

$db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [
    ':name' => 'Qiang',
])->execute();

Yii 为以下关系型数据库提供了 Active Record 支持

此外,Yii 还支持使用 Active Record 与以下 NoSQL 数据库一起使用

  • Redis 2.6.12 或更高版本:通过 yii\redis\ActiveRecord,需要 yii2-redis 扩展
  • MongoDB 1.3.0 或更高版本:通过 yii\mongodb\ActiveRecord,需要 yii2-mongodb 扩展

在本教程中,我们将主要介绍关系型数据库中 Active Record 的用法。但是,这里描述的大部分内容也适用于 NoSQL 数据库的 Active Record。

声明 Active Record 类

要开始,请通过扩展 yii\db\ActiveRecord 声明 Active Record 类。

设置表名

默认情况下,每个 Active Record 类都与其数据库表相关联。 tableName() 方法通过 yii\helpers\Inflector::camel2id() 转换类名来返回表名。如果表没有按照此约定命名,您可以覆盖此方法。

还可以应用默认的 tablePrefix。例如,如果 tablePrefixtbl_,则 Customer 变成 tbl_customer,而 OrderItem 变成 tbl_order_item

如果表名被赋予为 {{%TableName}},则百分号 % 将被替换为表前缀。例如,{{%post}} 变成 {{tbl_post}}。表名周围的方括号用于 在 SQL 查询中引号

在下面的示例中,我们为 customer 数据库表声明了一个名为 Customer 的 Active Record 类。

namespace app\models;

use yii\db\ActiveRecord;

class Customer extends ActiveRecord
{
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
    
    /**
     * @return string the name of the table associated with this ActiveRecord class.
     */
    public static function tableName()
    {
        return '{{customer}}';
    }
}

Active Record 被称为“模型”

Active Record 实例被视为 模型。出于这个原因,我们通常将 Active Record 类放在 app\models 命名空间(或用于保存模型类的其他命名空间)下。

因为 yii\db\ActiveRecord 继承自 yii\base\Model,所以它继承了所有 模型 特性,例如属性、验证规则、数据序列化等。

连接到数据库

默认情况下,Active Record 使用 db 应用程序组件 作为 DB 连接 来访问和操作数据库数据。如 数据库访问对象 中所述,您可以在应用程序配置中配置 db 组件,如下所示,

return [
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=testdb',
            'username' => 'demo',
            'password' => 'demo',
        ],
    ],
];

如果您想使用与 db 组件不同的数据库连接,您应该覆盖 getDb() 方法

class Customer extends ActiveRecord
{
    // ...

    public static function getDb()
    {
        // use the "db2" application component
        return \Yii::$app->db2;  
    }
}

查询数据

声明 Active Record 类后,您可以使用它从相应的数据库表中查询数据。该过程通常包括以下三个步骤

  1. 通过调用 yii\db\ActiveRecord::find() 方法创建一个新的查询对象;
  2. 通过调用 查询构建方法 构建查询对象;
  3. 调用 查询方法 以 Active Record 实例的形式检索数据。

如您所见,这与使用 查询构建器 的过程非常相似。唯一的区别是,您不是使用 new 运算符来创建查询对象,而是调用 yii\db\ActiveRecord::find() 来返回一个新的查询对象,该对象是 yii\db\ActiveQuery 类。

以下是使用 Active Query 查询数据的示例

// return a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
    ->where(['id' => 123])
    ->one();

// return all active customers and order them by their IDs
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->orderBy('id')
    ->all();

// return the number of active customers
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->count();

// return all customers in an array indexed by customer IDs
// SELECT * FROM `customer`
$customers = Customer::find()
    ->indexBy('id')
    ->all();

在上面,$customer 是一个 Customer 对象,而 $customers 是一个 Customer 对象数组。它们都使用从 customer 表中检索到的数据填充。

信息:因为 yii\db\ActiveQuery 继承自 yii\db\Query,所以您可以使用所有查询构建方法和查询方法,如 查询构建器 部分所述。

因为根据主键值或一组列值进行查询是一项常见任务,所以 Yii 为此目的提供了两种快捷方法

这两种方法都可以接受以下参数格式之一

  • 一个标量值:该值被视为要查找的所需主键值。Yii 将通过读取数据库模式信息来自动确定哪个列是主键列。
  • 一个标量值数组:该数组被视为要查找的所需主键值。
  • 一个关联数组:键是列名,值是要查找的相应所需列值。有关更多详细信息,请参阅 哈希格式

以下代码展示了如何使用这些方法

// returns a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// returns customers whose ID is 100, 101, 123 or 124
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// returns an active customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
    'id' => 123,
    'status' => Customer::STATUS_ACTIVE,
]);

// returns all inactive customers
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
    'status' => Customer::STATUS_INACTIVE,
]);

警告:如果您需要将用户输入传递给这些方法,请确保输入值是标量,或者在数组条件的情况下,请确保数组结构无法从外部更改

// yii\web\Controller ensures that $id is scalar
public function actionView($id)
{
    $model = Post::findOne($id);
    // ...
}

// explicitly specifying the column to search, passing a scalar or array here will always result in finding a single record
$model = Post::findOne(['id' => Yii::$app->request->get('id')]);

// do NOT use the following code! it is possible to inject an array condition to filter by arbitrary column values!
$model = Post::findOne(Yii::$app->request->get('id'));

注意:yii\db\ActiveRecord::findOne() 也不 yii\db\ActiveQuery::one() 将不会向生成的 SQL 语句添加 LIMIT 1。如果您的查询可能会返回许多行数据,您应该显式调用 limit(1) 以提高性能,例如,Customer::find()->limit(1)->one()

除了使用查询构建方法,您还可以编写原始 SQL 来查询数据并将结果填充到 Active Record 对象中。您可以通过调用 yii\db\ActiveRecord::findBySql() 方法来实现

// returns all inactive customers
$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

不要在调用 findBySql() 后调用额外的查询构建方法,因为它们会被忽略。

访问数据

如前所述,从数据库中取回的数据将填充到 Active Record 实例中,查询结果的每一行对应一个 Active Record 实例。您可以通过访问 Active Record 实例的属性来访问列值,例如,

// "id" and "email" are the names of columns in the "customer" table
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

注意:Active Record 属性以与关联表列相同的方式区分大小写。Yii 会自动为关联表的每一列在 Active Record 中定义一个属性。您不应该重新声明任何属性。

因为 Active Record 属性以表列命名,所以您可能会发现自己编写了类似 $customer->first_name 的 PHP 代码,如果您的表列以这种方式命名,则使用下划线来分隔属性名称中的单词。如果您关心代码风格的一致性,您应该相应地重命名您的表列(例如,使用 camelCase)。

数据转换

通常会发生这样的情况,即输入和/或显示的数据格式与存储在数据库中的数据格式不同。例如,在数据库中,您将客户的生日存储为 UNIX 时间戳(虽然这不是一个好的设计),而在大多数情况下,您希望将生日以 'YYYY/MM/DD' 格式的字符串形式进行操作。为了实现这个目标,您可以在 Customer Active Record 类中定义数据转换方法,如下所示

class Customer extends ActiveRecord
{
    // ...

    public function getBirthdayText()
    {
        return date('Y/m/d', $this->birthday);
    }
    
    public function setBirthdayText($value)
    {
        $this->birthday = strtotime($value);
    }
}

现在,在您的 PHP 代码中,您不会访问 $customer->birthday,而是访问 $customer->birthdayText,这将允许您以 'YYYY/MM/DD' 格式输入和显示客户的生日。

提示:上面的示例展示了一种以不同格式转换数据的通用方法。如果您正在使用日期值,您可以使用 DateValidatoryii\jui\DatePicker,它们更易于使用且功能更强大。

以数组形式检索数据

虽然以 Active Record 对象的形式检索数据很方便且灵活,但在您需要取回大量数据时,由于内存占用过大,并不总是可取的。在这种情况下,您可以通过在执行查询方法之前调用 asArray() 来使用 PHP 数组检索数据

// return all customers
// each customer is returned as an associative array
$customers = Customer::find()
    ->asArray()
    ->all();

注意:虽然此方法可以节省内存并提高性能,但它更接近于较低的 DB 抽象层,您将失去大多数 Active Record 功能。一个非常重要的区别在于列值的类型。当您以 Active Record 实例的形式返回数据时,列值将根据实际的列类型自动进行类型转换;另一方面,当您以数组的形式返回数据时,列值将是字符串(因为它们是未经处理的 PDO 结果),无论它们的实际列类型如何。

分批检索数据

查询构建器 中,我们已经解释过,您可以使用批次查询来最大限度地减少从数据库查询大量数据时的内存使用量。您可以在 Active Record 中使用相同的技术。例如,

// fetch 10 customers at a time
foreach (Customer::find()->batch(10) as $customers) {
    // $customers is an array of 10 or fewer Customer objects
}

// fetch 10 customers at a time and iterate them one by one
foreach (Customer::find()->each(10) as $customer) {
    // $customer is a Customer object
}

// batch query with eager loading
foreach (Customer::find()->with('orders')->each() as $customer) {
    // $customer is a Customer object with the 'orders' relation populated
}

保存数据

使用 Active Record,您可以通过执行以下步骤轻松地将数据保存到数据库中

  1. 准备一个 Active Record 实例
  2. 将新值赋给 Active Record 属性
  3. 调用 yii\db\ActiveRecord::save() 将数据保存到数据库中。

例如,

// insert a new row of data
$customer = new Customer();
$customer->name = 'James';
$customer->email = '[email protected]';
$customer->save();

// update an existing row of data
$customer = Customer::findOne(123);
$customer->email = '[email protected]';
$customer->save();

save() 方法可以插入或更新一行数据,具体取决于 Active Record 实例的状态。如果实例是通过 new 运算符新创建的,调用 save() 将导致插入新行;如果实例是查询方法的结果,调用 save() 将更新与该实例关联的行。

您可以通过检查其 isNewRecord 属性值来区分 Active Record 实例的两种状态。此属性也由 save() 在内部使用,如下所示

public function save($runValidation = true, $attributeNames = null)
{
    if ($this->getIsNewRecord()) {
        return $this->insert($runValidation, $attributeNames);
    } else {
        return $this->update($runValidation, $attributeNames) !== false;
    }
}

提示:您可以直接调用 insert()update() 来插入或更新一行。

数据验证

因为 yii\db\ActiveRecord 继承自 yii\base\Model,所以它共享相同的 数据验证 功能。您可以通过覆盖 rules() 方法来声明验证规则,并通过调用 validate() 方法来执行数据验证。

当您调用 save() 时,默认情况下它会自动调用 validate()。只有当验证通过时,它才会实际保存数据;否则它将简单地返回 false,您可以检查 errors 属性以检索验证错误消息。

提示:如果您确定您的数据不需要验证(例如,数据来自可信来源),您可以调用 save(false) 来跳过验证。

批量赋值

与普通的模型一样,Active Record 实例也支持批量赋值功能。使用此功能,您可以通过单个 PHP 语句为 Active Record 实例的多个属性赋值,如下所示。请记住,只能安全属性进行批量赋值。

$values = [
    'name' => 'James',
    'email' => '[email protected]',
];

$customer = new Customer();

$customer->attributes = $values;
$customer->save();

更新计数器

在数据库表中递增或递减列是一个常见任务。我们将这些列称为“计数器列”。您可以使用updateCounters() 更新一个或多个计数器列。例如,

$post = Post::findOne(100);

// UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100
$post->updateCounters(['view_count' => 1]);

注意:如果您使用yii\db\ActiveRecord::save() 更新计数器列,您可能会得到不准确的结果,因为多个请求可能同时读取和写入相同的计数器值,从而导致计数器值不一致。

脏属性

当您调用save() 保存 Active Record 实例时,仅会保存脏属性。如果属性的值在从数据库加载或最近保存到数据库后被修改,则该属性被认为是脏属性。请注意,无论 Active Record 实例是否有脏属性,都会执行数据验证。

Active Record 会自动维护脏属性列表。它通过维护属性值的旧版本并将其与最新版本进行比较来实现。您可以调用yii\db\ActiveRecord::getDirtyAttributes() 获取当前为脏属性的属性。您也可以调用yii\db\ActiveRecord::markAttributeDirty() 显式地将属性标记为脏属性。

如果您对属性值在最近一次修改之前的值感兴趣,您可以调用getOldAttributes()getOldAttribute()

注意:旧值和新值的比较将使用===运算符进行,因此即使值具有相同的值但类型不同,也会被认为是脏属性。这种情况在模型从 HTML 表单接收用户输入时经常发生,因为每个值都以字符串形式表示。为了确保例如整数值的正确类型,您可以应用验证过滤器['attributeName', 'filter', 'filter' => 'intval']。这适用于所有 PHP 类型转换函数,例如intval()floatval()boolval 等。

默认属性值

您的一些表列可能在数据库中定义了默认值。有时,您可能希望使用这些默认值预先填充 Active Record 实例的 Web 表单。为了避免再次编写相同的默认值,您可以调用loadDefaultValues() 将数据库定义的默认值填充到相应的 Active Record 属性中。

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz will be assigned the default value declared when defining the "xyz" column

属性类型转换

由于yii\db\ActiveRecord 由查询结果填充,因此它会使用数据库表模式中的信息,对属性值执行自动类型转换。这使得从声明为整型的表列检索到的数据可以在 ActiveRecord 实例中以 PHP 整数、布尔值以布尔值等方式填充。但是,类型转换机制有一些限制。

  • 浮点值不会被转换,将被表示为字符串,否则可能会丢失精度。
  • 整数的转换取决于您使用的操作系统的整数容量。特别是:声明为“无符号整数”或“大整数”的列的值将在 64 位操作系统上转换为 PHP 整数,而在 32 位操作系统上,它们将被表示为字符串。

请注意,属性类型转换仅在从查询结果填充 ActiveRecord 实例时执行。对于从 HTTP 请求加载或通过属性访问直接设置的值,没有自动转换。在为 ActiveRecord 数据保存准备 SQL 语句时,也会使用表模式,确保值以正确的类型绑定到查询。但是,ActiveRecord 实例属性值在保存过程中不会被转换。

提示:您可以使用yii\behaviors\AttributeTypecastBehavior 在 ActiveRecord 验证或保存时促进属性值的类型转换。

从 2.0.14 开始,Yii ActiveRecord 支持复杂数据类型,例如 JSON 或多维数组。

MySQL 和 PostgreSQL 中的 JSON

在数据填充后,来自 JSON 列的值将根据标准 JSON 解码规则自动从 JSON 解码。

要将属性值保存到 JSON 列,ActiveRecord 会自动创建一个JsonExpression 对象,该对象将在QueryBuilder 级别上被编码为 JSON 字符串。

PostgreSQL 中的数组

在数据填充后,来自数组列的值将自动从 PgSQL 符号解码为一个ArrayExpression 对象。它实现了 PHP 的ArrayAccess接口,因此您可以将其用作数组,或者调用->getValue()获取数组本身。

要将属性值保存到数组列,ActiveRecord 会自动创建一个ArrayExpression 对象,该对象将被QueryBuilder 编码为数组的 PgSQL 字符串表示形式。

您也可以对 JSON 列使用条件。

$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])])

要详细了解表达式构建系统,请阅读Query Builder – 添加自定义条件和表达式文章。

更新多行

上面描述的方法都作用于单个 Active Record 实例,导致单个表行的插入或更新。要同时更新多行,您应该调用updateAll(),而不是单个实例方法,这是一个静态方法。

// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);

类似地,您可以调用updateAllCounters() 同时更新多行的计数器列。

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

删除数据

要删除一行数据,首先检索与该行相对应的 Active Record 实例,然后调用yii\db\ActiveRecord::delete() 方法。

$customer = Customer::findOne(123);
$customer->delete();

您可以调用yii\db\ActiveRecord::deleteAll() 删除多行或所有数据。例如,

Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]);

注意:在调用deleteAll() 时要格外小心,因为如果您在指定条件时犯了错误,它可能会完全擦除表中的所有数据。

Active Record 生命周期

在 Active Record 用于不同目的时,了解 Active Record 的生命周期非常重要。在每个生命周期中,都会调用特定的一系列方法,您可以覆盖这些方法来获得自定义生命周期的机会。您还可以响应生命周期中触发的特定 Active Record 事件,以注入自定义代码。当您开发需要自定义 Active Record 生命周期 行为 时,这些事件特别有用。

下面将总结各种 Active Record 生命周期以及生命周期中涉及的方法/事件。

新实例生命周期

当通过new运算符创建新的 Active Record 实例时,将发生以下生命周期

  1. 类构造函数。
  2. init():触发EVENT_INIT 事件。

查询数据生命周期

当通过查询方法之一查询数据时,每个新填充的 Active Record 将经历以下生命周期

  1. 类构造函数。
  2. init():触发EVENT_INIT 事件。
  3. afterFind():触发EVENT_AFTER_FIND 事件。

保存数据生命周期

当调用save() 插入或更新 Active Record 实例时,将发生以下生命周期

  1. beforeValidate():触发EVENT_BEFORE_VALIDATE 事件。如果该方法返回falseyii\base\ModelEvent::$isValidfalse,则将跳过其余步骤。
  2. 执行数据验证。如果数据验证失败,则将跳过步骤 3 之后的步骤。
  3. afterValidate():触发EVENT_AFTER_VALIDATE 事件。
  4. beforeSave():触发EVENT_BEFORE_INSERTEVENT_BEFORE_UPDATE 事件。如果该方法返回falseyii\base\ModelEvent::$isValidfalse,则将跳过其余步骤。
  5. 执行实际的数据插入或更新。
  6. afterSave():触发EVENT_AFTER_INSERTEVENT_AFTER_UPDATE 事件。

删除数据生命周期

当调用delete() 删除 Active Record 实例时,将发生以下生命周期

  1. beforeDelete():触发EVENT_BEFORE_DELETE 事件。如果该方法返回falseyii\base\ModelEvent::$isValidfalse,则将跳过其余步骤。
  2. 执行实际的数据删除。
  3. afterDelete():触发EVENT_AFTER_DELETE 事件。

注意:调用以下任何方法都不会启动上述任何生命周期,因为它们直接作用于数据库,而不是基于记录。

注意:由于性能方面的考虑,默认情况下不支持 DI。如果需要,您可以通过覆盖instantiate() 方法来通过Yii::createObject() 实例化类。

public static function instantiate($row)
{
    return Yii::createObject(static::class);
}

刷新数据生命周期

当调用refresh() 刷新 Active Record 实例时,如果刷新成功且该方法返回true,则会触发EVENT_AFTER_REFRESH 事件。

使用事务

在使用 Active Record 时,有两种方法可以 使用事务

第一种方法是显式地将 Active Record 方法调用封装在一个事务块中,如下所示:

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
});

// or alternatively

$transaction = Customer::getDb()->beginTransaction();
try {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
    $transaction->commit();
} catch(\Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

注意:在上面的代码中,我们有两个 catch 块,以兼容 PHP 5.x 和 PHP 7.x。\Exception 自 PHP 7.0 起实现了 \Throwable 接口,因此如果您的应用程序仅使用 PHP 7.0 及更高版本,则可以跳过包含 \Exception 的部分。

第二种方法是在 yii\db\ActiveRecord::transactions() 方法中列出需要事务支持的数据库操作。例如:

class Customer extends ActiveRecord
{
    public function transactions()
    {
        return [
            'admin' => self::OP_INSERT,
            'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
            // the above is equivalent to the following:
            // 'api' => self::OP_ALL,
        ];
    }
}

yii\db\ActiveRecord::transactions() 方法中,应返回一个数组,其键是 场景 名称,其值为相应的操作,这些操作应该包含在事务中。您应该使用以下常量来引用不同的数据库操作

使用 | 操作符连接以上常量以指示多个操作。您也可以使用快捷常量 OP_ALL 来引用以上所有三个操作。

使用此方法创建的事务将在调用 beforeSave() 之前启动,并在 afterSave() 执行完毕后提交。

乐观锁

乐观锁是一种防止多个用户更新同一行数据时可能发生的冲突的方法。例如,用户 A 和用户 B 同时编辑同一篇维基百科文章。用户 A 保存他的编辑后,用户 B 点击“保存”按钮试图保存他的编辑。因为用户 B 实际上是在编辑文章的旧版本,所以最好有一种方法可以阻止他保存文章并向他显示一些提示信息。

乐观锁通过使用一列记录每一行的版本号来解决上述问题。当一行以过时的版本号保存时,将抛出一个 yii\db\StaleObjectException 异常,这将阻止该行被保存。乐观锁仅在您使用 yii\db\ActiveRecord::update()yii\db\ActiveRecord::delete() 分别更新或删除现有数据行时才受支持。

要使用乐观锁,

  1. 在与 Active Record 类关联的数据库表中创建一个列来存储每一行的版本号。该列应该为大整数类型(在 MySQL 中为 BIGINT DEFAULT 0)。
  2. 覆盖 yii\db\ActiveRecord::optimisticLock() 方法以返回该列的名称。
  3. 在您的模型类中实现 OptimisticLockBehavior 以自动从接收到的请求中解析其值。从验证规则中删除版本属性,因为 OptimisticLockBehavior 应该处理它。
  4. 在接收用户输入的 Web 表单中,添加一个隐藏字段来存储正在更新的行当前版本号。
  5. 在使用 Active Record 更新行的控制器操作中,尝试捕获 yii\db\StaleObjectException 异常。实现必要的业务逻辑(例如合并更改、提示过时数据)以解决冲突。

例如,假设版本列名为 version。您可以使用以下代码实现乐观锁。

// ------ view code -------

use yii\helpers\Html;

// ...other input fields
echo Html::activeHiddenInput($model, 'version');


// ------ controller code -------

use yii\db\StaleObjectException;

public function actionUpdate($id)
{
    $model = $this->findModel($id);

    try {
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    } catch (StaleObjectException $e) {
        // logic to resolve the conflict
    }
}

// ------ model code -------

use yii\behaviors\OptimisticLockBehavior;

public function behaviors()
{
    return [
        OptimisticLockBehavior::class,
    ];
}

public function optimisticLock()
{
    return 'version';
}

注意:因为 OptimisticLockBehavior 将确保记录仅在用户通过直接解析 getBodyParam() 提交有效版本号时才保存,所以扩展您的模型类并在父模型中执行步骤 2 同时将行为(步骤 3)附加到子类可能会有用,这样您就可以拥有一个专用于内部使用的实例,同时将另一个实例与负责接收最终用户输入的控制器绑定。或者,您可以通过配置其 value 属性来实现您自己的逻辑。

处理关系数据

除了处理单个数据库表外,Active Record 还能够将相关数据整合在一起,使它们可以通过主数据轻松访问。例如,客户数据与订单数据相关联,因为一个客户可能下过一个或多个订单。通过适当声明这种关系,您可以使用表达式 $customer->orders 访问客户的订单信息,它以 Order Active Record 实例数组的形式返回客户的订单信息。

声明关系

要使用 Active Record 处理关系数据,您首先需要在 Active Record 类中声明关系。这个任务就像为每个感兴趣的关系声明一个关系方法一样简单,如下所示:

class Customer extends ActiveRecord
{
    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    // ...

    public function getCustomer()
    {
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

在上面的代码中,我们为 Customer 类声明了一个 orders 关系,为 Order 类声明了一个 customer 关系。

每个关系方法都必须命名为 getXyz。我们称 xyz(第一个字母为小写)为关系名称。请注意,关系名称是区分大小写的

在声明关系时,您应该指定以下信息

  • 关系的多重性:通过调用 hasMany()hasOne() 来指定。在上面的示例中,您可以在关系声明中轻松地读出,一个客户可以有多个订单,而一个订单只有一个客户。
  • 相关 Active Record 类的名称:作为 hasMany()hasOne() 的第一个参数指定。推荐的做法是调用 Xyz::class 来获取类名字符串,以便您可以在编译阶段获得 IDE 自动完成支持以及错误检测。
  • 两种类型数据之间的链接:指定两种类型数据相关的列。数组值是主数据(由您声明关系的 Active Record 类表示)的列,而数组键是相关数据的列。

    记住这一点的一个简单规则是,正如您在上面的示例中看到的,您将属于相关 Active Record 的列直接写入它旁边。您看到 customer_idOrder 的属性,而 idCustomer 的属性。

警告:关系名称 relation 是保留的。当使用它时,它将产生 ArgumentCountError

访问关系数据

声明关系后,您可以通过关系名称访问关系数据。这就像访问关系方法定义的对象 属性 一样。因此,我们称它为关系属性。例如:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
// $orders is an array of Order objects
$orders = $customer->orders;

信息:当您通过 getter 方法 getXyz() 声明一个名为 xyz 的关系时,您将能够像访问 对象属性 一样访问 xyz。请注意,名称区分大小写。

如果一个关系是用 hasMany() 声明的,访问此关系属性将返回相关 Active Record 实例的数组;如果一个关系是用 hasOne() 声明的,访问关系属性将返回相关 Active Record 实例或 null(如果未找到相关数据)。

当您第一次访问关系属性时,将执行一个 SQL 语句,如上面的示例所示。如果再次访问同一个属性,将返回之前的结果,而不会重新执行 SQL 语句。要强制重新执行 SQL 语句,您应该首先取消设置关系属性:unset($customer->orders)

注意:虽然这个概念看起来类似于 对象属性 功能,但存在一个重要的区别。对于普通对象属性,属性值与定义的 getter 方法类型相同。然而,关系方法返回一个 yii\db\ActiveQuery 实例,而访问关系属性将返回 yii\db\ActiveRecord 实例或这些实例的数组。

$customer->orders; // is an array of `Order` objects
$customer->getOrders(); // returns an ActiveQuery instance

这对创建自定义查询很有用,这将在下一节中介绍。

动态关系查询

因为关系方法返回一个 yii\db\ActiveQuery 实例,您可以在执行数据库查询之前使用查询构建方法进一步构建此查询。例如:

$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getOrders()
    ->where(['>', 'subtotal', 200])
    ->orderBy('id')
    ->all();

与访问关系属性不同,每次通过关系方法执行动态关系查询时,都会执行一个 SQL 语句,即使之前执行过相同的动态关系查询。

有时您甚至可能希望参数化关系声明,以便您可以更轻松地执行动态关系查询。例如,您可以像下面这样声明一个 bigOrders 关系:

class Customer extends ActiveRecord
{
    public function getBigOrders($threshold = 100)
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id'])
            ->where('subtotal > :threshold', [':threshold' => $threshold])
            ->orderBy('id');
    }
}

然后,您将能够执行以下关系查询

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getBigOrders(200)->all();

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 100 ORDER BY `id`
$orders = $customer->bigOrders;

通过连接表的关系

在数据库建模中,当两个相关表之间的多重性是多对多时,通常会引入一个 连接表。例如,order 表和 item 表可以通过一个名为 order_item 的连接表相关联。然后一个订单将对应多个订单项,而一个商品项也将对应多个订单项。

在声明此类关系时,您将调用 via()viaTable() 来指定连接表。via()viaTable() 之间的区别在于,前者使用现有关系名称来指定连接表,而后者直接使用连接表。例如:

class Order extends ActiveRecord
{
    public function getItems()
    {
        return $this->hasMany(Item::class, ['id' => 'item_id'])
            ->viaTable('order_item', ['order_id' => 'id']);
    }
}

或者:

class Order extends ActiveRecord
{
    public function getOrderItems()
    {
        return $this->hasMany(OrderItem::class, ['order_id' => 'id']);
    }

    public function getItems()
    {
        return $this->hasMany(Item::class, ['id' => 'item_id'])
            ->via('orderItems');
    }
}

使用带连接表声明的关系与使用普通关系相同。例如:

// SELECT * FROM `order` WHERE `id` = 100
$order = Order::findOne(100);

// SELECT * FROM `order_item` WHERE `order_id` = 100
// SELECT * FROM `item` WHERE `item_id` IN (...)
// returns an array of Item objects
$items = $order->items;

通过多个表链关系定义

还可以通过使用 via() 链关系定义来通过多个表定义关系。考虑到上面的示例,我们有类 CustomerOrderItem。我们可以向 Customer 类添加一个关系,列出他们所有订单中的所有商品,并将其命名为 getPurchasedItems(),关系链在以下代码示例中显示

class Customer extends ActiveRecord
{
    // ...

    public function getPurchasedItems()
    {
        // customer's items, matching 'id' column of `Item` to 'item_id' in OrderItem
        return $this->hasMany(Item::class, ['id' => 'item_id'])
                    ->via('orderItems');
    }

    public function getOrderItems()
    {
        // customer's order items, matching 'id' column of `Order` to 'order_id' in OrderItem
        return $this->hasMany(OrderItem::class, ['order_id' => 'id'])
                    ->via('orders');
    }

    public function getOrders()
    {
        // same as above
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

延迟加载和预加载

访问关系数据 中,我们解释了您可以像访问普通对象属性一样访问 Active Record 实例的关系属性。只有在您第一次访问关系属性时才会执行 SQL 语句。我们将这种关系数据访问方法称为延迟加载。例如:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$orders = $customer->orders;

// no SQL executed
$orders2 = $customer->orders;

延迟加载非常方便使用。但是,当您需要访问多个 ActiveRecord 实例的相同关系属性时,可能会遇到性能问题。考虑以下代码示例,将执行多少个 SQL 语句?

// SELECT * FROM `customer` LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
    // SELECT * FROM `order` WHERE `customer_id` = ...
    $orders = $customer->orders;
}

如您在上面的代码注释中所见,执行了 101 个 SQL 语句!这是因为每次在 for 循环中访问不同 `Customer` 对象的 `orders` 关系属性时,都会执行一个 SQL 语句。

要解决此性能问题,您可以使用以下所示的所谓“预加载”方法,

// SELECT * FROM `customer` LIMIT 100;
// SELECT * FROM `orders` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->with('orders')
    ->limit(100)
    ->all();

foreach ($customers as $customer) {
    // no SQL executed
    $orders = $customer->orders;
}

通过调用 yii\db\ActiveQuery::with(),您可以指示 ActiveRecord 在一个 SQL 语句中将前 100 个客户的订单带回来。因此,您将执行的 SQL 语句数量从 101 个减少到 2 个!

您可以预加载一个或多个关系。您甚至可以预加载“嵌套关系”。嵌套关系是在相关 ActiveRecord 类中声明的关系。例如,`Customer` 通过 `orders` 关系与 `Order` 相关联,而 `Order` 通过 `items` 关系与 `Item` 相关联。当查询 `Customer` 时,您可以使用嵌套关系符号 `orders.items` 预加载 `items`。

以下代码显示了 with() 的不同用法。我们假设 `Customer` 类有两个关系 `orders` 和 `country`,而 `Order` 类有一个关系 `items`。

// eager loading both "orders" and "country"
$customers = Customer::find()->with('orders', 'country')->all();
// equivalent to the array syntax below
$customers = Customer::find()->with(['orders', 'country'])->all();
// no SQL executed 
$orders= $customers[0]->orders;
// no SQL executed 
$country = $customers[0]->country;

// eager loading "orders" and the nested relation "orders.items"
$customers = Customer::find()->with('orders.items')->all();
// access the items of the first order of the first customer
// no SQL executed
$items = $customers[0]->orders[0]->items;

您可以预加载深度嵌套的关系,例如 `a.b.c.d`。所有父关系都将被预加载。也就是说,当您使用 `a.b.c.d` 调用 with() 时,您将预加载 `a`、`a.b`、`a.b.c` 和 `a.b.c.d`。

信息:通常,当预加载 `N` 个关系时,其中 `M` 个关系使用 联接表 定义,将总共执行 `N+M+1` 个 SQL 语句。请注意,嵌套关系 `a.b.c.d` 被计为 4 个关系。

在预加载关系时,您可以使用匿名函数自定义相应的关系查询。例如,

// find customers and bring back together their country and active orders
// SELECT * FROM `customer`
// SELECT * FROM `country` WHERE `id` IN (...)
// SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1
$customers = Customer::find()->with([
    'country',
    'orders' => function ($query) {
        $query->andWhere(['status' => Order::STATUS_ACTIVE]);
    },
])->all();

在自定义关系的关联查询时,您应该将关系名称指定为数组键,并使用匿名函数作为相应的数组值。匿名函数将接收一个 `$query` 参数,该参数表示用于对关系执行关联查询的 yii\db\ActiveQuery 对象。在上面的代码示例中,我们通过追加关于订单状态的附加条件来修改关联查询。

注意:如果您在预加载关系时调用 select(),您必须确保在关系声明中引用的列被选中。否则,相关模型可能无法正确加载。例如,

$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer is always `null`. To fix the problem, you should do the following:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

与关系联接

注意:本小节中描述的内容仅适用于关系数据库,例如 MySQL、PostgreSQL 等。

我们已经描述的关联查询在查询主数据时只引用主表列。实际上,我们经常需要引用相关表中的列。例如,我们可能希望将至少有一个活动订单的客户带回来。为了解决这个问题,我们可以构建一个类似于以下的联接查询

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id`
// WHERE `order`.`status` = 1
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->select('customer.*')
    ->leftJoin('order', '`order`.`customer_id` = `customer`.`id`')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->with('orders')
    ->all();

注意:在构建包含 JOIN SQL 语句的关联查询时,区分列名非常重要。常见的做法是用相应的表名前缀列名。

但是,更好的方法是通过调用 yii\db\ActiveQuery::joinWith() 来利用现有的关系声明

$customers = Customer::find()
    ->joinWith('orders')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->all();

这两种方法都执行相同的 SQL 语句集。尽管如此,后一种方法要干净得多,代码更简洁。

默认情况下,joinWith() 将使用 `LEFT JOIN` 将主表与相关表联接。您可以通过其第三个参数 `$joinType` 指定不同的联接类型(例如 `RIGHT JOIN`)。如果所需的联接类型是 `INNER JOIN`,您可以直接调用 innerJoinWith()

调用 joinWith() 默认情况下会 预加载 相关数据。如果您不想引入相关数据,可以将其第二个参数 `$eagerLoading` 指定为 `false`。

注意:即使使用 joinWith()innerJoinWith() 并启用预加载,相关数据也不会从 `JOIN` 查询的结果中填充。因此,如 预加载 部分所述,每个联接关系仍然有一个额外的查询。

with() 一样,您可以联接一个或多个关系;您可以在运行时自定义关系查询;您可以联接嵌套关系;并且您可以混合使用 with()joinWith()。例如,

$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->andWhere(['>', 'subtotal', 100]);
    },
])->with('country')
    ->all();

有时在联接两个表时,您可能需要在 `JOIN` 查询的 `ON` 部分指定一些额外的条件。这可以通过调用 yii\db\ActiveQuery::onCondition() 方法来实现,如下所示

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` AND `order`.`status` = 1 
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->onCondition(['order.status' => Order::STATUS_ACTIVE]);
    },
])->all();

上面的查询将返回所有客户,并且对于每个客户,它将返回所有活动订单。请注意,这与我们之前的示例不同,之前的示例只返回至少有一个活动订单的客户。

信息:yii\db\ActiveQuery 使用 onCondition() 指定条件时,如果查询涉及 JOIN 查询,条件将放在 `ON` 部分。如果查询不涉及 JOIN,则联接条件将自动附加到查询的 `WHERE` 部分。因此,它可能只包含包括相关表列的条件。

关系表别名

如前所述,在查询中使用 JOIN 时,我们需要区分列名。因此,通常为表定义别名。可以通过以下方式自定义关系查询来设置关系查询的别名

$query->joinWith([
    'orders' => function ($q) {
        $q->from(['o' => Order::tableName()]);
    },
])

然而,这看起来很复杂,并且涉及硬编码相关对象的表名或调用 `Order::tableName()`。从 2.0.7 版本开始,Yii 为此提供了一个快捷方式。您现在可以像以下示例一样定义和使用关系表的别名

// join the orders relation and sort the result by orders.id
$query->joinWith(['orders o'])->orderBy('o.id');

上面的语法适用于简单关系。如果您需要在联接嵌套关系时为中间表设置别名,例如 `$query->joinWith(['orders.product'])`,您需要像以下示例一样嵌套 joinWith 调用

$query->joinWith(['orders o' => function($q) {
        $q->joinWith('product p');
    }])
    ->where('o.amount > 100');

逆关系

关系声明在两个 ActiveRecord 类之间通常是相互的。例如,`Customer` 通过 `orders` 关系与 `Order` 相关联,而 `Order` 通过 `customer` 关系与 `Customer` 相关联。

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    public function getCustomer()
    {
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

现在考虑以下代码片段

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// SELECT * FROM `customer` WHERE `id` = 123
$customer2 = $order->customer;

// displays "not the same"
echo $customer2 === $customer ? 'same' : 'not the same';

我们会认为 `$customer` 和 `$customer2` 是相同的,但它们不是!实际上,它们包含相同的客户数据,但它们是不同的对象。当访问 `$order->customer` 时,将执行一个额外的 SQL 语句来填充一个新的对象 `$customer2`。

为了避免在上面的示例中冗余执行最后一个 SQL 语句,我们应该告诉 Yii `customer` 是 `orders` 的“逆关系”,方法是调用 inverseOf() 方法,如下所示

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer');
    }
}

使用此修改后的关系声明,我们将有

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// No SQL will be executed
$customer2 = $order->customer;

// displays "same"
echo $customer2 === $customer ? 'same' : 'not the same';

注意:不能为涉及 联接表 的关系定义逆关系。也就是说,如果一个关系使用 via()viaTable() 定义,则不应该再调用 inverseOf()

保存关系

在处理关系数据时,您经常需要在不同数据之间建立关系或销毁现有关系。这需要为定义关系的列设置正确的值。使用 ActiveRecord,您最终可能会编写如下代码

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

// setting the attribute that defines the "customer" relation in Order
$order->customer_id = $customer->id;
$order->save();

ActiveRecord 提供了 link() 方法,允许您更优雅地完成此任务

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

$order->link('customer', $customer);

link() 方法要求您指定关系名称和应建立关系的目标 ActiveRecord 实例。该方法将修改链接两个 ActiveRecord 实例的属性的值,并将它们保存到数据库中。在上面的示例中,它将设置 `Order` 实例的 `customer_id` 属性为 `Customer` 实例的 `id` 属性的值,然后将其保存到数据库中。

注意:您无法链接两个新创建的 ActiveRecord 实例。

当一个关系通过 联接表 定义时,使用 link() 的好处更加明显。例如,您可以使用以下代码将一个 `Order` 实例与一个 `Item` 实例链接

$order->link('items', $item);

上面的代码将自动在 `order_item` 联接表中插入一行以将订单与商品相关联。

信息: link() 方法在保存受影响的 ActiveRecord 实例时不会执行任何数据验证。在调用此方法之前,您有责任验证任何输入数据。

link() 的相反操作是 unlink(),它会破坏两个 ActiveRecord 实例之间的现有关系。例如,

$customer = Customer::find()->with('orders')->where(['id' => 123])->one();
$customer->unlink('orders', $customer->orders[0]);

默认情况下, unlink() 方法将设置指定现有关系的外键值(如果存在)为 `null`。但是,您可以选择通过将 `$delete` 参数传递给方法作为 `true` 来删除包含外键值的表行。

当一个关系涉及联接表时,调用 unlink() 将导致联接表中的外键被清除,或者如果 `$delete` 为 `true`,则删除联接表中相应的行。

跨数据库关系

ActiveRecord 允许您声明由不同数据库支持的 ActiveRecord 类之间的关系。这些数据库可以是不同类型的(例如 MySQL 和 PostgreSQL,或 MS SQL 和 MongoDB),并且可以运行在不同的服务器上。您可以使用相同的语法执行关系查询。例如,

// Customer is associated with the "customer" table in a relational database (e.g. MySQL)
class Customer extends \yii\db\ActiveRecord
{
    public static function tableName()
    {
        return 'customer';
    }

    public function getComments()
    {
        // a customer has many comments
        return $this->hasMany(Comment::class, ['customer_id' => 'id']);
    }
}

// Comment is associated with the "comment" collection in a MongoDB database
class Comment extends \yii\mongodb\ActiveRecord
{
    public static function collectionName()
    {
        return 'comment';
    }

    public function getCustomer()
    {
        // a comment has one customer
        return $this->hasOne(Customer::class, ['id' => 'customer_id']);
    }
}

$customers = Customer::find()->with('comments')->all();

您可以使用本节中描述的大多数关系查询功能。

注意: joinWith() 的使用仅限于允许跨数据库 JOIN 查询的数据库。因此,您无法在上面的示例中使用此方法,因为 MongoDB 不支持 JOIN。

自定义查询类

默认情况下,所有 ActiveRecord 查询都由 yii\db\ActiveQuery 支持。要在 ActiveRecord 类中使用自定义查询类,您应该覆盖 yii\db\ActiveRecord::find() 方法并返回自定义查询类的实例。例如,

// file Comment.php
namespace app\models;

use yii\db\ActiveRecord;

class Comment extends ActiveRecord
{
    public static function find()
    {
        return new CommentQuery(get_called_class());
    }
}

现在,无论何时执行查询(例如 find()findOne())或定义关系(例如 hasOne())与 Comment,您都将调用 CommentQuery 的实例而不是 ActiveQuery

您现在必须定义 CommentQuery 类,它可以以多种创造性的方式进行自定义,以改善您的查询构建体验。例如,

// file CommentQuery.php
namespace app\models;

use yii\db\ActiveQuery;

class CommentQuery extends ActiveQuery
{
    // conditions appended by default (can be skipped)
    public function init()
    {
        $this->andOnCondition(['deleted' => false]);
        parent::init();
    }

    // ... add customized query methods here ...

    public function active($state = true)
    {
        return $this->andOnCondition(['active' => $state]);
    }
}

注意: 而不是调用 onCondition(),您通常应该调用 andOnCondition()orOnCondition() 来在定义新的查询构建方法时追加附加条件,这样就不会覆盖任何现有条件。

这允许您编写如下查询构建代码

$comments = Comment::find()->active()->all();
$inactiveComments = Comment::find()->active(false)->all();

提示: 在大型项目中,建议您使用自定义查询类来保存大多数与查询相关的代码,以便保持 Active Record 类简洁。

您还可以在定义 Comment 的关系或执行关系查询时使用新的查询构建方法

class Customer extends \yii\db\ActiveRecord
{
    public function getActiveComments()
    {
        return $this->hasMany(Comment::class, ['customer_id' => 'id'])->active();
    }
}

$customers = Customer::find()->joinWith('activeComments')->all();

// or alternatively
class Customer extends \yii\db\ActiveRecord
{
    public function getComments()
    {
        return $this->hasMany(Comment::class, ['customer_id' => 'id']);
    }
}

$customers = Customer::find()->joinWith([
    'comments' => function($q) {
        $q->active();
    }
])->all();

信息: 在 Yii 1.1 中,存在一个名为 scope 的概念。Scope 在 Yii 2.0 中不再直接支持,您应该使用自定义查询类和查询方法来实现相同目标。

选择额外字段

当 Active Record 实例从查询结果中填充时,其属性将由接收到的数据集中的相应列值填充。

您能够从查询中获取额外的列或值并将其存储在 Active Record 中。例如,假设我们有一个名为 room 的表,其中包含有关酒店中可用房间的信息。每个房间使用字段 lengthwidthheight 存储有关其几何尺寸的信息。想象一下,我们需要检索所有可用房间及其体积的列表,按体积降序排列。因此,您不能使用 PHP 计算体积,因为我们需要按其值对记录进行排序,但您也希望在列表中显示 volume。为了实现目标,您需要在 Room Active Record 类中声明一个额外字段,它将存储 volume

class Room extends \yii\db\ActiveRecord
{
    public $volume;

    // ...
}

然后,您需要编写一个查询,该查询计算房间的体积并执行排序

$rooms = Room::find()
    ->select([
        '{{room}}.*', // select all columns
        '([[length]] * [[width]] * [[height]]) AS volume', // calculate a volume
    ])
    ->orderBy('volume DESC') // apply sort
    ->all();

foreach ($rooms as $room) {
    echo $room->volume; // contains value calculated by SQL
}

能够选择额外字段对于聚合查询特别有用。假设您需要显示一个客户列表,其中包含他们下达的订单数量。首先,您需要声明一个 Customer 类,该类具有 orders 关系和用于存储计数的额外字段

class Customer extends \yii\db\ActiveRecord
{
    public $ordersCount;

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

然后,您可以编写一个查询,该查询连接订单并计算其数量

$customers = Customer::find()
    ->select([
        '{{customer}}.*', // select all customer fields
        'COUNT({{order}}.id) AS ordersCount' // calculate orders count
    ])
    ->joinWith('orders') // ensure table junction
    ->groupBy('{{customer}}.id') // group the result to ensure aggregation function works
    ->all();

使用此方法的缺点是,如果信息未在 SQL 查询中加载,则必须单独计算它。因此,如果您通过常规查询找到了特定记录而没有额外的选择语句,它将无法返回额外字段的实际值。对于新保存的记录也会发生同样的情况。

$room = new Room();
$room->length = 100;
$room->width = 50;
$room->height = 2;

$room->volume; // this value will be `null`, since it was not declared yet

使用 __get()__set() 魔术方法,我们可以模拟属性的行为

class Room extends \yii\db\ActiveRecord
{
    private $_volume;
    
    public function setVolume($volume)
    {
        $this->_volume = (float) $volume;
    }
    
    public function getVolume()
    {
        if (empty($this->length) || empty($this->width) || empty($this->height)) {
            return null;
        }
        
        if ($this->_volume === null) {
            $this->setVolume(
                $this->length * $this->width * $this->height
            );
        }
        
        return $this->_volume;
    }

    // ...
}

当 select 查询未提供体积时,模型将能够使用模型的属性自动计算它。

您还可以使用定义的关系来计算聚合字段

class Customer extends \yii\db\ActiveRecord
{
    private $_ordersCount;

    public function setOrdersCount($count)
    {
        $this->_ordersCount = (int) $count;
    }

    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // this avoid calling a query searching for null primary keys
        }

        if ($this->_ordersCount === null) {
            $this->setOrdersCount($this->getOrders()->count()); // calculate aggregation on demand from relation
        }

        return $this->_ordersCount;
    }

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }
}

使用此代码,如果 'ordersCount' 存在于 'select' 语句中,Customer::ordersCount 将由查询结果填充,否则将根据需要使用 Customer::orders 关系计算它。

这种方法也可以用于创建某些关系数据的快捷方式,尤其是聚合。例如

class Customer extends \yii\db\ActiveRecord
{
    /**
     * Defines read-only virtual property for aggregation data.
     */
    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // this avoid calling a query searching for null primary keys
        }
        
        return empty($this->ordersAggregation) ? 0 : $this->ordersAggregation[0]['counted'];
    }

    /**
     * Declares normal 'orders' relation.
     */
    public function getOrders()
    {
        return $this->hasMany(Order::class, ['customer_id' => 'id']);
    }

    /**
     * Declares new relation based on 'orders', which provides aggregation.
     */
    public function getOrdersAggregation()
    {
        return $this->getOrders()
            ->select(['customer_id', 'counted' => 'count(*)'])
            ->groupBy('customer_id')
            ->asArray(true);
    }

    // ...
}

foreach (Customer::find()->with('ordersAggregation')->all() as $customer) {
    echo $customer->ordersCount; // outputs aggregation data from relation without extra query due to eager loading
}

$customer = Customer::findOne($pk);
$customer->ordersCount; // output aggregation data from lazy loaded relation

发现错别字或您认为此页面需要改进?
在 github 上编辑它 !