虽然Yii的DAO可以完成所有的数据库操作,我们还是要花90%的时间来写一些CRUD的操作。如果这些操作跟SQL语句混合使用,将会变得非常难以维护。为了解决这些困难,我们可以使用记录集Active Record。
AR是一个流行的对象关系集的技术。每个AR类代表一个数据库的表(视图),这些表的字段就对应AR的属性,一个AR的实例,就代表数据表中的记录。常用的CRUD操作,一般都采用AR来实现,所以,我们可以用一种更加面向对象的方法来获取数据。例如,我们可以用以下的代码来实现一个记录的插入:
- $post=new Post;
- $post->title='sample post';
- $post->content='post body content';
- $post->save();
接下来我们将介绍如何创建AR,并用他来执行CRUD操作。
下一节中,我们会为大家展示如何用AR处理关系数据库。现在先简单的说,我们用以下的表作为我们的演示使用。
- CREATE TABLE tbl post (
- id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
- title VARCHAR(128) NOT NULL,
- content TEXT NOT NULL,
- create time INTEGER NOT NULL
- );
注意:AR并不能用来完全解决数据库相关的操作,他最好是用来作为数据库表的模型类,用来做CRUD的简单操作,而不死执行复杂的SQL语句。如果是需要复杂的SQL,那么用DAO来操作。
1.1 建立数据库连接
AR是基于数据库连接来进行数据库相关的操作的。默认情况下,我们假设db是这个应用的数据库连接组件。下面展示一个配置文件作为例子
- return array(
- 'components'=>array(
- 'db'=>array(
- 'class'=>'system.db.CDbConnection',
- 'connectionString'=>'sqlite:path/to/dbfile',
- // turn on schema caching to improve performance
- // 'schemaCachingDuration'=>3600,
- ),
- ),
- );
上例中,如果是数据库结构不经常改变,则启用'schemaCachingDuration' 会使得数据库更高效。
支持AR的数据库系统有:(注意,不是所有数据库都支持啊)
● MySQL 4.1 or later
● PostgreSQL 7.3 or later
● SQLite 2 and 3
● Microsoft SQL Server 2000 or later
● Oracle
如果你不想用db作为数据库句柄,或者是要连接多个数据库,那么需要重写CActiveRecord::getDbConnection()。CActiveRecord类是所有AR类的基类。
Tips:有两个方法让AR操作多个数据库。如果这些数据库的架构是不一样的(我的理解是不同的数据库系统),那么就必须去建立一个AR的基类,实现不同的getDbConnection()方法;而如果都是相同的数据库系统,动态的去改变静态变量CActiveRecord::db就是最好的方法了。
1.2 定义AR类
要访问一个数据库的表,我们首先要定义一个类,这个类继承于CActiveRecort。每个AR类代表一个数据库的表,一个AR对象,则代表数据表中的一条记录。以下代码最简洁的展示了如何让一个AR代表tbl_post表:
- class Post extends CActiveRecord
- {
- public static function model($className= CLASS )
- {
- return parent::model($className);
- }
- public function tableName()
- {
- return 'tbl_post';
- }
- }
提示:因为AR类经常会被很多地方用到,所以,我们可以一次引入整个目录。例如,我们所有的AR都是放在protected/models,我们在配置工程的时候就可以:
- return array(
- 'import'=>array(
- 'application.models.*',
- ),
- );
默认情况下,AR类的名称就是数据库中的表明。如果不一样的话,就重载tableName()。
TIPS:如果使用了表前缀,那么上例中的tableName()就应该按照下列的实现:
- public function tableName()
- {
- return 'ffpostgg';
- }
数据表中的字段值,可以通过AR的实例属性来访问,例如:
- $post=new Post;
- $post->title='a sample post';
虽然我们从来没有显示的在post中声明title这个属性,我们还是可以使用。这是因为title是表tbl_post的一个字段,CActiveRecord通过PHP的_get()这个神奇的方法,让他变成了AR的属性。如果我们尝试访问一个不存在的字段时,会抛出异常。
AR是基于一个表的好的主键设计。如果表中不存在主键,那么就要求我们重载primaryKey()这个办法,来指定一个主键。
- public function primaryKey()
- {
- return 'id';
- // For composite primary key, return an array like the following
- // return array('pk1', 'pk2');
- }
1.3 创建记录
要想在中插入一行记录,我们就要新建一个AR的对象。设置了相对应字段的属性值之后,调用save()的方法,就插入到数据库中了。
- $post=new Post;
- $post->title='sample post';
- $post->content='content for the sample post';
- $post->create time=time();
- $post->save();
如果主键是自动增长的,那么在保存之后,会自动加上该主键,并且数值自动增加。上例中,id的属性就会是新的记录集的ID,虽然我们改变他。
如果在表中的某些字段,已经有默认值了,那么在插入时,AR的这些属性就会自动赋予默认值。如果想要更改这些默认值,就需要显示的在代码中设置AR的属性。
在记录被保存到数据库之前,属性可以被设置为CDbExpression类型的值。例如,为了保存Mysql返回的now()值,我们可以用以下的代码实现:
- $post=new Post;
- $post->create_time=new CDbExpression('NOW()');
- // $post->create_time='NOW()'; will not work because
- // 'NOW()' will be treated as a string
- $post->save();
TIPS:AR虽然让我们可以不用写整片的SQL语句,但是有时候还是想看看数据库到底执行了什么语句。我们可以在Yii的logging feature中开启。例如,我们在项目工程配置中,开启CWebLogRoute,我们就可以在网页端看到所执行的SQL。我们也可以设置CDbConnection::enableParamLogging,这样还可以看到具体的参数值。
1.4 读取记录
要读取数据表的数据,我们调用以下的其中一种:
- // find the first row satisfying the specified condition
- $post=Post::model()->find($condition,$params);
- // find the row with the specified primary key
- $post=Post::model()->findByPk($postID,$condition,$params);
- // find the row with the specified attribute values
- $post=Post::model()->findByAttributes($attributes,$condition,$params);
- // find the first row using the specified SQL statement
- $post=Post::model()->findBySql($sql,$params);
上例中,我们调用Post::model()的find方法,注意,对每个AR类来说,一个静态的model方法是必须的。这个静态方法,返回一个AR实例,用来访问类级别的方法。
如果find方法找到了匹配条件的记录,就返回一个Post的实例。然后我们就可以像访问我们普通类的属性那样访问数据库的值了,例如$post->title。
如果没有找到匹配的记录,find会返回一个空值。
调用find方法的时候,我们用$condition 和$params作为查询的条件。例如:
- // find the row with postID=10
- $post=Post::model()->find('postID=:postID', array(':postID'=>10));
我们也可以用$condition 作复杂的查询。当然了,复杂的就不是一句话了,我们用CDbCriteria的实例作为$condition ,然后我们就可以设定更多的条件了:
- $criteria=new CDbCriteria;
- $criteria->select='title'; // only select the 'title' column
- $criteria->condition='postID=:postID';
- $criteria->params=array(':postID'=>10);
- $post=Post::model()->find($criteria); // $params is not needed
我们用CDbCriteria作为查询条件以后,$params就不需要了。
还有一个替换CDbCriteria的方法是像find提供一个数组参数,这个数组参数中包含查询条件以及对应的值。放例子,不解释
- $post=Post::model()->find(array(
- 'select'=>'title',
- 'condition'=>'postID=:postID',
- 'params'=>array(':postID'=>10),
- ));
TIPS:如果查询条件只是某字段的值等于特定的值,可以使用 findByAttributes()
如果符合查询条件的记录很多,我们可以通过调用findAll这些方法一次性全部读取。
- // find all rows satisfying the specified condition
- $posts=Post::model()->findAll($condition,$params);
- // find all rows with the specified primary keys
- $posts=Post::model()->findAllByPk($postIDs,$condition,$params);
- // find all rows with the specified attribute values
- $posts=Post::model()->findAllByAttributes($attributes,$condition,$params);
- // find all rows using the specified SQL statement
- $posts=Post::model()->findAllBySql($sql,$params);
如果没有符合查询条件的记录,那么以上方法就会放回一个空的数组。这跟find不同的是,find会返回NULL。
除了之前提到的find以及findAll这些方法,以下的方法也是很好用的:
- // get the number of rows satisfying the specified condition
- $n=Post::model()->count($condition,$params);
- // get the number of rows using the specified SQL statement
- $n=Post::model()->countBySql($sql,$params);
- // check if there is at least a row satisfying the specified condition
- $exists=Post::model()->exists($condition,$params);
1.5 更新记录
当AR实例的属性被字段值赋值后,我们可以更改这些属性,可以将这些更新过的属性值保存到数据库中。
- $post=Post::model()->findByPk(10);
- $post->title='new post title';
- $post->save(); // save the change to database
正如我们看到的,不论是更新还是插入新数据,我们都是调用save这个方法。如果AR的对象是new出来的,调用save就是执行插入操作;如果对象是从find或者是findAll得到的,调用save的时候就是更新操作。事实上,我们可以调用CActiveRecord::isNewRecord来判断AR对象是新的与否。
我们也可以在没有装载数据的情况下,更新一条或者是多条记录。AR提供了下列便捷的类方法:
- // update the rows matching the specified condition
- Post::model()->updateAll($attributes,$condition,$params);
- // update the rows matching the specified condition and primary key(s)
- Post::model()->updateByPk($pk,$attributes,$condition,$params);
- // update counter columns in the rows satisfying the specified conditions
- Post::model()->updateCounters($counters,$condition,$params);
$attributes 是一组字段名;
condition 是更新的条件;
$params 参数值;
1.6 删除数据
我们可以用下面的方法来删除数据库记录
- $post=Post::model()->findByPk(10); // assuming there is a post whose ID is 10
- $post->delete(); // delete the row from the database table
注意!!!删除之后,AR的属性什么的都没变,但是数据库记录没啦!!!
下面的这些方法可以不需要加载数据而直接删除:
- // delete the rows matching the specified condition
- Post::model()->deleteAll($condition,$params);
- // delete the rows matching the specified condition and primary key(s)
- Post::model()->deleteByPk($pk,$condition,$params);
1.7 数据验证
在更新或者是插入数据的时候,经常需要根据一些规则来验证字段值师傅符合规范。当这些数据是由终端用户提供的时候,这种校验尤为重要。正常情况下,我们是完全不信任任何用户输入信息的。
AR在用户提交save之后,会自动校验数据。校验规则是AR里的rules()方法设定的。至于具体的规则设定,参考之前就行了。以下是一个典型的工作流:
- if($post->save())
- {
- // data is valid and is successfully inserted/updated
- }
- else
- {
- // data is invalid. call getErrors() to retrieve error messages
- }
当更新或者是插入的值,是从终端用户输入的,那么我们要给AR的属性赋值:
- $post->title=$ POST['title'];
- $post->content=$ POST['content'];
- $post->save();
如果是多个字段一起更新,那么可以采用之前提到的集体赋值:
- // assume $ POST['Post'] is an array of column values indexed by column names
- $post->attributes=$ POST['Post'];
- $post->save();
1.8 数据对比
就像表记录一样,AR的对象也跟主键一样的,有唯一ID。所以,如果比较两个AR对象,如果他们属于同一个AR类,我们仅仅比较他们的主键。一个简单的方法是调用CActiveRecord::equals()。 但是:
TIPS:跟其他框架不同,Yii支持联合主键。联合组建在Yii里面是一个数组的形式。
1.9 自定义
AR类提供了一些虚函数,用来给子类重载,自定义工作流:
● beforeValidate and afterValidate: 在验证之前(之后)调用
● beforeSave and afterSave:
● beforeDelete and afterDelete:
● afterConstruct:
● beforeFind:
● afterFind:
1.10 AR中使用事务
每个AR对象都包含了一个叫做dbConnection的属性,也就是CDbConnection的句柄。所以在我们用AR的时候,可以使用DAO的事务功能。
- $model=Post::model();
- $transaction=$model->dbConnection->beginTransaction();
- try
- {
- // find and save are two steps which may be intervened by another request
- // we therefore use a transaction to ensure consistency and integrity
- $post=$model->findByPk(10);
- $post->title='new post title';
- $post->save();
- $transaction->commit();
- }
- catch(Exception $e)
- {
- $transaction->rollBack();
- }
1.11 命名空间
一个命名空间意味着一个命名的查询条件(query criteria),可以用来连接其他的命名空间,运用于AR查询。
命名空间主要是在CActiveRecord::scopes()里声明,格式为name-criteria。下面的代码定义了两个命名空间:published 和 recently。
- 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 DESC,limit的属性值是5;转换成查询条件就是返回最近的5条记录。
命名空间一般是用于作为find方法的变体。许多命名空间可能会被关联起来,得到一个更多筛选条件的结果。例如,查找最近发布的文章,我们可以用以下的方法:
- $posts=Post::model()->published()->recently()->findAll();
一般来说了,命名空间的右边都有一个find的方法,每个命名空间都提供一个查询规则。作用就好比在查询语句中添加一串过滤器。
参数化命名空间
命名空间也可以参数化。例如,我们想定制recently这个命名空间里的文章数量。我们不在CActiveRecord::scopes里修改方法,换一种做法,我们要定义一个与这个命名空间同名的方法
- public function recently($limit=5)
- {
- $this->getDbCriteria()->mergeWith(array(
- 'order'=>'create time DESC',
- 'limit'=>$limit,
- ));
- return $this;
- }
然后我们就可以通过以下的方法来获取最近的3篇文章(而不是5篇)
- $posts=Post::model()->published()->recently(3)->findAll();
如果我们不指定参数是3,那么就回去返回默认的5篇。
默认空间
一个模型类可以有一个默认的空间,可以用来做任何查询。例如,一个多语言的网页,只想显示用户当前语言的那部分内容。由于可能会有很多关于这个网页内容的查询,我们通过定义一个默认空间来解决该问题。我们重载CActiveRecord::defaultScope这个方法,
- class Content extends CActiveRecord
- {
- public function defaultScope()
- {
- return array(
- 'condition'=>"language='".Yii::app()->language."'",
- );
- }
- }
所以,当下面的方法被调用的时候,就会自动调用上面的默认空间:
- $contents=Content::model()->findAll();
注意!默认空间以及命名空间只支持SELECT的查询动作,不支持INSERT, UPDATE and DELETE。同样的,当声明一个空间(默认或者命名的),AR类不能用于当前的数据库操作。