Yii快速入门(三)数据库操作

Yii提供了强大的数据库编程支持。Yii数据访问对象(DAO)建立在PHP的数据对象(PDO)extension上,使得在一个单一的统一的接口可以访问不同的数据库管理系统(DBMS)。使用Yii的DAO开发的应用程序可以很容易地切换使用不同的数据库管理系统,而不需要修改数据访问代码。Yii 的Active Record( AR ),实现了被广泛采用的对象关系映射(ORM)办法,进一步简化数据库编程。按照约定,一个类代表一个表,一个实例代表一行数据。Yii AR消除了大部分用于处理CRUD(创建,读取,更新和删除)数据操作的sql语句的重复任务。
尽管Yii的DAO和AR能够处理几乎所有数据库相关的任务,您仍然可以在Yii application中使用自己的数据库。事实上,Yii框架精心设计使得可以与其他第三方库同时使用。

一、数据访问对象 (DAO)

Yii DAO 基于 PHP Data Objects (PDO) 构建。它是一个为众多流行的DBMS提供统一数据访问的扩展,这些 DBMS 包括 MySQL, PostgreSQL 等等。因此,要使用 Yii DAO,PDO 扩展和特定的 PDO 数据库驱动(例如 PDO_MYSQL) 必须安装。
Yii DAO 主要包含如下四个类:

CDbConnection: 代表一个数据库连接。
CDbCommand: 代表一条通过数据库执行的 SQL 语句。
CDbDataReader: 代表一个只向前移动的,来自一个查询结果集中的行的流。
CDbTransaction: 代表一个数据库事务。

1、建立数据库连接

要建立一个数据库连接,创建一个 CDbConnection 实例并将其激活。连接到数据库需要一个数据源的名字(DSN)以指定连接信息。用户名和密码也可能会用到。当连接到数据库的过程中发生错误时 (例如,错误的 DSN 或无效的用户名/密码),将会抛出一个异常。

$connection=new CDbConnection($dsn,$username,$password);
// 建立连接。你可以使用  try...catch 捕获可能抛出的异常
$connection->active=true;
......
$connection->active=false;  // 关闭连接

DSN 的格式取决于所使用的 PDO 数据库驱动。总体来说, DSN 要含有 PDO 驱动的名字,跟上一个冒号,再跟上驱动特定的连接语法。可查阅 PDO 文档 获取更多信息。下面是一个常用DSN格式的列表。
* SQLite: sqlite:/path/to/dbfile
* MySQL: mysql:host=localhost;dbname=testdb
* PostgreSQL: pgsql:host=localhost;port=5432;dbname=testdb
* SQL Server: mssql:host=localhost;dbname=testdb
* Oracle: oci:dbname=//localhost:1521/testdb
由于 CDbConnection 继承自 CApplicationComponent,我们也可以将其作为一个 应用组件 使用。要这样做的话,请在 应用配置 中配置一个 db (或其他名字)应用组件如下:

array(
    ......
    'components'=>array(
        ......
        'db'=>array(
            'class'=>'CDbConnection',
            'connectionString'=>'mysql:host=localhost;dbname=testdb',
            'username'=>'root',
            'password'=>'password',
            'emulatePrepare'=>true,  // needed by some MySQL installations
        ),
    ),
)

然后我们就可以通过Yii::app()->db 访问数据库连接了。它已经被自动激活了,除非我们特意配置了 CDbConnection::autoConnect 为 false。通过这种方式,这个单独的DB连接就可以在我们代码中的很多地方共享。

2、执行SQL语句

数据库连接建立后,SQL 语句就可以通过使用 CDbCommand 执行了。你可以通过使用指定的SQL语句作为参数调用 CDbConnection::createCommand()创建一个 CDbCommand 实例。

$connection=Yii::app()->db;   // 假设你已经建立了一个 "db" 连接
// 如果没有,你可能需要显式建立一个连接:
// $connection=new CDbConnection($dsn,$username,$password);
$command=$connection->createCommand($sql);
// 如果需要,此 SQL 语句可通过如下方式修改:
// $command->text=$newSQL;

一条 SQL 语句会通过 CDbCommand 以如下两种方式被执行:
execute(): 执行一个无查询 (non-query)SQL语句,例如 INSERT, UPDATE 和 DELETE 。如果成功,它将返回此执行所影响的行数。
query(): 执行一条会返回若干行数据的 SQL 语句,例如 SELECT。如果成功,它将返回一个 CDbDataReader 实例,通过此实例可以遍历数据的结果行。为简便起见,(Yii)还实现了一系列 queryXXX() 方法以直接返回查询结果。
执行 SQL 语句时如果发生错误,将会抛出一个异常。

$rowCount=$command->execute();   // 执行无查询SQL
$dataReader=$command->query();   // 执行一个SQL查询
$rows=$command->queryAll();      // 查询并返回结果中的所有行
$row=$command->queryRow();       // 查询并返回结果中的第一行
$column=$command->queryColumn(); // 查询并返回结果中的第一列
$value=$command->queryScalar();  // 查询并返回结果中第一行的第一个字段

3、获取查询结果

CDbCommand::query() 生成 CDbDataReader 实例之后,你可以通过重复调用CDbDataReader::read() 获取结果中的行。你也可以在 PHP 的 foreach 语言结构中使用 CDbDataReader 一行行检索数据。

$dataReader=$command->query();
// 重复调用 read() 直到它返回 false
while(($row=$dataReader->read())!==false) { ... }
// 使用 foreach 遍历数据中的每一行
foreach($dataReader as $row) { ... }
// 一次性提取所有行到一个数组
$rows=$dataReader->readAll();

注意: 不同于query(), 所有的queryXXX()方法会直接返回数据。例如,queryRow()会返回代表查询结果第一行的一个数组。

4、使用事务

事务,在 Yii 中表现为 CDbTransaction 实例,可能会在下面的情况中启动:
* 开始事务.
* 一个个执行查询。任何对数据库的更新对外界不可见。
* 提交事务。如果事务成功,更新变为可见。
* 如果查询中的一个失败,整个事务回滚。
上述工作流可以通过如下代码实现:

$transaction=$connection->beginTransaction();
try
{
    $connection->createCommand($sql1)->execute();
    $connection->createCommand($sql2)->execute();
    //.... other SQL executions
    $transaction->commit();
}
catch(Exception $e) // 如果有一条查询失败,则会抛出异常
{
    $transaction->rollBack();
}

5、绑定参数

要避免 SQL 注入攻击 并提高重复执行的 SQL 语句的效率,你可以 "准备(prepare)"一条含有可选参数占位符的 SQL 语句,在参数绑定时,这些占位符将被替换为实际的参数。
参数占位符可以是命名的 (表现为一个唯一的标记) 或未命名的 (表现为一个问号)。调用 CDbCommand::bindParam()CDbCommand::bindValue() 以使用实际参数替换这些占位符。这些参数不需要使用引号引起来:底层的数据库驱动会为你搞定这个。参数绑定必须在 SQL 语句执行之前完成。

// 一条带有两个占位符 ":username" 和 ":email"的 SQL
$sql="INSERT INTO tbl_user (username, email) VALUES(:username,:email)";
$command=$connection->createCommand($sql);
// 用实际的用户名替换占位符 ":username"
$command->bindParam(":username",$username,PDO::PARAM_STR);
// 用实际的 Email 替换占位符 ":email"
$command->bindParam(":email",$email,PDO::PARAM_STR);
$command->execute();
// 使用新的参数集插入另一行
$command->bindParam(":username",$username2,PDO::PARAM_STR);
$command->bindParam(":email",$email2,PDO::PARAM_STR);
$command->execute();

方法 bindParam() 和 bindValue() 非常相似。唯一的区别就是前者使用一个PHP变量绑定参数,而后者使用一个值。对于那些内存中的大数据块参数,处于性能的考虑,应优先使用前者。

6、绑定列

当获取查询结果时,你也可以使用PHP变量绑定列。这样在每次获取查询结果中的一行时就会自动使用最新的值填充。

$sql="SELECT username, email FROM tbl_user";
$dataReader=$connection->createCommand($sql)->query();
// 使用 $username 变量绑定第一列 (username)
$dataReader->bindColumn(1,$username);
// 使用 $email 变量绑定第二列 (email)
$dataReader->bindColumn(2,$email);
while($dataReader->read()!==false)
{
    // $username 和 $email 含有当前行中的 username 和 email
}

7、使用表前缀

要使用表前缀,配置 CDbConnection::tablePrefix 属性为所希望的表前缀。然后,在 SQL 语句中使用 {{TableName}} 代表表的名字,其中的 TableName 是指不带前缀的表名。例如,如果数据库含有一个名为 tbl_user 的表,而 tbl_ 被配置为表前缀,那我们就可以使用如下代码执行用户相关的查询:

$sql='SELECT * FROM {{user}}';
$users=$connection->createCommand($sql)->queryAll();

二、Active Record

虽然Yii DAO可以处理几乎任何数据库相关的任务,但很可能我们会花费 90% 的时间以编写一些执行普通 CRUD(create, read, update 和 delete)操作的SQL语句。而且我们的代码中混杂了SQL语句时也会变得难以维护。要解决这些问题,我们可以使用Active Record。
Active Record(AR)是一个流行的对象-关系映射(ORM)技术。每个 AR 类代表一个数据表(或视图),数据表(或视图)的列在 AR 类中体现为类的属性,一个AR实例则表示表中的一行。常见的 CRUD 操作作为 AR 的方法实现。因此,我们可以以一种更加面向对象的方式访问数据。例如,我们可以使用以下代码向tbl_post表中插入一个新行。

$post=new Post;
$post->title='sample post';
$post->content='post body content';
$post->save();

注意: AR并非要解决所有数据库相关的任务。它的最佳应用是模型化数据表为PHP结构和执行不包含复杂SQL语句的查询。 对于复杂查询的场景,应使用Yii DAO。

1、建立数据库连接

AR依靠一个数据库连接以执行数据库相关的操作。默认情况下,它假定db应用组件提供了所需的CDbConnection数据库连接实例。如下应用配置提供了一个例子:

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'system.db.CDbConnection',
            'connectionString'=>'sqlite:path/to/dbfile',
            // 开启表结构缓存(schema caching)提高性能
            // 'schemaCachingDuration'=>3600,
        ),
    ),
);

提示: 由于Active Record依靠表的元数据(metadata)测定列的信息,读取元数据并解析需要时间。 如果你数据库的表结构很少改动,你应该通过配置CDbConnection::schemaCachingDuration属性的值为一个大于零的值开启表结构缓存。
如果你想使用一个不是db的应用组件,或者如果你想使用AR处理多个数据库,你应该覆盖CActiveRecord::getDbConnection() 。CActiveRecord类是所有AR类的基类。
提示: 通过AR使用多个数据库有两种方式。如果数据库的结构不同,你可以创建不同的AR基类实现不同的getDbConnection()。否则,动态改变静态变量CActiveRecord::db是一个好主意。

2、定义AR类

要访问一个数据表,我们首先需要通过集成CActiveRecord定义一个AR类。每个AR类代表一个单独的数据表,一个AR实例则代表那个表中的一行。
如下例子演示了代表tbl_post表的AR类的最简代码:

class Post extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
 
    public function tableName()
    {
        return 'tbl_post';
    }
}

提示: 由于 AR 类经常在多处被引用,我们可以导入包含 AR 类的整个目录,而不是一个个导入。 例如,如果我们所有的 AR 类文件都在 protected/models 目录中,我们可以配置应用如下:

 return array(
      'import'=>array(
          'application.models.*',
      ),
    );

默认情况下,AR类的名字和数据表的名字相同。如果不同,请覆盖tableName()方法。
要使用表前缀功能,AR类的 tableName() 方法可以通过如下方式覆盖

public function tableName()
{
    return '{{post}}';
}

这就是说,我们将没有前缀的表名用双大括号括起来,这样Yii就能自动添加前缀,从而返回完整的表名。
数据表行中列的值可以作为相应AR实例的属性访问。例如,如下代码设置了 title 列 (属性):

$post=new Post;
$post->title='a sample post';

虽然我们从未在Post类中显式定义属性title,我们还是可以通过上述代码访问。这是因为title是tbl_post表中的一个列,CActiveRecord通过PHP的__get()魔术方法使其成为一个可访问的属性。如果我们尝试以同样的方式访问一个不存在的列,将会抛出一个异常。
如果一个表没有主键,则必须在相应的AR类中通过如下方式覆盖 primaryKey() 方法指定哪一列或哪几列作为主键。

public function primaryKey()
{
    return 'id';
    // 对于复合主键,要返回一个类似如下的数组
    // return array('pk1', 'pk2');
}

3、创建记录

要向数据表中插入新行,我们要创建一个相应 AR 类的实例,设置其与表的列相关的属性,然后调用 save() 方法完成插入:

$post=new Post;
$post->title='sample post';
$post->content='content for the sample post';
$post->create_time=time();
$post->save();

如果表的主键是自增的,在插入完成后,AR实例将包含一个更新的主键。在上面的例子中,id属性将反映出新插入帖子的主键值,即使我们从未显式地改变它。
如果一个列在表结构中使用了静态默认值(例如一个字符串,一个数字)定义。则AR实例中相应的属性将在此实例创建时自动含有此默认值。改变此默认值的一个方式就是在AR类中显示定义此属性:

class Post extends CActiveRecord
{
    public $title='please enter a title';
    ......
}
$post=new Post;
echo $post->title;  // 这儿将显示: please enter a title

记录在保存(插入或更新)到数据库之前,其属性可以赋值为 CDbExpression 类型。例如,为保存一个由MySQL的 NOW() 函数返回的时间戳,我们可以使用如下代码:

$post=new Post;
$post->create_time=new CDbexpression_r('NOW()'); //CDbExpression类就是计算数据库表达式的值
// $post->create_time='NOW()'; 不会起作用,因为
// 'NOW()' 将会被作为一个字符串处理。
$post->save();

提示: 由于AR允许我们无需写一大堆SQL语句就能执行数据库操作, 我们经常会想知道AR在背后到底执行了什么SQL语句。这可以通过开启Yii的日志功能实现。例如,我们在应用配置中开启了CWebLogRoute,我们将会在每个网页的最后看到执行过的SQL语句。 我们也可以在应用配置中设置CDbConnection::enableParamLogging为true,这样绑定在SQL语句中的参数值也会被记录。

4、读取记录

要读取数据表中的数据,我们可以通过如下方式调用 find 系列方法中的一种:

// 查找满足指定条件的结果中的第一行
$post=Post::model()->find($condition,$params);
// 查找具有指定主键值的那一行
$post=Post::model()->findByPk($postID,$condition,$params);
// 查找具有指定属性值的行
$post=Post::model()->findByAttributes($attributes,$condition,$params);
// 通过指定的SQL语句查找结果中的第一行
$post=Post::model()->findBySql($sql,$params);

如上所示,我们通过 Post::model() 调用find 方法。请记住,静态方法 model() 是每个AR类所必须的。此方法返回在对象上下文中的一个用于访问类级别方法(类似于静态类方法的东西)的AR实例。
如果find方法找到了一个满足查询条件的行,它将返回一个Post实例,实例的属性含有数据表行中相应列的值。然后我们就可以像读取普通对象的属性那样读取载入的值,例如 echo $post->title;
如果使用给定的查询条件在数据库中没有找到任何东西, find 方法将返回null。
调用find时,我们使用 $condition$params 指定查询条件。此处 $condition 可以是 SQL 语句中的 WHERE 字符串,$params 则是一个参数数组,其中的值应绑定到 $condition 中的占位符。例如:

// 查找 postID=10 的那一行
$post=Post::model()->find('postID=:postID', array(':postID'=>10));

注意: 在上面的例子中,我们可能需要在特定的 DBMS 中将 postID 列的引用进行转义。
例如,如果我们使用 PostgreSQL,我们必须将此表达式写为 "postID"=:postID,因为 PostgreSQL 在默认情况下对列名大小写不敏感。
我们也可以使用 $condition 指定更复杂的查询条件。不使用字符串,我们可以让 $condition 成为一个 CDbCriteria 的实例,它允许我们指定不限于 WHERE 的条件。例如:

$criteria=new CDbCriteria;
$criteria->select='title';  // 只选择 'title' 列
$criteria->condition='postID=:postID';
$criteria->params=array(':postID'=>10);
$post=Post::model()->find($criteria); // $params 不需要了

注意,当使用 CDbCriteria 作为查询条件时,$params 参数不再需要了,因为它可以在 CDbCriteria 中指定,就像上面那样。
一种替代 CDbCriteria 的方法是给 find 方法传递一个数组。数组的键和值各自对应标准(criterion)的属性名和值,上面的例子可以重写为如下:

$post=Post::model()->find(array(
    'select'=>'title',
    'condition'=>'postID=:postID',
    'params'=>array(':postID'=>10),
));

当一个查询条件是关于按指定的值匹配几个列时,我们可以使用 findByAttributes()。我们使 $attributes 参数是一个以列名做索引的值的数组。在一些框架中,此任务可以通过调用类似 findByNameAndTitle 的方法实现。虽然此方法看起来很诱人, 但它常常引起混淆,冲突和比如列名大小写敏感的问题。
当有多行数据匹配指定的查询条件时,我们可以通过下面的 findAll 方法将他们全部带回。每个都有其各自的 find 方法,就像我们已经讲过的那样。

// 查找满足指定条件的所有行
$posts=Post::model()->findAll($condition,$params);
// 查找带有指定主键的所有行
$posts=Post::model()->findAllByPk($postIDs,$condition,$params);
// 查找带有指定属性值的所有行
$posts=Post::model()->findAllByAttributes($attributes,$condition,$params);
// 通过指定的SQL语句查找所有行
$posts=Post::model()->findAllBySql($sql,$params);

如果没有任何东西符合查询条件,findAll 将返回一个空数组。
这跟 find 不同,find 会在没有找到什么东西时返回 null。
除了上面讲述的 find 和 findAll 方法,为了方便,(Yii)还提供了如下方法:

// 获取满足指定条件的行数
$n=Post::model()->count($condition,$params);
// 通过指定的 SQL 获取结果行数
$n=Post::model()->countBySql($sql,$params);
// 检查是否至少有一行复合指定的条件
$exists=Post::model()->exists($condition,$params);

5、更新记录

在 AR 实例填充了列的值之后,我们可以改变它们并把它们存回数据表。

$post=Post::model()->findByPk(10);
$post->title='new post title';
$post->save(); // 将更改保存到数据库

正如我们可以看到的,我们使用同样的 save() 方法执行插入和更新操作。如果一个 AR 实例是使用 new 操作符创建的,调用 save() 将会向数据表中插入一行新数据;如果 AR 实例是某个findfindAll 方法的结果,调用 save() 将更新表中现有的行。实际上,我们是使用 CActiveRecord::isNewRecord 说明一个 AR 实例是不是新的。
直接更新数据表中的一行或多行而不首先载入也是可行的。 AR 提供了如下方便的类级别方法实现此目的:

// 更新符合指定条件的行
Post::model()->updateAll($attributes,$condition,$params);
// 更新符合指定条件和主键的行
Post::model()->updateByPk($pk,$attributes,$condition,$params);
// 更新满足指定条件的行的计数列
Post::model()->updateCounters($counters,$condition,$params);

在上面的代码中, $attributes 是一个含有以 列名作索引的列值的数组; $counters是一个由列名索引的可增加的值的数组;$condition$params在前面的段落中已有描述。

6、删除记录

如果一个 AR 实例被一行数据填充,我们也可以删除此行数据。

$post=Post::model()->findByPk(10); // 假设有一个帖子,其 ID 为 10
$post->delete(); // 从数据表中删除此行

注意,删除之后, AR 实例仍然不变,但数据表中相应的行已经没了。
使用下面的类级别代码,可以无需首先加载行就可以删除它。

// 删除符合指定条件的行
Post::model()->deleteAll($condition,$params);
// 删除符合指定条件和主键的行
Post::model()->deleteByPk($pk,$condition,$params);

7、数据验证

当插入或更新一行时,我们常常需要检查列的值是否符合相应的规则。如果列的值是由最终用户提供的,这一点就更加重要。总体来说,我们永远不能相信任何来自客户端的数据。
当调用 save() 时, AR 会自动执行数据验证。验证是基于在 AR 类的 rules() 方法中指定的规则进行的。关于验证规则的更多详情,请参考 声明验证规则 一节。下面是保存记录时所需的典型的工作流。

if($post->save())
{
    // 数据有效且成功插入/更新
}
else
{
    // 数据无效,调用  getErrors() 提取错误信息
}

当要插入或更新的数据由最终用户在一个 HTML 表单中提交时,我们需要将其赋给相应的 AR 属性。我们可以通过类似如下的方式实现:

$post->title=$_POST['title'];
$post->content=$_POST['content'];
$post->save();

如果有很多列,我们可以看到一个用于这种复制的很长的列表。这可以通过使用如下所示的 attributes 属性简化操作。更多信息可以在 安全的特性赋值 一节和 创建动作 一节找到。

// 假设 $_POST['Post'] 是一个以列名索引列值为值的数组
$post->attributes=$_POST['Post'];
$post->save();

8、对比记录

类似于表记录,AR实例由其主键值来识别。因此,要对比两个AR实例,假设它们属于相同的AR类, 我们只需要对比它们的主键值。然而,一个更简单的方式是调用 CActiveRecord::equals()
不同于AR在其他框架的执行, Yii在其 AR 中支持多个主键. 一个复合主键由两个或更多字段构成。相应地,主键值在Yii中表现为一个数组。primaryKey属性给出了一个 AR 实例的主键值。

9、自定义

CActiveRecord 提供了几个占位符方法,它们可以在子类中被覆盖以自定义其工作流。

beforeValidate 和 afterValidate:这两个将在验证数据有效性之前和之后被调用。
beforeSave 和 afterSave: 这两个将在保存 AR 实例之前和之后被调用。
beforeDelete 和 afterDelete: 这两个将在一个 AR 实例被删除之前和之后被调用。
afterConstruct: 这个将在每个使用 new 操作符创建 AR 实例后被调用。
beforeFind: 这个将在一个 AR 查找器被用于执行查询(例如 find(), findAll())之前被调用。
afterFind: 这个将在每个 AR 实例作为一个查询结果创建时被调用。

10、使用AR处理事务

每个 AR 实例都含有一个属性名叫 dbConnection ,是一个 CDbConnection 的实例,这样我们可以在需要时配合 AR 使用由 Yii DAO 提供的 事务 功能:

$model=Post::model();
$transaction=$model->dbConnection->beginTransaction();
try
{
    // 查找和保存是可能由另一个请求干预的两个步骤
    // 这样我们使用一个事务以确保其一致性和完整性
    $post=$model->findByPk(10);
    $post->title='new post title';
    $post->save();
    $transaction->commit();
}
catch(Exception $e)
{
    $transaction->rollBack();
}

11、命名范围

命名范围(named scope)表示一个命名的(named)查询规则,它可以和其他命名范围联合使用并应用于Active Record查询。
命名范围主要是在 CActiveRecord::scopes() 方法中以名字-规则对的方式声明。如下代码在Post模型类中声明了两个命名范围, publishedrecently

class Post extends CActiveRecord
{
    ......
    public function scopes()
    {
        return array(
            'published'=>array(
                'condition'=>'status=1',
            ),
            'recently'=>array(
                'order'=>'create_time DESC',
                'limit'=>5,
            ),
        );
    }
}

每个命名范围声明为一个可用于初始化 CDbCriteria 实例的数组。例如,recently 命名范围指定 order 属性为 create_time DESClimit 属性为 5。他们翻译为查询规则后就会返回最近的5篇帖子。
命名范围多用作 find 方法调用的修改器。几个命名范围可以链到一起形成一个更有约束性的查询结果集。例如,要找到最近发布的帖子,我们可以使用如下代码:
$posts=Post::model()->published()->recently()->findAll();
总体来说,命名范围必须出现在一个 find 方法调用的左边。它们中的每一个都提供一个查询规则,并联合到其他规则,包括传递给 find 方法调用的那一个。最终结果就像给一个查询添加了一系列过滤器。
命名范围也可用于 updatedelete 方法。例如,如下代码将删除所有最近发布的帖子:
Post::model()->published()->recently()->delete();
注意: 命名范围只能用于类级别方法。也就是说,此方法必须使用 ClassName::model() 调用。

12、参数化的命名范围

命名范围可以参数化。例如,我们想自定义 recently 命名范围中指定的帖子数量,要实现此目的,不是在CActiveRecord::scopes 方法中声明命名范围,而是需要定义一个名字和此命名范围的名字相同的方法:

public function recently($limit=5)
{
    $this->getDbCriteria()->mergeWith(array(
        'order'=>'create_time DESC',
        'limit'=>$limit,
    ));
    return $this;
}

然后,我们就可以使用如下语句获取3条最近发布的帖子。
$posts=Post::model()->published()->recently(3)->findAll();
上面的代码中,如果我们没有提供参数 3,我们将默认获取 5 条最近发布的帖子。

13、默认的命名范围

模型类可以有一个默认命名范围,它将应用于所有 (包括相关的那些) 关于此模型的查询。例如,一个支持多种语言的网站可能只想显示当前用户所指定的语言的内容。因为可能会有很多关于此网站内容的查询,我们可以定义一个默认的命名范围以解决此问题。为实现此目的,我们覆盖 CActiveRecord::defaultScope 方法如下:

class Content extends CActiveRecord
{
    public function defaultScope()
    {
        return array(
            'condition'=>"language='".Yii::app()->language."'",
        );
    }
}

现在,如果下面的方法被调用,将会自动使用上面定义的查询规则:
$contents=Content::model()->findAll();
注意,默认的命名范围只会应用于 SELECT 查询。INSERT, UPDATEDELETE 查询将被忽略。

三、Relational Active Record(关联查询)

我们已经知道如何通过Active Record(AR)从单个数据表中取得数据了,在这一节中,我们将要介绍如何使用AR来连接关联的数据表获取数据。
在使用关联AR之前,首先要在数据库中建立关联的数据表之间的主键-外键关联,AR需要通过分析数据库中的定义数据表关联的元信息,来决定如何连接数据。

1、如何声明关联

在使用AR进行关联查询之前,我们需要告诉AR各个AR类之间有怎样的关联。
AR类之间的关联直接反映着数据库中这个类所代表的数据表之间的关联。从关系数据库的角度来说,两个数据表A,B之间可能的关联有三种:一对多,一对一,多对多。而在AR中,关联有以下四种:

BELONGS_TO: 如果数据表A和B的关系是一对多,那我们就说B属于A(B belongs to A)。
HAS_MANY: 如果数据表A和B的关系是多对一,那我们就说B有多个A(B has many A)。
HAS_ONE: 这是‘HAS_MANY’关系中的一个特例,当A最多有一个的时候,我们说B有一个A (B has one A)。
MANY_MANY: 这个相当于关系数据库中的多对多关系。因为绝大多数关系数据库并不直接支持多对多的关系,这时通常都需要一个单独的关联表,把多对多的关系分解为两个一对多的关系。用AR的方式去理解的话,我们可以认为 MANY_MANY关系是由BELONGS_TO和HAS_MANY组成的。

在AR中声明关联,是通过覆盖(Override)父类CActiveRecord中的relations()方法来实现的。这个方法返回一个包含了关系定义的数组,数组中的每一组键值代表一个关联:
'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...additional options)
这里的VarName是这个关联的名称;RelationType指定了这个关联的类型,有四个常量代表了四种关联的类型:
self::BELONGS_TO,self::HAS_ONE,self::HAS_MANY和self::MANY_MANY; ClassName是这个关系关联到的AR类的类名;ForeignKey指定了这个关联是通过哪个外键联系起来的。后面的additional options可以加入一些额外的设置,后面会做介绍。
下面的代码演示了如何定义UserPost之间的关联。

class Post extends CActiveRecord {
    public function relations() {
        return array(
                'author'=>array(
                    self::BELONGS_TO,
                     'User',
                     'authorID'
                    ),
                 'categories'=>array(
                    self::MANY_MANY,
                    'Category',
                    'PostCategory(postID, categoryID)'
                    ),
            );
    }
}

class User extends CActiveRecord {
    public function relations() {
        return array(
                'posts'=>array(
                    self::HAS_MANY,
                    'Post',
                    'authorID'
                    ),
                'profile'=>array(
                    self::HAS_ONE,
                    'Profile',
                    'ownerID'
                    ),
            );
    }
}

说明: 有时外键可能由两个或更多字段组成,在这里可以将多个字段名由逗号或空格分隔, 一并写在这里。对于多对多的关系,关联表必须在外键中注明,例如在Post类的categories 关联中,外键就需要写成PostCategory(postID, categoryID)。
在AR类中声明关联时,每个关联会作为一个属性添加到AR类中,属性名就是关联的名称。在进行关联查询时,这些属性就会被设置为关联到的AR类的实例,例如在查询取得一个Post实例时,它的$author属性就是代表Post作者的一个User类的实例。

2、关联查询

进行关联查询最简单的方式就是访问一个关联AR对象的某个关联属性。如果这个属性之前没有被访问过,这时就会启动一个关联查询,通过当前AR对象的主键连接相关的表,来取得关联对象的值,然后将这些数据保存在对象的属性中。这种方式叫做“延迟加载”,也就是只有等到访问到某个属性时,才会真正到数据库中把这些关联的数据取出来。下面的例子描述了延迟加载的过程:

// retrieve the post whose ID is 10
$post=Post::model()->findByPk(10);
// retrieve the post's author: a relational query will be performed here
$author=$post->author;

在不同的关联情况下,如果没有查询到结果,其返回的值也不同:BELONGS_TOHAS_ONE 关联,无结果时返回null; HAS_MANYMANY_MANY, 无结果时返回空数组。
延迟加载方法使用非常方便,但在某些情况下并不高效。例如,若我们要取得N个post的作者信息,使用延迟方法将执行N次连接查询。此时我们应当使用所谓的急切加载方法。
急切加载方法检索主要的 AR 实例及其相关的 AR 实例. 这通过使用 with() 方法加上 findfindAll 方法完成。例如,
$posts=Post::model()->with('author')->findAll();
上面的代码将返回一个由 Post 实例组成的数组. 不同于延迟加载方法,每个Post 实例中的author 属性在我们访问此属性之前已经被关联的 User 实例填充。不是为每个post 执行一个连接查询, 急切加载方法在一个单独的连接查询中取出所有的 post 以及它们的author!
我们可以在with()方法中指定多个关联名字。例如, 下面的代码将取回 posts 以及它们的作者和分类:
$posts=Post::model()->with('author','categories')->findAll();
我们也可以使用嵌套的急切加载。不使用一个关联名字列表, 我们将关联名字以分层的方式传递到 with() 方法, 如下,

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->findAll();

上面的代码将取回所有的 posts 以及它们的作者和分类。它也将取出每个作者的profileposts.
急切加载也可以通过指定 CDbCriteria::with 属性被执行, 如下:

$criteria=new CDbCriteria;
$criteria->with=array(
'author.profile',
    'author.posts',
    'categories',
);
$posts=Post::model()->findAll($criteria);
或
$posts=Post::model()->findAll(array(
'with'=>array(
        'author.profile',
        'author.posts',
        'categories',
    )
);

3、关联查询选项

之前我们提到额外的参数可以被指定在关联声明中。这些选项,指定为 name-value 对,被用来定制关联查询。它们被概述如下:

select: 为关联 AR 类查询的字段列表。默认是 '*', 意味着所有字段。
        查询的字段名字可用别名表达式来消除歧义(例如:COUNT(??.name) AS nameCount)。
condition: WHERE 子语句。默认为空。注意, 列要使用别名引用(例如:??.id=10)。
params: 被绑定到 SQL 语句的参数. 应当为一个由 name-value 对组成的数组()。
on: ON 子语句. 这里指定的条件将使用 and 操作符被追加到连接条件中。
       此选项中的字段名应被消除歧义。此选项不适用于 MANY_MANY 关联。
order: ORDER BY 子语句。默认为空。注意, 列要使用别名引用(例如:??.age DESC)。
with: 应当和此对象一同载入的子关联对象列表. 注意, 不恰当的使用可能会形成一个无穷的关联循环。
joinType: 此关联的连接类型。默认是 LEFT OUTER JOIN。
aliasToken:列前缀占位符。默认是“??.”。
alias: 关联的数据表的别名。默认是 null, 意味着表的别名和关联的名字相同。
together: 是否关联的数据表被强制与主表和其他表连接。此选项只对于HAS_MANY 和 MANY_MANY 关联有意义。
          若此选项被设置为 false, ......(此处原文出错!).默认为空。此选项中的字段名以被消除歧义。
having: HAVING 子语句。默认是空。注意, 列要使用别名引用。
index: 返回的数组索引类型。确定返回的数组是关键字索引数组还是数字索引数组。
       不设置此选项, 将使用数字索引数组。此选项只对于HAS_MANY 和 MANY_MANY 有意义
       此外, 下面的选项在延迟加载中对特定关联是可用的:
group: GROUP BY子句。默认为空。注意, 列要使用别名引用(例如:??.age)。 
       本选项仅应用于HAS_MANY 和 MANY_MANY 关联。
having: HAVING子句。默认为空。注意, 列要使用别名引用(例如:??.age)。
       本选项仅应用于HAS_MANY 和 MANY_MANY 关联。
limit: 限制查询的行数。本选项不能用于BELONGS_TO关联。
offset: 偏移。本选项不能用于BELONGS_TO关联。

下面我们改变在 User 中的 posts 关联声明,通过使用上面的一些选项:

class User extends CActiveRecord 
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
                            'order'=>'posts.create_time DESC',
                            'with'=>'categories'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
        );
    }
}

现在若我们访问 $author->posts, 我们将得到用户的根据发表时间降序排列的 posts. 每个 post 实例也载入了它的分类。

4、为字段名消除歧义

当一个字段的名字出现在被连接在一起的两个或更多表中,需要消除歧义(disambiguated)。可以通过使用表的别名作为字段名的前缀实现。
在关联AR查询中,主表的别名确定为 t,而一个关联表的别名和相应的关联的名字相同(默认情况下)。 例如,在下面的语句中,Post 的别名是 t ,而 Comment 的别名是 comments:
$posts=Post::model()->with('comments')->findAll();
现在假设 PostComment 都有一个字段 create_time , 我们希望取出 posts 及它们的 comments ,排序方式是先根据 posts 的创建时间,然后根据 comment 的创建时间。 我们需要消除create_time 字段的歧义,如下:

$posts=Post::model()->with('comments')->findAll(array(
    'order'=>'t.create_time, comments.create_time'
));

默认情况下,Yii 自动为每个关联表产生一个表别名,我们必须使用此前缀 ??. 来指向这个自动产生的别名。 主表的别名是表自身的名字。

5、动态关联查询选项

我们使用 with()with 均可使用动态关联查询选项。 动态选项将覆盖在 relations() 方法中指定的
已存在的选项。例如,使用上面的 User 模型, 若我们想要使用急切加载方法以升序来取出属于一个作者的 posts(关联中的order 选项指定为降序), 我们可以这样做:

User::model()->with(array(
    'posts'=>array('order'=>'posts.create_time ASC'),
    'profile',
))->findAll();

动态查询选项也可以在使用延迟加载方法时使用以执行关联查询。 要这样做,我们应当调用一个方法,它的名字和关联的名字相同,并传递动态查询选项 作为此方法的参数。例如,下面的代码返回一个用户的 status 为 1 的posts :

$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));

6、关联查询的性能

如上所述,急切加载方法主要用于当我们需要访问许多关联对象时。 通过连接所有所需的表它产生一个大而复杂的 SQL 语句。一个大的 SQL 语句在许多情况下是首选的。然而在一些情况下它并不高效。
考虑一个例子,若我们需要找出最新的文章以及它们的评论。 假设每个文章有 10 条评论,使用一个大的 SQL 语句,我们将取回很多多余的 post 数据, 因为每个post 将被它的每条评论反复使用。现在让我们尝试另外的方法:我们首先查询最新的文章, 然后查询它们的评论。用新的方法,我们需要执行执行两条 SQL 语句。有点是在查询结果中没有多余的数据。
因此哪种方法更加高效?没有绝对的答案。执行一条大的 SQL 语句也许更加高效,因为它需要更少的花销来解析和执行 SQL 语句。另一方面,使用单条 SQL 语句,我们得到更多冗余的数据,因此需要更多时间来阅读和处理它们。 因为这个原因,Yii 提供了 together 查询选项一边我们在需要时选择两种方法之一。默认下, Yii 使用第一种方式,即产生一个单独的 SQL 语句来执行急切加载。我们可以在关联声明中设置 together 选项为 false 以便一些表被连接在单独的 SQL 语句中。例如,为了使用第二种方法来查询最新的文章及它们的评论,我们可以在 Post 类中声明 comments 关联如下,

public function relations()
{
    return array(
        'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false),
    );
}

当我们执行急切加载时,我们也可以动态地设置此选项:
$posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll();

7、统计查询

除了上面描述的关联查询,Yii 也支持所谓的统计查询(或聚合查询)。 它指的是检索关联对象的聚合信息,例如每个 post 的评论的数量,每个产品的平均等级等。 统计查询只被 HAS_MANY(例如,一个 post 有很多评论) 或 MANY_MANY (例如,一个post 属于很多分类和一个 category 有很多 post) 关联对象执行。
执行统计查询非常类似于之前描述的关联查询。我们首先需要在 CActiveRecordrelations() 方法中声明统计查询。

class Post extends CActiveRecord
{
    public function relations()
   {
        return array(
            'commentCount'=>array(self::STAT, 'Comment', 'post_id'),
            'categoryCount'=>array(self::STAT, 'Category', 'post_category(post_id,
category_id)'),
        );
    }
}

在上面,我们声明了两个统计查询:commentCount 计算属于一个 post 的评论的数量,categoryCount 计算一个 post 所属分类的数量。注意 PostComment 之间的关联类型是 HAS_MANY, 而 PostCategory 之间的关联类型是 MANY_MANY (使用连接表 PostCategory)。 如我们所看到的,声明非常类似于之间小节中的关联。唯一的不同是这里的关联类型是 STAT
有了上面的声明,我们可以检索使用表达式 $post->commentCount 检索一个 post 的评论的数量。 当我们首次访问此属性,一个 SQL 语句将被隐含地执行并检索 对应的结果。我们已经知道,这是所谓的 lazy loading 方法。若我们需要得到多个post 的评论数目,我们也可以使用 eager loading 方法:
$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();
上面的语句将执行三个 SQL 语句以取回所有的 post 及它们的评论数目和分类数目。使用延迟加载方法, 若有 N 个 post ,我们使用 2*N+1 条 SQL 查询完成。
默认情况下,一个统计查询将计算 COUNT 表达式(and thus the comment count and category count in the above example). 当我们在 relations()中声明它时,通过 指定额外的选项,可以定制它。可用的选项简介如下。

select:       统计表达式。默认是 COUNT(*),意味着子对象的个数。
defaultValue: 没有接收一个统计查询结果时被赋予的值。例如,若一个 post 没有任何评论,
              它的 commentCount 将接收此值。此选项的默认值是 0。
condition:    WHERE 子语句。默认是空。
params:       被绑定到产生的SQL 语句中的参数。它应当是一个 name-value 对组成的数组。
order:        ORDER BY 子语句。默认是空。
group:        GROUP BY 子语句。默认是空。
having:       HAVING 子语句。默认是空。

8、关联查询命名空间

关联查询也可以和 命名空间一起执行。有两种形式。第一种形式,命名空间被应用到主模型。第二种形式,命名空间被应用到关联模型。
下面的代码展示了如何应用命名空间到主模型。
$posts=Post::model()->published()->recently()->with('comments')->findAll();
这非常类似于非关联的查询。唯一的不同是我们在命名空间后使用了 with() 调用。 此查询应当返回最近发布的 post和它们的评论。
下面的代码展示了如何应用命名空间到关联模型。
$posts=Post::model()->with('comments:recently:approved')->findAll();
上面的查询将返回所有的 post 及它们审核后的评论。注意 comments 指的是关联名字,而recentlyapproved 指的是 在 Comment 模型类中声明的命名空间。关联名字和命名空间应当由冒号分隔。
命名空间也可以在 CActiveRecord::relations() 中声明的关联规则的 with 选项中指定。在下面的例子中, 若我们访问 $user->posts,它将返回此post 的所有审核后的评论。

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
                'with'=>'comments:approved'),
        );
    }
}

注意: 应用到关联模型的命名空间必须在 CActiveRecord::scopes 中指定。结果,它们不能被参数化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容