活动记录(Active Record)

    例如,假定 Active Record 类关联着 customer 表,
    且该类的 name 属性代表 customer 表的 name 列。
    你可以写以下代码来哉 customer 表里插入一行新的记录:

    对于 MySql,上面的代码和使用下面的原生 SQL 语句是等效的,但显然前者更直观,
    更不易出错,并且面对不同的数据库系统(DBMS, Database Management System)时更不容易产生兼容性问题。

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

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

    • MySQL 4.1 及以上:通过 [[yii\db\ActiveRecord]] 支持
    • PostgreSQL 7.3 及以上:通过 [[yii\db\ActiveRecord]] 支持
    • SQLite 2 and 3:通过 [[yii\db\ActiveRecord]] 支持
    • Microsoft SQL Server 2008 及以上:通过 [[yii\db\ActiveRecord]] 支持
    • Oracle:通过 [[yii\db\ActiveRecord]] 支持
    • CUBRID 9.3 及以上:通过 [[yii\db\ActiveRecord]] 支持 (提示, 由于 CUBRID PDO 扩展的 bug
      给变量加引用将不起作用,所以你得使用 CUBRID 9.3 客户端及服务端。
    • Sphinx:通过 [[yii\sphinx\ActiveRecord]] 支持, 依赖 yii2-sphinx 扩展
    • ElasticSearch:通过 [[yii\elasticsearch\ActiveRecord]] 支持, 依赖 yii2-elasticsearch 扩展

    此外,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 类关联各自的数据库表。
    经过 [[yii\helpers\Inflector::camel2id()]] 处理,[[yii\db\ActiveRecord::tableName()|tableName()]] 方法默认返回的表名称是通过类名转换来得。
    如果这个默认名称不正确,你得重写这个方法。

    此外,[[yii\db\Connection::$tablePrefix|tablePrefix]] 表前缀也会起作用。例如,如果
    [[yii\db\Connection::$tablePrefix|tablePrefix]] 表前缀是 tbl_Customer 的类名将转换成 tbl_customer 表名,OrderItem 转换成 tbl_order_item

    如果你定义的表名是 {{%TableName}}, 百分比字符 % 会被替换成表前缀。
    例如, {{%post}} 会变成 {{tbl_post}}。表名两边的括号会被 处理。

    下面的例子中,我们给 customer 数据库表定义叫 Customer 的 Active Record 类。

    1. namespace app\models;
    2. use yii\db\ActiveRecord;
    3. class Customer extends ActiveRecord
    4. {
    5. const STATUS_INACTIVE = 0;
    6. const STATUS_ACTIVE = 1;
    7. /**
    8. * @return string Active Record 类关联的数据库表名称
    9. */
    10. public static function tableName()
    11. {
    12. return '{{customer}}';
    13. }
    14. }

    将 Active Record 称为模型(Active records are called “models”)

    Active Record 实例称为。因此, 我们通常将 Active Record 类
    放在 app\models 命名空间下(或者其他保存模型的命名空间)。

    因为 [[yii\db\ActiveRecord]] 继承了模型 [[yii\base\Model]], 它就拥有所有模型特性,
    比如说属性(attributes),验证规则(rules),数据序列化(data serialization),等等。

    建立数据库连接(Connecting to Databases)

    活动记录 Active Record 默认使用 db 组件
    作为连接器 [[yii\db\Connection|DB connection]] 访问和操作数据库数据。
    基于中的解释,你可以在系统配置中
    这样配置 db 组件。

    1. return [
    2. 'components' => [
    3. 'db' => [
    4. 'class' => 'yii\db\Connection',
    5. 'dsn' => 'mysql:host=localhost;dbname=testdb',
    6. 'username' => 'demo',
    7. 'password' => 'demo',
    8. ],
    9. ],
    10. ];

    如果你要用不同的数据库连接,而不仅仅是 db 组件,
    你可以重写 [[yii\db\ActiveRecord::getDb()|getDb()]] 方法。

    1. class Customer extends ActiveRecord
    2. {
    3. // ...
    4. public static function getDb()
    5. {
    6. // 使用 "db2" 组件
    7. return \Yii::$app->db2;
    8. }
    9. }

    " class="reference-link">查询数据(Querying Data)

    定义 Active Record 类后,你可以从相应的数据库表中查询数据。
    查询过程大致如下三个步骤:

    1. 通过 [[yii\db\ActiveRecord::find()]] 方法创建一个新的查询生成器对象;
    2. 使用来构建你的查询;
    3. 调用查询生成器的查询方法来取出数据到 Active Record 实例中。

    正如你看到的,是不是跟的步骤差不多。
    唯一有区别的地方在于你用 [[yii\db\ActiveRecord::find()]] 去获得一个新的查询生成器对象,这个对象是 [[yii\db\ActiveQuery]],
    而不是使用 new 操作符创建一个查询生成器对象。

    下面是一些例子,介绍如何使用 Active Query 查询数据:

    1. // 返回 ID 为 123 的客户:
    2. // SELECT * FROM `customer` WHERE `id` = 123
    3. $customer = Customer::find()
    4. ->where(['id' => 123])
    5. ->one();
    6. // 取回所有活跃客户并以他们的 ID 排序:
    7. // SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
    8. $customers = Customer::find()
    9. ->where(['status' => Customer::STATUS_ACTIVE])
    10. ->orderBy('id')
    11. ->all();
    12. // 取回活跃客户的数量:
    13. // SELECT COUNT(*) FROM `customer` WHERE `status` = 1
    14. $count = Customer::find()
    15. ->where(['status' => Customer::STATUS_ACTIVE])
    16. ->count();
    17. // 以客户 ID 索引结果集:
    18. // SELECT * FROM `customer`
    19. $customers = Customer::find()
    20. ->indexBy('id')
    21. ->all();

    上述代码中,$customer 是个 Customer 对象,而 $customers 是个以 Customer 对象为元素的数组。
    它们两都是以 customer 表中取回的数据结果集填充的。

    根据主键获取数据行是比较常见的操作,所以 Yii
    提供了两个快捷方法:

    • [[yii\db\ActiveRecord::findOne()]]:返回一个 Active Record 实例,填充于查询结果的第一行数据。
    • [[yii\db\ActiveRecord::findAll()]]:返回一个 Active Record 实例的数据,填充于查询结果的全部数据。

    这两个方法的传参格式如下:

    • 标量值:这个值会当作主键去查询。
      Yii 会通过读取数据库模式信息来识别主键列。
    • 标量值的数组:这数组里的值都当作要查询的主键的值。
    • 关联数组:键值是表的列名,元素值是相应的要查询的条件值。
      可以到 哈希格式 查看更多信息。

    如下代码描述如何使用这些方法:

    1. // 返回 id 为 123 的客户
    2. // SELECT * FROM `customer` WHERE `id` = 123
    3. $customer = Customer::findOne(123);
    4. // 返回 id 是 100, 101, 123, 124 的客户
    5. // SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
    6. $customers = Customer::findAll([100, 101, 123, 124]);
    7. // 返回 id 是 123 的活跃客户
    8. // SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
    9. $customer = Customer::findOne([
    10. 'id' => 123,
    11. 'status' => Customer::STATUS_ACTIVE,
    12. ]);
    13. // 返回所有不活跃的客户
    14. // SELECT * FROM `customer` WHERE `status` = 0
    15. $customers = Customer::findAll([
    16. 'status' => Customer::STATUS_INACTIVE,
    17. ]);

    注:如果你需要将用户输入传递给这些方法,请确保输入值是标量或者是
    数组条件,确保数组结构不能被外部所改变:

    1. // yii\web\Controller 确保了 $id 是标量
    2. public function actionView($id)
    3. {
    4. $model = Post::findOne($id);
    5. // ...
    6. }
    7. // 明确了指定要搜索的列,在此处传递标量或数组将始终只是查找出单个记录而已
    8. $model = Post::findOne(['id' => Yii::$app->request->get('id')]);
    9. // 不要使用下面的代码!可以注入一个数组条件来匹配任意列的值!
    10. $model = Post::findOne(Yii::$app->request->get('id'));

    Tip: [[yii\db\ActiveRecord::findOne()]] 和 [[yii\db\ActiveQuery::one()]] 都不会添加 LIMIT 1
    生成的 SQL 语句中。如果你的查询会返回很多行的数据,
    你明确的应该加上 limit(1) 来提高性能,比如 Customer::find()->limit(1)->one()

    除了使用查询生成器的方法之外,你还可以书写原生的 SQL 语句来查询数据,并填充结果集到 Active Record 对象中。
    通过使用 [[yii\db\ActiveRecord::findBySql()]] 方法:

    1. // 返回所有不活跃的客户
    2. $sql = 'SELECT * FROM customer WHERE status=:status';
    3. $customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

    不要在 [[yii\db\ActiveRecord::findBySql()|findBySql()]] 方法后加其他查询方法了,
    多余的查询方法都会被忽略。

    访问数据(Accessing Data)

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

    1. // "id" 和 "email" 是 "customer" 表中的列名
    2. $customer = Customer::findOne(123);
    3. $id = $customer->id;
    4. $email = $customer->email;

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

    由于 Active Record 的属性以表的列名命名,可能你会发现你正在编写像这样的 PHP 代码:
    $ customer-> first_name,如果你的表的列名是使用下划线分隔的,那么属性名中的单词
    以这种方式命名。 如果您担心代码风格一致性的问题,那么你应当重命名相应的表列名
    (例如使用骆驼拼写法)。

    " class="reference-link">数据转换(Data Transformation)

    常常遇到,要输入或显示的数据是一种格式,而要将其存储在数据库中是另一种格式。
    例如,在数据库中,您将客户的生日存储为 UNIX 时间戳(虽然这不是一个很好的设计),
    而在大多数情况下,你想以字符串 'YYYY/MM/DD' 的格式处理生日数据。
    为了实现这一目标,您可以在 Customer 中定义 数据转换 方法
    定义 Active Record 类如下:

    1. class Customer extends ActiveRecord
    2. {
    3. // ...
    4. public function getBirthdayText()
    5. {
    6. return date('Y/m/d', $this->birthday);
    7. }
    8. public function setBirthdayText($value)
    9. {
    10. $this->birthday = strtotime($value);
    11. }
    12. }

    现在你的 PHP 代码中,你可以访问 $customer->birthdayText
    来以 'YYYY/MM/DD' 的格式输入和显示客户生日,而不是访问 $customer->birthday

    Tip: 上述示例显示了以不同格式转换数据的通用方法。如果你正在使用
    日期值,您可以使用 和 [[yii\jui\DatePicker|DatePicker]] 来操作,
    这将更易用,更强大。

    " class="reference-link">以数组形式获取数据(Retrieving Data in Arrays)

    通过 Active Record 对象获取数据十分方便灵活,与此同时,当你需要返回大量的数据的时候,
    这样的做法并不令人满意,因为这将导致大量内存占用。在这种情况下,您可以
    在查询方法前调用 [[yii\db\ActiveQuery::asArray()|asArray()]] 方法,来获取 PHP 数组形式的结果:

    1. // 返回所有客户
    2. // 每个客户返回一个关联数组
    3. $customers = Customer::find()
    4. ->asArray()
    5. ->all();

    Tip: 虽然这种方法可以节省内存并提高性能,但它更靠近较低的 DB 抽象层
    你将失去大部分的 Active Record 提供的功能。 一个非常重要的区别在于列值的数据类型。
    当您在 Active Record 实例中返回数据时,列值将根据实际列类型,自动类型转换;
    然而,当您以数组返回数据时,列值将为
    字符串(因为它们是没有处理过的 PDO 的结果),不管它们的实际列是什么类型。

    批量获取数据(Retrieving Data in Batches)

    查询生成器 中,我们已经解释说可以使用 批处理查询 来最小化你的内存使用,
    每当从数据库查询大量数据。你可以在 Active Record 中使用同样的技巧。例如,

    1. // 每次获取 10 条客户数据
    2. foreach (Customer::find()->batch(10) as $customers) {
    3. // $customers 是个最多拥有 10 条数据的数组
    4. }
    5. // 每次获取 10 条客户数据,然后一条一条迭代它们
    6. foreach (Customer::find()->each(10) as $customer) {
    7. // $customer 是个 `Customer` 对象
    8. }
    9. // 贪婪加载模式的批处理查询
    10. foreach (Customer::find()->with('orders')->each() as $customer) {
    11. // $customer 是个 `Customer` 对象,并附带关联的 `'orders'`
    12. }

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

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

    例如,

    1. // 插入新记录
    2. $customer = new Customer();
    3. $customer->name = 'James';
    4. $customer->email = 'james@example.com';
    5. $customer->save();
    6. // 更新已存在的记录
    7. $customer = Customer::findOne(123);
    8. $customer->email = 'james@newexample.com';
    9. $customer->save();

    [[yii\db\ActiveRecord::save()|save()]] 方法可能插入或者更新表的记录,这取决于 Active Record 实例的状态。
    如果实例通过 new 操作符实例化,调用 [[yii\db\ActiveRecord::save()|save()]] 方法将插入新记录;
    如果实例是一个查询方法的结果,调用 [[yii\db\ActiveRecord::save()|save()]] 方法
    将更新这个实例对应的表记录行。

    你可以通过检查 Active Record 实例的 [[yii\db\ActiveRecord::isNewRecord|isNewRecord]] 属性值来区分这两个状态。
    此属性也被使用在 [[yii\db\ActiveRecord::save()|save()]] 方法内部,
    代码如下:

    1. public function save($runValidation = true, $attributeNames = null)
    2. {
    3. if ($this->getIsNewRecord()) {
    4. return $this->insert($runValidation, $attributeNames);
    5. } else {
    6. return $this->update($runValidation, $attributeNames) !== false;
    7. }
    8. }

    Tip: 你可以直接调用 [[yii\db\ActiveRecord::insert()|insert()]] 或者 [[yii\db\ActiveRecord::update()|update()]]
    方法来插入或更新一条记录。

    数据验证(Data Validation)

    因为 [[yii\db\ActiveRecord]] 继承于 [[yii\base\Model]],它共享相同的 输入验证 功能。
    你可以通过重写 [[yii\db\ActiveRecord::rules()|rules()]] 方法声明验证规则并执行,
    通过调用 [[yii\db\ActiveRecord::validate()|validate()]] 方法进行数据验证。

    当你调用 [[yii\db\ActiveRecord::save()|save()]] 时,默认情况下会自动调用 [[yii\db\ActiveRecord::validate()|validate()]]。
    只有当验证通过时,它才会真正地保存数据; 否则将简单地返回false
    您可以检查 [[yii\db\ActiveRecord::errors|errors]] 属性来获取验证过程的错误消息。

    Tip: 如果你确定你的数据不需要验证(比如说数据来自可信的场景),
    你可以调用 save(false) 来跳过验证过程。

    块赋值(Massive Assignment)

    和普通的 models 一样,你亦可以享受 Active Record 实例的 特性。
    使用此功能,您可以在单个 PHP 语句中,给 Active Record 实例的多个属性批量赋值,
    如下所示。 记住,只有 安全属性 才可以批量赋值。

    1. $values = [
    2. 'name' => 'James',
    3. 'email' => 'james@example.com',
    4. ];
    5. $customer = new Customer();
    6. $customer->attributes = $values;
    7. $customer->save();

    更新计数(Updating Counters)

    在数据库表中增加或减少一个字段的值是个常见的任务。我们将这些列称为“计数列”。
    您可以使用 [[yii\db\ActiveRecord::updateCounters()|updateCounters()]] 更新一个或多个计数列。
    例如,

    1. $post = Post::findOne(100);
    2. // UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100
    3. $post->updateCounters(['view_count' => 1]);

    Note: 如果你使用 [[yii\db\ActiveRecord::save()]] 更新一个计数列,你最终将得到错误的结果,
    因为可能发生这种情况,多个请求间并发读写同一个计数列。

    当您调用 [[yii\db\ActiveRecord::save()|save()]] 保存 Active Record 实例时,只有 脏属性
    被保存。如果一个属性的值已被修改,则会被认为是 ,因为它是从 DB 加载出来的或者
    刚刚保存到 DB 。请注意,无论如何 Active Record 都会执行数据验证
    不管有没有脏属性。

    Active Record 自动维护脏属性列表。 它保存所有属性的旧值,
    并其与最新的属性值进行比较,就是酱紫个道理。你可以调用 [[yii\db\ActiveRecord::getDirtyAttributes()]]
    获取当前的脏属性。你也可以调用 [[yii\db\ActiveRecord::getDirtyAttributes()]]
    将属性显式标记为脏。

    如果你有需要获取属性原先的值,你可以调用
    [[yii\db\ActiveRecord::getOldAttributes()|getOldAttributes()]] 或者 [[yii\db\ActiveRecord::getOldAttribute()|getOldAttribute()]]。

    注:属性新旧值的比较是用 === 操作符,所以一样的值但类型不同,
    依然被认为是脏的。当模型从 HTML 表单接收用户输入时,通常会出现这种情况,
    其中每个值都表示为一个字符串类型。
    为了确保正确的类型,比如,整型需要用过滤验证器
    ['attributeName', 'filter', 'filter' => 'intval']。其他 PHP 类型转换函数一样适用,像
    floatval()
    ,等等

    " class="reference-link">默认属性值(Default Attribute Values)

    某些表列可能在数据库中定义了默认值。有时,你可能想预先填充
    具有这些默认值的 Active Record 实例的 Web 表单。 为了避免再次写入相同的默认值,
    您可以调用 [[yii\db\ActiveRecord::loadDefaultValues()|loadDefaultValues()]] 来填充 DB 定义的默认值
    进入相应的 Active Record 属性:

    1. $customer = new Customer();
    2. $customer->loadDefaultValues();
    3. // $customer->xyz 将被 “zyz” 列定义的默认值赋值

    属性类型转换(Attributes Typecasting)

    在查询结果填充 [[yii\db\ActiveRecord]] 时,将自动对其属性值执行类型转换,基于
    数据库表模式 中的信息。 这允许从数据表中获取数据,
    声明为整型的,使用 PHP 整型填充 ActiveRecord 实例,布尔值(boolean)的也用布尔值填充,等等。
    但是,类型转换机制有几个限制:

    • 浮点值不被转换,并且将被表示为字符串,否则它们可能会使精度降低。
    • 整型值的转换取决于您使用的操作系统的整数容量。尤其是:
      声明为“无符号整型”或“大整型”的列的值将仅转换为 64 位操作系统的 PHP 整型,
      而在 32 位操作系统中 - 它们将被表示为字符串。

    值得注意的是,只有在从查询结果填充 ActiveRecord 实例时才执行属性类型转换。
    而从 HTTP 请求加载的值或直接通过属性访问赋值的,没有自动转换。
    在准备用于在 ActiveRecord 保存时,准备 SQL 语句还使用了表模式,以确保查询时
    值绑定到具有正确类型的。但是,ActiveRecord 实例的属性值不会
    在保存过程中转换。

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

    MySQL 和 PostgreSQL 中的 JSON(JSON in MySQL and PostgreSQL)

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

    PostgreSQL 中的数组(Arrays in PostgreSQL)

    数据填充后,来自 Array 列的值将自动从 PgSQL 的编码值解码为 一个 [[yii\db\ArrayExpression|ArrayExpression]]
    对象。它继承于 PHP 的 ArrayAccess 接口,所以你可以把它当作一个数组用,或者调用 ->getValue() 来获取数组本身。

    另一方面,为了将属性值保存到数组列,ActiveRecord 会自动创建一个 [[yii\db\ArrayExpression|ArrayExpression]] 对象,
    这对象将在 中被编码成数组的 PgSQL 字符串表达式。

    你还可以这样使用 JSON 列的条件:

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

    要详细了解表达式构建系统,可以访问 Query Builder – 增加自定义条件和语句
    文章。

    更新多个数据行(Updating Multiple Rows)

    上述方法都可以用于单个 Active Record 实例,以插入或更新单条
    表数据行。 要同时更新多个数据行,你应该调用 [[yii\db\ActiveRecord::updateAll()|updateAll()]]
    这是一个静态方法。

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

    同样, 你可以调用 [[yii\db\ActiveRecord::updateAllCounters()|updateAllCounters()]] 同时更新多条记录的计数列。

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

    " class="reference-link">删除数据(Deleting Data)

    要删除单行数据,首先获取与该行对应的 Active Record 实例,然后调用
    [[yii\db\ActiveRecord::delete()]] 方法。

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

    你可以调用 [[yii\db\ActiveRecord::deleteAll()]] 方法删除多行甚至全部的数据。例如,

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

    Tip: 调用 [[yii\db\ActiveRecord::deleteAll()|deleteAll()]] 时要非常小心,因为如果在指定条件时出错,
    它可能会完全擦除表中的所有数据。

    Active Record 的生命周期(Active Record Life Cycles)

    当你实现各种功能的时候,会发现了解 Active Record 的生命周期很重要。
    在每个生命周期中,一系列的方法将被调用执行,您可以重写这些方法
    以定制你要的生命周期。您还可以响应触发某些 Active Record 事件
    以便在生命周期中注入您的自定义代码。这些事件在开发 Active Record 的 行为时特别有用,
    通过行为可以定制 Active Record 生命周期的 。

    下面,我们将总结各种 Active Record 的生命周期,以及生命周期中
    所涉及的各种方法、事件。

    实例化生命周期(New Instance Life Cycle)

    当通过 new 操作符新建一个 Active Record 实例时,会发生以下生命周期:

    1. 类的构造函数调用.
    2. [[yii\db\ActiveRecord::init()|init()]]:触发 [[yii\db\ActiveRecord::EVENT_INIT|EVENT_INIT]] 事件。

    " class="reference-link">查询数据生命周期(Querying Data Life Cycle)

    当通过 查询数据时,每个新填充出来的 Active Record 实例
    将发生下面的生命周期:

    1. 类的构造函数调用。
    2. [[yii\db\ActiveRecord::init()|init()]]:触发 [[yii\db\ActiveRecord::EVENT_INIT|EVENT_INIT]] 事件。
    3. [[yii\db\ActiveRecord::afterFind()|afterFind()]]:触发 [[yii\db\ActiveRecord::EVENT_AFTER_FIND|EVENT_AFTER_FIND]] 事件。

    " class="reference-link">保存数据生命周期(Saving Data Life Cycle)

    当通过 [[yii\db\ActiveRecord::save()|save()]] 插入或更新 Active Record 实例时
    会发生以下生命周期:

    1. 执行数据验证。如果数据验证失败,步骤 3 之后的步骤将被跳过。
    2. [[yii\db\ActiveRecord::afterValidate()|afterValidate()]]:触发
      [[yii\db\ActiveRecord::EVENT_AFTER_VALIDATE|EVENT_AFTER_VALIDATE]] 事件。
    3. [[yii\db\ActiveRecord::beforeSave()|beforeSave()]]:触发
      [[yii\db\ActiveRecord::EVENT_BEFORE_INSERT|EVENT_BEFORE_INSERT]]
      或者 [[yii\db\ActiveRecord::EVENT_BEFORE_UPDATE|EVENT_BEFORE_UPDATE]] 事件。 如果这方法返回 false
      或者 [[yii\base\ModelEvent::isValid]] 值为 false,接下来的步骤都会被跳过。
    4. 执行真正的数据插入或者更新。
    5. [[yii\db\ActiveRecord::afterSave()|afterSave()]]:触发
      [[yii\db\ActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]]
      或者 [[yii\db\ActiveRecord::EVENT_AFTER_UPDATE|EVENT_AFTER_UPDATE]] 事件。

    删除数据生命周期(Deleting Data Life Cycle)

    当通过 [[yii\db\ActiveRecord::delete()|delete()]] 删除 Active Record 实例时,
    会发生以下生命周期:

    1. [[yii\db\ActiveRecord::beforeDelete()|beforeDelete()]]:触发
      [[yii\db\ActiveRecord::EVENT_BEFORE_DELETE|EVENT_BEFORE_DELETE]] 事件。 如果这方法返回 false
      或者 [[yii\base\ModelEvent::isValid]] 值为 false,接下来的步骤都会被跳过。
    2. 执行真正的数据删除。
    3. [[yii\db\ActiveRecord::afterDelete()|afterDelete()]]:触发
      [[yii\db\ActiveRecord::EVENT_AFTER_DELETE|EVENT_AFTER_DELETE]] 事件。

    Tip: 调用以下方法则不会启动上述的任何生命周期,
    因为这些方法直接操作数据库,而不是基于 Active Record 模型:

    • [[yii\db\ActiveRecord::updateAll()]]
    • [[yii\db\ActiveRecord::deleteAll()]]
    • [[yii\db\ActiveRecord::updateCounters()]]
    • [[yii\db\ActiveRecord::updateAllCounters()]]

    当通过 [[yii\db\ActiveRecord::refresh()|refresh()]] 刷新 Active Record 实例时,
    如刷新成功方法返回 true,那么 [[yii\db\ActiveRecord::EVENT_AFTER_REFRESH|EVENT_AFTER_REFRESH]] 事件将被触发。

    " class="reference-link">事务操作(Working with Transactions)

    Active Record 有两种方式来使用。

    第一种方法是在事务块中显式地包含 Active Record 的各个方法调用,如下所示,

    Tip: 在上面的代码中,我们有两个catch块用于兼容
    PHP 5.x 和 PHP 7.x。 \Exception 继承于 \Throwable interface
    由于 PHP 7.0 的改动,如果您的应用程序仅使用 PHP 7.0 及更高版本,您可以跳过 \Exception 部分。

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

    1. class Customer extends ActiveRecord
    2. {
    3. public function transactions()
    4. {
    5. return [
    6. 'admin' => self::OP_INSERT,
    7. 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
    8. // 上面等价于:
    9. // 'api' => self::OP_ALL,
    10. ];
    11. }
    12. }

    [[yii\db\ActiveRecord::transactions()]] 方法应当返回以 为键、
    以需要放到事务中的 DB 操作为值的数组。以下的常量
    可以表示相应的 DB 操作:

    • [[yii\db\ActiveRecord::OP_INSERT|OP_INSERT]]:插入操作用于执行 [[yii\db\ActiveRecord::insert()|insert()]];
    • [[yii\db\ActiveRecord::OP_UPDATE|OP_UPDATE]]:更新操作用于执行 [[yii\db\ActiveRecord::update()|update()]];
    • [[yii\db\ActiveRecord::OP_DELETE|OP_DELETE]]:删除操作用于执行 [[yii\db\ActiveRecord::delete()|delete()]]。

    使用 | 运算符连接上述常量来表明多个操作。您也可以使用
    快捷常量 [[yii\db\ActiveRecord::OP_ALL|OP_ALL]] 来指代上述所有的三个操作。

    这个事务方法的原理是:相应的事务在调用 [[yii\db\ActiveRecord::beforeSave()|beforeSave()]] 方法时开启,
    在调用 [[yii\db\ActiveRecord::afterSave()|afterSave()]] 方法时被提交。

    乐观锁是一种防止此冲突的方法:一行数据
    同时被多个用户更新。例如,同一时间内,用户 A 和用户 B 都在编辑
    相同的 wiki 文章。用户 A 保存他的编辑后,用户 B 也点击“保存”按钮来
    保存他的编辑。实际上,用户 B 正在处理的是过时版本的文章,
    因此最好是,想办法阻止他保存文章并向他提示一些信息。

    乐观锁通过使用一个字段来记录每行的版本号来解决上述问题。
    当使用过时的版本号保存一行数据时,[[yii\db\StaleObjectException]] 异常
    将被抛出,这阻止了该行的保存。乐观锁只支持更新 [[yii\db\ActiveRecord::update()]]
    或者删除 [[yii\db\ActiveRecord::delete()]]
    已经存在的单条数据行。

    使用乐观锁的步骤,

    1. 在与 Active Record 类相关联的 DB 表中创建一个列,以存储每行的版本号。
      这个列应当是长整型(在 MySQL 中是 BIGINT DEFAULT 0)。
    2. 重写 [[yii\db\ActiveRecord::optimisticLock()]] 方法返回这个列的命名。
    3. 在用于用户填写的 Web 表单中,添加一个隐藏字段(hidden field)来存储正在更新的行的当前版本号。
      (Active Record 类中)版本号这个属性你要自行写进 rules() 方法并自己验证一下。
    4. 在使用 Active Record 更新数据的控制器动作中,要捕获(try/catch) [[yii\db\StaleObjectException]] 异常。
      实现一些业务逻辑来解决冲突(例如合并更改,提示陈旧的数据等等)。

    例如,假定版本列被命名为 version。您可以使用下面的代码来实现乐观锁。

    1. // ------ 视图层代码 -------
    2. use yii\helpers\Html;
    3. // ...其他输入栏
    4. echo Html::activeHiddenInput($model, 'version');
    5. // ------ 控制器代码 -------
    6. use yii\db\StaleObjectException;
    7. public function actionUpdate($id)
    8. {
    9. $model = $this->findModel($id);
    10. try {
    11. if ($model->load(Yii::$app->request->post()) && $model->save()) {
    12. return $this->redirect(['view', 'id' => $model->id]);
    13. } else {
    14. return $this->render('update', [
    15. 'model' => $model,
    16. ]);
    17. }
    18. // 解决冲突的代码
    19. }
    20. }

    " class="reference-link">使用关联数据(Working with Relational Data)

    除了处理单个数据库表之外,Active Record 还可以将相关数据集中进来,
    使其可以通过原始数据轻松访问。 例如,客户数据与订单数据相关
    因为一个客户可能已经存放了一个或多个订单。这种关系通过适当的声明,
    你可以使用 $customer->orders 表达式访问客户的订单信息
    这表达式将返回包含 Order Active Record 实例的客户订单信息的数组。

    声明关联关系(Declaring Relations)

    你必须先在 Active Record 类中定义关联关系,才能使用 Active Record 的关联数据。
    简单地为每个需要定义关联关系声明一个 关联方法 即可,如下所示,

    1. class Customer extends ActiveRecord
    2. {
    3. // ...
    4. public function getOrders()
    5. {
    6. return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    7. }
    8. }
    9. class Order extends ActiveRecord
    10. {
    11. // ...
    12. public function getCustomer()
    13. {
    14. return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    15. }
    16. }

    上述的代码中,我们为 Customer 类声明了一个 orders 关联,
    和为 Order 声明了一个 customer 关联。

    每个关联方法必须这样命名:getXyz。然后我们通过 xyz(首字母小写)调用这个关联名。
    请注意关联名是大小写敏感的。

    当声明一个关联关系的时候,必须指定好以下的信息:

    • 关联的对应关系:通过调用 [[yii\db\ActiveRecord::hasMany()|hasMany()]]
      或者 [[yii\db\ActiveRecord::hasOne()|hasOne()]] 指定。在上面的例子中,您可以很容易看出这样的关联声明:
      一个客户可以有很多订单,而每个订单只有一个客户。
    • 相关联 Active Record 类名:用来指定为 [[yii\db\ActiveRecord::hasMany()|hasMany()]] 或者
      [[yii\db\ActiveRecord::hasOne()|hasOne()]] 方法的第一个参数。
      推荐的做法是调用 Xyz::className() 来获取类名称的字符串,以便您
      可以使用 IDE 的自动补全,以及让编译阶段的错误检测生效。
    • 两组数据的关联列:用以指定两组数据相关的列(hasOne()/hasMany() 的第二个参数)。
      数组的值填的是主数据的列(当前要声明关联的 Active Record 类为主数据),
      而数组的键要填的是相关数据的列。

      一个简单的口诀,先附表的主键,后主表的主键。
      正如上面的例子,customer_idOrder 的属性,而 idCustomer 的属性。
      (译者注:hasMany() 的第二个参数,这个数组键值顺序不要弄反了)

    " class="reference-link">访问关联数据(Accessing Relational Data)

    定义了关联关系后,你就可以通过关联名访问相应的关联数据了。就像
    访问一个由关联方法定义的对象一样,具体概念请查看 。
    因此,现在我们可以称它为 关联属性 了。

    1. // SELECT * FROM `customer` WHERE `id` = 123
    2. $customer = Customer::findOne(123);
    3. // SELECT * FROM `order` WHERE `customer_id` = 123
    4. // $orders 是由 Order 类组成的数组
    5. $orders = $customer->orders;

    Tip: 当你通过 getter 方法 getXyz() 声明了一个叫 xyz 的关联属性,你就可以像
    属性 那样访问 xyz。注意这个命名是区分大小写的。

    如果使用 [[yii\db\ActiveRecord::hasMany()|hasMany()]] 声明关联关系,则访问此关联属性
    将返回相关的 Active Record 实例的数组;
    如果使用 [[yii\db\ActiveRecord::hasOne()|hasOne()]] 声明关联关系,访问此关联属性
    将返回相关的 Active Record 实例,如果没有找到相关数据的话,则返回 null

    当你第一次访问关联属性时,将执行 SQL 语句获取数据,如
    上面的例子所示。如果再次访问相同的属性,将返回先前的结果,而不会重新执行
    SQL 语句。要强制重新执行 SQL 语句,你应该先 unset 这个关联属性,
    如:unset($ customer-> orders)

    Tip: 虽然这个概念跟 这个 特性很像,
    但是还是有一个很重要的区别。普通对象属性的属性值与其定义的 getter 方法的类型是相同的。
    而关联方法返回的是一个 [[yii\db\ActiveQuery]] 活动查询生成器的实例。只有当访问关联属性的的时候,
    才会返回 [[yii\db\ActiveRecord]] Active Record 实例,或者 Active Record 实例组成的数组。

    1. $customer->orders; // 获得 `Order` 对象的数组
    2. $customer->getOrders(); // 返回 ActiveQuery 类的实例

    这对于创建自定义查询很有用,下一节将对此进行描述。

    " class="reference-link">动态关联查询(Dynamic Relational Query)

    由于关联方法返回 [[yii\db\ActiveQuery]] 的实例,因此你可以在执行 DB 查询之前,
    使用查询构建方法进一步构建此查询。例如,

    1. $customer = Customer::findOne(123);
    2. // SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
    3. $orders = $customer->getOrders()
    4. ->where(['>', 'subtotal', 200])
    5. ->orderBy('id')
    6. ->all();

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

    有时你可能需要给你的关联声明传递参数,以便您能更方便地执行
    动态关系查询。例如,您可以声明一个 bigOrders 关联如下,

    1. class Customer extends ActiveRecord
    2. {
    3. public function getBigOrders($threshold = 100) // 老司机的提醒:$threshold 参数一定一定要给个默认值
    4. {
    5. return $this->hasMany(Order::className(), ['customer_id' => 'id'])
    6. ->where('subtotal > :threshold', [':threshold' => $threshold])
    7. ->orderBy('id');
    8. }
    9. }

    然后你就可以执行以下关联查询:

    1. // SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
    2. $orders = $customer->getBigOrders(200)->all();
    3. // SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 100 ORDER BY `id`
    4. $orders = $customer->bigOrders;

    中间关联表(Relations via a Junction Table)

    在数据库建模中,当两个关联表之间的对应关系是多对多时,
    通常会引入一个连接表。例如,order
    item 表可以通过名为 order_item 的连接表相关联。一个 order 将关联多个 order items,
    而一个 order item 也会关联到多个 orders。

    当声明这种表关联后,您可以调用 [[yii\db\ActiveQuery::via()|via()]] 或 [[yii\db\ActiveQuery::viaTable()|viaTable()]]
    指明连接表。[[yii\db\ActiveQuery::via()|via()]] 和 [[yii\db\ActiveQuery::viaTable()|viaTable()]] 之间的区别是
    前者是根据现有的关联名称来指定连接表,而后者直接使用
    连接表。例如,

    1. class Order extends ActiveRecord
    2. {
    3. public function getItems()
    4. {
    5. return $this->hasMany(Item::className(), ['id' => 'item_id'])
    6. ->viaTable('order_item', ['order_id' => 'id']);
    7. }
    8. }

    或者,

    1. class Order extends ActiveRecord
    2. {
    3. public function getOrderItems()
    4. {
    5. return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
    6. }
    7. public function getItems()
    8. {
    9. return $this->hasMany(Item::className(), ['id' => 'item_id'])
    10. ->via('orderItems');
    11. }
    12. }

    使用连接表声明的关联和正常声明的关联是等同的,例如,

    1. // SELECT * FROM `order` WHERE `id` = 100
    2. $order = Order::findOne(100);
    3. // SELECT * FROM `order_item` WHERE `order_id` = 100
    4. // SELECT * FROM `item` WHERE `item_id` IN (...)
    5. // 返回 Item 类组成的数组
    6. $items = $order->items;

    通过多个表来连接关联声明(Chaining relation definitions via multiple tables)

    通过使用 [[yii\db\ActiveQuery::via()|via()]] 方法,它还可以通过多个表来定义关联声明。
    再考虑考虑上面的例子,我们有 Customer, Order, 和 Item 类。
    我们可以添加一个关联关系到 Customer 类,这个关联可以列出了 Customer(客户) 的订单下放置的所有 Item(商品),
    这个关联命名为 getPurchasedItems(),关联声明如下代码示例所示:

    1. class Customer extends ActiveRecord
    2. {
    3. // ...
    4. public function getPurchasedItems()
    5. {
    6. // 客户的商品,将 Item 中的 'id' 列与 OrderItem 中的 'item_id' 相匹配
    7. return $this->hasMany(Item::className(), ['id' => 'item_id'])
    8. ->via('orderItems');
    9. }
    10. public function getOrderItems()
    11. {
    12. // 客户订单中的商品,将 `Order` 的 'id' 列和 OrderItem 的 'order_id' 列相匹配
    13. return $this->hasMany(OrderItem::className(), ['order_id' => 'id'])
    14. ->via('orders');
    15. }
    16. public function getOrders()
    17. {
    18. // 见上述列子
    19. return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    20. }
    21. }

    " class="reference-link">延迟加载和即时加载(Lazy Loading and Eager Loading)

    在 中,我们解释说可以像问正常的对象属性那样
    访问 Active Record 实例的关联属性。SQL 语句仅在
    你第一次访问关联属性时执行。我们称这种关联数据访问方法为 延迟加载
    例如,

    1. // SELECT * FROM `customer` WHERE `id` = 123
    2. $customer = Customer::findOne(123);
    3. // SELECT * FROM `order` WHERE `customer_id` = 123
    4. $orders = $customer->orders;
    5. // 没有 SQL 语句被执行
    6. $orders2 = $customer->orders;

    延迟加载使用非常方便。但是,当你需要访问相同的具有多个 Active Record 实例的关联属性时,
    可能会遇到性能问题。请思考一下以下代码示例。
    有多少 SQL 语句会被执行?

    1. // SELECT * FROM `customer` LIMIT 100
    2. $customers = Customer::find()->limit(100)->all();
    3. foreach ($customers as $customer) {
    4. // SELECT * FROM `order` WHERE `customer_id` = ...
    5. $orders = $customer->orders;
    6. }

    你瞅瞅,上面的代码会产生 101 次 SQL 查询!
    这是因为每次你访问 for 循环中不同的 Customer 对象的 orders 关联属性时,SQL 语句
    都会被执行一次。

    为了解决上述的性能问题,你可以使用所谓的 即时加载,如下所示,

    1. // SELECT * FROM `customer` LIMIT 100;
    2. // SELECT * FROM `orders` WHERE `customer_id` IN (...)
    3. $customers = Customer::find()
    4. ->with('orders')
    5. ->limit(100)
    6. ->all();
    7. foreach ($customers as $customer) {
    8. // 没有任何的 SQL 执行
    9. $orders = $customer->orders;
    10. }

    通过调用 [[yii\db\ActiveQuery::with()]] 方法,你使 Active Record 在一条 SQL 语句里就返回了这 100 位客户的订单。
    结果就是,你把要执行的 SQL 语句从 101 减少到 2 条!

    你可以即时加载一个或多个关联。 你甚至可以即时加载 嵌套关联 。嵌套关联是一种
    在相关的 Active Record 类中声明的关联。例如,Customer 通过 orders 关联属性 与 Order 相关联,
    OrderItem 通过 items 关联属性相关联。 当查询 Customer 时,您可以即时加载
    通过嵌套关联符 orders.items 关联的 items

    1. // 即时加载 "orders" and "country"
    2. $customers = Customer::find()->with('orders', 'country')->all();
    3. // 等同于使用数组语法 如下
    4. $customers = Customer::find()->with(['orders', 'country'])->all();
    5. // 没有任何的 SQL 执行
    6. $orders= $customers[0]->orders;
    7. // 没有任何的 SQL 执行
    8. $country = $customers[0]->country;
    9. // 即时加载“订单”和嵌套关系“orders.items”
    10. $customers = Customer::find()->with('orders.items')->all();
    11. // 访问第一个客户的第一个订单中的商品
    12. // 没有 SQL 查询执行
    13. $items = $customers[0]->orders[0]->items;

    你也可以即时加载更深的嵌套关联,比如 a.b.c.d。所有的父关联都会被即时加载。
    那就是, 当你调用 [[yii\db\ActiveQuery::with()|with()]] 来 with a.b.c.d, 你将即时加载
    a, a.b, a.b.c and a.b.c.d

    Tip: 一般来说,当即时加载 N 个关联,另有 M 个关联
    通过 连接表 声明,则会有 N+M+1 条 SQL 语句被执行。
    请注意这样的的嵌套关联 a.b.c.d 算四个关联。

    当即时加载一个关联,你可以通过匿名函数自定义相应的关联查询。
    例如,

    1. // 查找所有客户,并带上他们国家和活跃订单
    2. // SELECT * FROM `customer`
    3. // SELECT * FROM `country` WHERE `id` IN (...)
    4. // SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1
    5. $customers = Customer::find()->with([
    6. 'country',
    7. 'orders' => function ($query) {
    8. $query->andWhere(['status' => Order::STATUS_ACTIVE]);
    9. },
    10. ])->all();

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

    Tip: 如果你在即时加载的关联中调用 [[yii\db\Query::select()|select()]] 方法,你要确保
    在关联声明中引用的列必须被 select。否则,相应的模型(Models)可能
    无法加载。例如,

    1. $orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
    2. // $orders[0]->customer 会一直是 `null`。你应该这样写,以解决这个问题:
    3. $orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

    关联关系的 JOIN 查询(Joining with Relations)

    Tip: 这小节的内容仅仅适用于关系数据库,
    比如 MySQL,PostgreSQL 等等。

    到目前为止,我们所介绍的关联查询,仅仅是使用主表列
    去查询主表数据。实际应用中,我们经常需要在关联表中使用这些列。例如,
    我们可能要取出至少有一个活跃订单的客户。为了解决这个问题,我们可以
    构建一个 join 查询,如下所示:

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

    但是,更好的方法是通过调用 [[yii\db\ActiveQuery::joinWith()]] 来利用已存在的关联声明:

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

    两种方法都执行相同的 SQL 语句集。然而,后一种方法更干净、简洁。

    默认的,[[yii\db\ActiveQuery::joinWith()|joinWith()]] 会使用 LEFT JOIN 去连接主表和关联表。
    你可以通过 $joinType 参数指定不同的连接类型(比如 RIGHT JOIN)。
    如果你想要的连接类型是 INNER JOIN,你可以直接用 [[yii\db\ActiveQuery::innerJoinWith()|innerJoinWith()]] 方法代替。

    调用 [[yii\db\ActiveQuery::joinWith()|joinWith()]] 方法会默认 即时加载 相应的关联数据。
    如果你不需要那些关联数据,你可以指定它的第二个参数 $eagerLoadingfalse

    Note: 即使在启用即时加载的情况下使用 [[yii\db\ActiveQuery::joinWith()|joinWith()]] 或 [[yii\db\ActiveQuery::innerJoinWith()|innerJoinWith()]],相应的关联数据也不会从这个 JOIN 查询的结果中填充。 因此,每个连接关系还有一个额外的查询,正如部分所述。

    和 [[yii\db\ActiveQuery::with()|with()]] 一样,你可以 join 多个关联表;你可以动态的自定义
    你的关联查询;你可以使用嵌套关联进行 join。你也可以将 [[yii\db\ActiveQuery::with()|with()]]
    和 [[yii\db\ActiveQuery::joinWith()|joinWith()]] 组合起来使用。例如:

    1. $customers = Customer::find()->joinWith([
    2. 'orders' => function ($query) {
    3. $query->andWhere(['>', 'subtotal', 100]);
    4. },
    5. ])->with('country')
    6. ->all();

    有时,当连接两个表时,你可能需要在 JOIN 查询的 ON 部分中指定一些额外的条件。
    这可以通过调用 [[yii\db\ActiveQuery::onCondition()]] 方法来完成,如下所示:

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

    以上查询取出 所有 客户,并为每个客户取回所有活跃订单。
    请注意,这与我们之前的例子不同,后者仅取出至少有一个活跃订单的客户。

    Tip: 当通过 [[yii\db\ActiveQuery::onCondition()|onCondition()]] 修改 [[yii\db\ActiveQuery]] 时,
    如果查询涉及到 JOIN 查询,那么条件将被放在 ON 部分。如果查询不涉及
    JOIN ,条件将自动附加到查询的 WHERE 部分。
    因此,它可以只包含 包含了关联表的列 的条件。(译者注:意思是 onCondition() 中可以只写关联表的列,主表的列写不写都行)

    " class="reference-link">关联表别名(Relation table aliases)

    如前所述,当在查询中使用 JOIN 时,我们需要消除列名的歧义。因此通常为一张表定义
    一个别名。可以通过以下列方式自定义关联查询来设置关联查询的别名:

    然而,这看起来很复杂和耦合,不管是对表名使用硬编码或是调用 Order::tableName()
    从 2.0.7 版本起,Yii 为此提供了一个快捷方法。您现在可以定义和使用关联表的别名,如下所示:

    1. // 连接 `orders` 关联表并根据 `orders.id` 排序
    2. $query->joinWith(['orders o'])->orderBy('o.id');

    上述语法适用于简单的关联。如果在 join 嵌套关联时,
    需要用到中间表的别名,例如 $query->joinWith(['orders.product'])
    你需要嵌套 joinWith 调用,如下例所示:

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

    两个 Active Record 类之间的关联声明往往是相互关联的。例如,Customer
    通过 orders 关联到 Order ,而Order 通过 customer 又关联回到了 Customer

    1. class Customer extends ActiveRecord
    2. {
    3. public function getOrders()
    4. {
    5. return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    6. }
    7. }
    8. {
    9. public function getCustomer()
    10. {
    11. return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    12. }
    13. }

    现在考虑下面的一段代码:

    1. // SELECT * FROM `customer` WHERE `id` = 123
    2. $customer = Customer::findOne(123);
    3. // SELECT * FROM `order` WHERE `customer_id` = 123
    4. $order = $customer->orders[0];
    5. // SELECT * FROM `customer` WHERE `id` = 123
    6. $customer2 = $order->customer;
    7. // 显示 "not the same"
    8. echo $customer2 === $customer ? 'same' : 'not the same';

    我们原本认为 $customer$customer2 是一样的,但不是!其实他们确实包含相同的
    客户数据,但它们是不同的对象。 访问 $order->customer 时,需要执行额外的 SQL 语句,
    以填充出一个新对象 $customer2

    为了避免上述例子中最后一个 SQL 语句被冗余执行,我们应该告诉 Yii
    customerorders反向关联,可以通过调用 [[yii\db\ActiveQuery::inverseOf()|inverseOf()]] 方法声明,
    如下所示:

    1. class Customer extends ActiveRecord
    2. {
    3. public function getOrders()
    4. {
    5. return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
    6. }
    7. }

    这样修改关联声明后:

    1. // SELECT * FROM `customer` WHERE `id` = 123
    2. $customer = Customer::findOne(123);
    3. // SELECT * FROM `order` WHERE `customer_id` = 123
    4. $order = $customer->orders[0];
    5. // No SQL will be executed
    6. $customer2 = $order->customer;
    7. // 输出 "same"
    8. echo $customer2 === $customer ? 'same' : 'not the same';

    Note: 反向关联不能用在有 关联声明中。
    也就是说,如果一个关联关系通过 [[yii\db\ActiveQuery::via()|via()]] 或 [[yii\db\ActiveQuery::viaTable()|viaTable()]] 声明,
    你就不能再调用 [[yii\db\ActiveQuery::inverseOf()|inverseOf()]] 了。

    " class="reference-link">保存关联数据(Saving Relations)

    在使用关联数据时,您经常需要建立不同数据之间的关联或销毁
    现有关联。这需要为定义的关联的列设置正确的值。通过使用 Active Record,
    你就可以编写如下代码:

    1. $customer = Customer::findOne(123);
    2. $order = new Order();
    3. $order->subtotal = 100;
    4. // ...
    5. // 为 Order 设置属性以定义与 "customer" 的关联关系
    6. $order->customer_id = $customer->id;
    7. $order->save();

    Active Record 提供了 [[yii\db\ActiveRecord::link()|link()]] 方法,可以更好地完成此任务:

    1. $customer = Customer::findOne(123);
    2. $order = new Order();
    3. $order->subtotal = 100;
    4. // ...
    5. $order->link('customer', $customer);

    [[yii\db\ActiveRecord::link()|link()]] 方法需要指定关联名
    和要建立关联的目标 Active Record 实例。该方法将修改属性的值
    以连接两个 Active Record 实例,并将其保存到数据库。在上面的例子中,它将设置 Order 实例的 customer_id 属性
    Customer 实例的 id 属性的值,然后保存
    到数据库。

    Note: 你不能关联两个新的 Active Record 实例。

    使用 [[yii\db\ActiveRecord::link()|link()]] 的好处在通过 定义关系时更加明显。
    例如,你可以使用以下代码关联 Order 实例
    Item 实例:

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

    上述代码会自动在 order_item 关联表中插入一行,以关联 order 和 item 这两个数据记录。

    Info: [[yii\db\ActiveRecord::link()|link()]] 方法在保存相应的 Active Record 实例时,
    将不会执行任何数据验证。在调用此方法之前,
    您应当验证所有的输入数据。

    [[yii\db\ActiveRecord::link()|link()]] 方法的反向操作是 [[yii\db\ActiveRecord::unlink()|unlink()]] 方法,
    这将可以断掉两个 Active Record 实例间的已经存在了的关联关系。例如,

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

    默认情况下,[[yii\db\ActiveRecord::unlink()|unlink()]] 方法将设置指定的外键值,
    以把现有的关联指定为 null。此外,你可以选择通过将 $delete 参数设置为true 传递给方法,
    删除包含此外键值的表记录行。

    当关联关系中有连接表时,调用 [[yii\db\ActiveRecord::unlink()|unlink()]] 时,
    如果 $delete 参数是 true 的话,将导致
    连接表中的外键或相应的行被删除。

    " class="reference-link">跨数据库关联(Cross-Database Relations)

    Active Record 允许您在不同数据库驱动的 Active Record 类之间声明关联关系。
    这些数据库可以是不同的类型(例如 MySQL 和 PostgreSQL ,或是 MS SQL 和 MongoDB),它们也可以运行在
    不同的服务器上。你可以使用相同的语法来执行关联查询。例如,

    1. // Customer 对应的表是关系数据库中(比如 MySQL)的 "customer" 表
    2. class Customer extends \yii\db\ActiveRecord
    3. {
    4. public static function tableName()
    5. {
    6. return 'customer';
    7. }
    8. public function getComments()
    9. {
    10. // 一个 customer 有很多条评论(comments)
    11. return $this->hasMany(Comment::className(), ['customer_id' => 'id']);
    12. }
    13. }
    14. // Comment 对应的是 MongoDB 数据库中的 "comment" 集合(译者注:MongoDB 中的集合相当于 MySQL 中的表)
    15. class Comment extends \yii\mongodb\ActiveRecord
    16. {
    17. public static function collectionName()
    18. {
    19. return 'comment';
    20. }
    21. public function getCustomer()
    22. {
    23. // 一条评论对应一位 customer
    24. return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    25. }
    26. }
    27. $customers = Customer::find()->with('comments')->all();

    本节中描述的大多数关联查询功能,你都可以抄一抄。

    Note: [[yii\db\ActiveQuery::joinWith()|joinWith()]] 这个功能限制于某些数据库是否支持跨数据库 JOIN 查询。
    因此,你再上述的代码里就不能用此方法了,因为 MongoDB 不支持 JOIN 查询。

    默认情况下,[[yii\db\ActiveQuery]] 支持所有 Active Record 查询。要在 Active Record 类中使用自定义的查询类,
    您应该重写 [[yii\db\ActiveRecord::find()]] 方法并返回一个你自定义查询类的实例。
    例如,

    1. // file Comment.php
    2. namespace app\models;
    3. use yii\db\ActiveRecord;
    4. class Comment extends ActiveRecord
    5. {
    6. public static function find()
    7. {
    8. return new CommentQuery(get_called_class());
    9. }
    10. }

    现在,对于 Comment 类,不管你执行查询(比如 find()findOne()),还是定义一个关联(比如 hasOne()),
    你都将调用到 CommentQuery 实例,而不再是 ActiveQuery 实例。

    现在你可以定义 CommentQuery 类了,发挥你的奇技淫巧,以改善查询构建体验。例如,

    1. // file CommentQuery.php
    2. namespace app\models;
    3. use yii\db\ActiveQuery;
    4. class CommentQuery extends ActiveQuery
    5. {
    6. // 默认加上一些条件(可以跳过)
    7. public function init()
    8. {
    9. $this->andOnCondition(['deleted' => false]);
    10. parent::init();
    11. }
    12. // ... 在这里加上自定义的查询方法 ...
    13. public function active($state = true)
    14. {
    15. return $this->andOnCondition(['active' => $state]);
    16. }
    17. }

    Note: 作为 [[yii\db\ActiveQuery::onCondition()|onCondition()]] 方法的替代方案,你应当
    调用 [[yii\db\ActiveQuery::andOnCondition()|andOnCondition()]] 或 [[yii\db\ActiveQuery::orOnCondition()|orOnCondition()]] 方法来附加新增的条件,不然在一个新定义的查询方法,已存在的条件可能会被覆盖。

    然后你就可以先下面这样构建你的查询了:

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

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

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

    1. class Customer extends \yii\db\ActiveRecord
    2. {
    3. public function getActiveComments()
    4. {
    5. return $this->hasMany(Comment::className(), ['customer_id' => 'id'])->active();
    6. }
    7. }
    8. $customers = Customer::find()->joinWith('activeComments')->all();
    9. // 或者这样
    10. class Customer extends \yii\db\ActiveRecord
    11. {
    12. public function getComments()
    13. {
    14. return $this->hasMany(Comment::className(), ['customer_id' => 'id']);
    15. }
    16. }
    17. $customers = Customer::find()->joinWith([
    18. 'comments' => function($q) {
    19. $q->active();
    20. }
    21. ])->all();

    选择额外的字段(Selecting extra fields)

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

    你可以从查询中获取其他列或值,并将其存储在 Active Record 活动记录中。
    例如,假设我们有一个名为 room 的表,其中包含有关酒店可用房间的信息。
    每个房间使用字段 lengthwidthheight 存储有关其空间大小的信息。
    想象一下,我们需要检索出所有可用房间的列表,并按照体积大小倒序排列。
    你不可能使用 PHP 来计算体积,但是,由于我们需要按照它的值对这些记录进行排序,你依然需要 volume (体积)
    来显示在这个列表中。
    为了达到这个目标,你需要在你的 Room 活动记录类中声明一个额外的字段,它将存储 volume 的值:

    1. class Room extends \yii\db\ActiveRecord
    2. {
    3. public $volume;
    4. // ...
    5. }

    然后,你需要撰写一个查询,它可以计算房间的大小并执行排序:

    1. $rooms = Room::find()
    2. ->select([
    3. '{{room}}.*', // select all columns
    4. '([[length]] * [[width]] * [[height]]) AS volume', // 计算体积
    5. ])
    6. ->orderBy('volume DESC') // 使用排序
    7. ->all();
    8. foreach ($rooms as $room) {
    9. echo $room->volume; // 包含了由 SQL 计算出的值
    10. }

    额外字段的特性对于聚合查询非常有用。
    假设您需要显示一系列客户的订单数量。
    首先,您需要使用 orders 关系声明一个 Customer 类,并指定额外字段来存储 count 结果:

    1. class Customer extends \yii\db\ActiveRecord
    2. {
    3. public $ordersCount;
    4. // ...
    5. public function getOrders()
    6. {
    7. return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    8. }
    9. }

    然后你可以编写一个查询来 JOIN 订单表,并计算订单的总数:

    1. $customers = Customer::find()
    2. ->select([
    3. '{{customer}}.*', // select customer 表所有的字段
    4. 'COUNT({{order}}.id) AS ordersCount' // 计算订单总数
    5. ])
    6. ->joinWith('orders') // 连接表
    7. ->groupBy('{{customer}}.id') // 分组查询,以确保聚合函数生效
    8. ->all();

    使用此方法的一个缺点是,如果数据不是从 SQL 查询上加载的,它必须再单独计算一遍。
    因此,如果你通过常规查询获取个别的数据记录时,它没有额外的 select 语句,那么它
    将无法返回额外字段的实际值。新保存的记录一样会发生这种情。

    1. $room = new Room();
    2. $room->length = 100;
    3. $room->width = 50;
    4. $room->height = 2;
    5. $room->volume; // 为 `null`, 因为它没有被声明(赋值)

    通过 [[yii\db\BaseActiveRecord::get()|get()]] 和 [[yii\db\BaseActiveRecord::set()|set()]] 魔术方法
    我们可以将属性赋予行为特性:

    1. class Room extends \yii\db\ActiveRecord
    2. {
    3. private $_volume;
    4. public function setVolume($volume)
    5. {
    6. $this->_volume = (float) $volume;
    7. }
    8. public function getVolume()
    9. {
    10. if (empty($this->length) || empty($this->width) || empty($this->height)) {
    11. return null;
    12. }
    13. if ($this->_volume === null) {
    14. $this->setVolume(
    15. $this->length * $this->width * $this->height
    16. );
    17. }
    18. return $this->_volume;
    19. }
    20. // ...
    21. }

    当 select 查询不提供 volume 体积时,这模型将能够自动计算体积的值出来,
    当访问模型的属性的时候。

    当定义关联关系的时候,你也可以计算聚合字段:

    1. class Customer extends \yii\db\ActiveRecord
    2. {
    3. private $_ordersCount;
    4. public function setOrdersCount($count)
    5. {
    6. $this->_ordersCount = (int) $count;
    7. }
    8. public function getOrdersCount()
    9. {
    10. if ($this->isNewRecord) {
    11. return null; // 这样可以避免调用空主键进行查询
    12. }
    13. if ($this->_ordersCount === null) {
    14. $this->setOrdersCount($this->getOrders()->count()); // 根据关联关系按需计算聚合字段
    15. }
    16. return $this->_ordersCount;
    17. }
    18. // ...
    19. public function getOrders()
    20. {
    21. return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    22. }

    这种方法也适用于创建一些关联数据的快捷访问方式,特别是对于聚合。
    例如: