数据库之记录集AR

时间:2021-04-11 13:26:11

虽然Yii的DAO可以完成所有的数据库操作,我们还是要花90%的时间来写一些CRUD的操作。如果这些操作跟SQL语句混合使用,将会变得非常难以维护。为了解决这些困难,我们可以使用记录集Active Record。

AR是一个流行的对象关系集的技术。每个AR类代表一个数据库的表(视图),这些表的字段就对应AR的属性,一个AR的实例,就代表数据表中的记录。常用的CRUD操作,一般都采用AR来实现,所以,我们可以用一种更加面向对象的方法来获取数据。例如,我们可以用以下的代码来实现一个记录的插入:

  
 
 
  1. $post=new Post;  
  2. $post->title='sample post';  
  3. $post->content='post body content';  
  4. $post->save(); 

接下来我们将介绍如何创建AR,并用他来执行CRUD操作。

下一节中,我们会为大家展示如何用AR处理关系数据库。现在先简单的说,我们用以下的表作为我们的演示使用。

  
 
 
  1. CREATE TABLE tbl post (  
  2.     id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,  
  3.     title VARCHAR(128) NOT NULL,  
  4.     content TEXT NOT NULL,  
  5.     create time INTEGER NOT NULL  
  6. ); 

注意:AR并不能用来完全解决数据库相关的操作,他最好是用来作为数据库表的模型类,用来做CRUD的简单操作,而不死执行复杂的SQL语句。如果是需要复杂的SQL,那么用DAO来操作。

1.1 建立数据库连接

AR是基于数据库连接来进行数据库相关的操作的。默认情况下,我们假设db是这个应用的数据库连接组件。下面展示一个配置文件作为例子

  
 
 
  1. return array(  
  2.     'components'=>array(  
  3.         'db'=>array(  
  4.             'class'=>'system.db.CDbConnection',  
  5.             'connectionString'=>'sqlite:path/to/dbfile',  
  6.             // turn on schema caching to improve performance  
  7.             // 'schemaCachingDuration'=>3600,  
  8.         ),  
  9.     ),  
  10. ); 

上例中,如果是数据库结构不经常改变,则启用'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表:

  
 
 
  1. class Post extends CActiveRecord  
  2. {  
  3.     public static function model($className= CLASS )  
  4.     {  
  5.         return parent::model($className);  
  6.     }  
  7.     public function tableName()  
  8.     {  
  9.         return 'tbl_post';  
  10.     }  

提示:因为AR类经常会被很多地方用到,所以,我们可以一次引入整个目录。例如,我们所有的AR都是放在protected/models,我们在配置工程的时候就可以:

  
 
 
  1. return array(  
  2.     'import'=>array(  
  3.         'application.models.*',  
  4.     ),  
  5. ); 

默认情况下,AR类的名称就是数据库中的表明。如果不一样的话,就重载tableName()。

TIPS:如果使用了表前缀,那么上例中的tableName()就应该按照下列的实现:

  
 
 
  1. public function tableName()  
  2. {  
  3.     return 'ffpostgg';  

数据表中的字段值,可以通过AR的实例属性来访问,例如:

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

虽然我们从来没有显示的在post中声明title这个属性,我们还是可以使用。这是因为title是表tbl_post的一个字段,CActiveRecord通过PHP的_get()这个神奇的方法,让他变成了AR的属性。如果我们尝试访问一个不存在的字段时,会抛出异常。

AR是基于一个表的好的主键设计。如果表中不存在主键,那么就要求我们重载primaryKey()这个办法,来指定一个主键。

  
 
 
  1. public function primaryKey()  
  2. {  
  3.     return 'id';  
  4.     // For composite primary key, return an array like the following  
  5.     // return array('pk1', 'pk2');  

1.3 创建记录

要想在中插入一行记录,我们就要新建一个AR的对象。设置了相对应字段的属性值之后,调用save()的方法,就插入到数据库中了。

  
 
 
  1. $post=new Post;  
  2. $post->title='sample post';  
  3. $post->content='content for the sample post';  
  4. $post->create time=time();  
  5. $post->save(); 

如果主键是自动增长的,那么在保存之后,会自动加上该主键,并且数值自动增加。上例中,id的属性就会是新的记录集的ID,虽然我们改变他。

如果在表中的某些字段,已经有默认值了,那么在插入时,AR的这些属性就会自动赋予默认值。如果想要更改这些默认值,就需要显示的在代码中设置AR的属性。

在记录被保存到数据库之前,属性可以被设置为CDbExpression类型的值。例如,为了保存Mysql返回的now()值,我们可以用以下的代码实现:

  
 
 
  1. $post=new Post;  
  2. $post->create_time=new CDbExpression('NOW()');  
  3. // $post->create_time='NOW()'; will not work because  
  4. // 'NOW()' will be treated as a string  
  5. $post->save(); 

TIPS:AR虽然让我们可以不用写整片的SQL语句,但是有时候还是想看看数据库到底执行了什么语句。我们可以在Yii的logging feature中开启。例如,我们在项目工程配置中,开启CWebLogRoute,我们就可以在网页端看到所执行的SQL。我们也可以设置CDbConnection::enableParamLogging,这样还可以看到具体的参数值。

1.4 读取记录

要读取数据表的数据,我们调用以下的其中一种:

  
 
 
  1. // find the first row satisfying the specified condition  
  2. $post=Post::model()->find($condition,$params);  
  3. // find the row with the specified primary key  
  4. $post=Post::model()->findByPk($postID,$condition,$params);  
  5. // find the row with the specified attribute values  
  6. $post=Post::model()->findByAttributes($attributes,$condition,$params);  
  7. // find the first row using the specified SQL statement  
  8. $post=Post::model()->findBySql($sql,$params); 

上例中,我们调用Post::model()的find方法,注意,对每个AR类来说,一个静态的model方法是必须的。这个静态方法,返回一个AR实例,用来访问类级别的方法。

如果find方法找到了匹配条件的记录,就返回一个Post的实例。然后我们就可以像访问我们普通类的属性那样访问数据库的值了,例如$post->title。

如果没有找到匹配的记录,find会返回一个空值。

调用find方法的时候,我们用$condition 和$params作为查询的条件。例如:

  
 
 
  1. // find the row with postID=10  
  2. $post=Post::model()->find('postID=:postID'array(':postID'=>10)); 

我们也可以用$condition 作复杂的查询。当然了,复杂的就不是一句话了,我们用CDbCriteria的实例作为$condition ,然后我们就可以设定更多的条件了:

  
 
 
  1. $criteria=new CDbCriteria;  
  2. $criteria->select='title'// only select the 'title' column  
  3. $criteria->condition='postID=:postID';  
  4. $criteria->params=array(':postID'=>10);  
  5. $post=Post::model()->find($criteria); // $params is not needed 

我们用CDbCriteria作为查询条件以后,$params就不需要了。

还有一个替换CDbCriteria的方法是像find提供一个数组参数,这个数组参数中包含查询条件以及对应的值。放例子,不解释

  
 
 
  1. $post=Post::model()->find(array(  
  2.     'select'=>'title',  
  3.     'condition'=>'postID=:postID',  
  4.     'params'=>array(':postID'=>10),  
  5. )); 

TIPS:如果查询条件只是某字段的值等于特定的值,可以使用 findByAttributes()

如果符合查询条件的记录很多,我们可以通过调用findAll这些方法一次性全部读取。

  
 
 
  1. // find all rows satisfying the specified condition  
  2. $posts=Post::model()->findAll($condition,$params);  
  3. // find all rows with the specified primary keys  
  4. $posts=Post::model()->findAllByPk($postIDs,$condition,$params);  
  5. // find all rows with the specified attribute values  
  6. $posts=Post::model()->findAllByAttributes($attributes,$condition,$params);  
  7. // find all rows using the specified SQL statement  
  8. $posts=Post::model()->findAllBySql($sql,$params); 

如果没有符合查询条件的记录,那么以上方法就会放回一个空的数组。这跟find不同的是,find会返回NULL。

除了之前提到的find以及findAll这些方法,以下的方法也是很好用的:

  
 
 
  1. // get the number of rows satisfying the specified condition  
  2. $n=Post::model()->count($condition,$params);  
  3. // get the number of rows using the specified SQL statement  
  4. $n=Post::model()->countBySql($sql,$params);  
  5. // check if there is at least a row satisfying the specified condition  
  6. $exists=Post::model()->exists($condition,$params); 

1.5 更新记录

当AR实例的属性被字段值赋值后,我们可以更改这些属性,可以将这些更新过的属性值保存到数据库中。

  
 
 
  1. $post=Post::model()->findByPk(10);  
  2. $post->title='new post title';  
  3. $post->save(); // save the change to database 

正如我们看到的,不论是更新还是插入新数据,我们都是调用save这个方法。如果AR的对象是new出来的,调用save就是执行插入操作;如果对象是从find或者是findAll得到的,调用save的时候就是更新操作。事实上,我们可以调用CActiveRecord::isNewRecord来判断AR对象是新的与否。

我们也可以在没有装载数据的情况下,更新一条或者是多条记录。AR提供了下列便捷的类方法:

  
 
 
  1. // update the rows matching the specified condition  
  2. Post::model()->updateAll($attributes,$condition,$params);  
  3. // update the rows matching the specified condition and primary key(s)  
  4. Post::model()->updateByPk($pk,$attributes,$condition,$params);  
  5. // update counter columns in the rows satisfying the specified conditions  
  6. Post::model()->updateCounters($counters,$condition,$params); 

$attributes 是一组字段名;
condition 是更新的条件;
$params  参数值;

1.6 删除数据

我们可以用下面的方法来删除数据库记录

  
 
 
  1. $post=Post::model()->findByPk(10); // assuming there is a post whose ID is 10  
  2. $post->delete(); // delete the row from the database table 

注意!!!删除之后,AR的属性什么的都没变,但是数据库记录没啦!!!

下面的这些方法可以不需要加载数据而直接删除:

  
 
 
  1. // delete the rows matching the specified condition  
  2. Post::model()->deleteAll($condition,$params);  
  3. // delete the rows matching the specified condition and primary key(s)  
  4. Post::model()->deleteByPk($pk,$condition,$params); 

1.7 数据验证

在更新或者是插入数据的时候,经常需要根据一些规则来验证字段值师傅符合规范。当这些数据是由终端用户提供的时候,这种校验尤为重要。正常情况下,我们是完全不信任任何用户输入信息的。

AR在用户提交save之后,会自动校验数据。校验规则是AR里的rules()方法设定的。至于具体的规则设定,参考之前就行了。以下是一个典型的工作流:

  
 
 
  1. if($post->save())  
  2. {  
  3.     // data is valid and is successfully inserted/updated  
  4. }  
  5. else 
  6. {  
  7.     // data is invalid. call getErrors() to retrieve error messages  

当更新或者是插入的值,是从终端用户输入的,那么我们要给AR的属性赋值:

  
 
 
  1. $post->title=$ POST['title'];  
  2. $post->content=$ POST['content'];  
  3. $post->save(); 

如果是多个字段一起更新,那么可以采用之前提到的集体赋值:

  
 
 
  1. // assume $ POST['Post'] is an array of column values indexed by column names  
  2. $post->attributes=$ POST['Post'];  
  3. $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的事务功能。

  
 
 
  1. $model=Post::model();  
  2. $transaction=$model->dbConnection->beginTransaction();  
  3. try  
  4. {  
  5.     // find and save are two steps which may be intervened by another request  
  6.     // we therefore use a transaction to ensure consistency and integrity  
  7.     $post=$model->findByPk(10);  
  8.     $post->title='new post title';  
  9.     $post->save();  
  10.     $transaction->commit();  
  11. }  
  12. catch(Exception $e)  
  13. {  
  14.     $transaction->rollBack();  

1.11 命名空间

一个命名空间意味着一个命名的查询条件(query criteria),可以用来连接其他的命名空间,运用于AR查询。

命名空间主要是在CActiveRecord::scopes()里声明,格式为name-criteria。下面的代码定义了两个命名空间:published 和 recently。

  
 
 
  1. class Post extends CActiveRecord  
  2. {  
  3.     ......  
  4.     public function scopes()  
  5.     {  
  6.         return array(  
  7.             'published'=>array(  
  8.                 'condition'=>'status=1',  
  9.             ),  
  10.             'recently'=>array(  
  11.                 'order'=>'create_time DESC',  
  12.                 'limit'=>5,  
  13.             ),  
  14.         );  
  15.     }  

每个命名空间可以用来作为CDbCriteria的实例。比如说,recently这个命名空间,指定了order的属性是create_time DESC,limit的属性值是5;转换成查询条件就是返回最近的5条记录。

命名空间一般是用于作为find方法的变体。许多命名空间可能会被关联起来,得到一个更多筛选条件的结果。例如,查找最近发布的文章,我们可以用以下的方法:

  
 
 
  1. $posts=Post::model()->published()->recently()->findAll(); 

一般来说了,命名空间的右边都有一个find的方法,每个命名空间都提供一个查询规则。作用就好比在查询语句中添加一串过滤器。

参数化命名空间

命名空间也可以参数化。例如,我们想定制recently这个命名空间里的文章数量。我们不在CActiveRecord::scopes里修改方法,换一种做法,我们要定义一个与这个命名空间同名的方法

  
 
 
  1. public function recently($limit=5)  
  2. {  
  3.     $this->getDbCriteria()->mergeWith(array(  
  4.         'order'=>'create time DESC',  
  5.         'limit'=>$limit,  
  6.     ));  
  7.     return $this;  

然后我们就可以通过以下的方法来获取最近的3篇文章(而不是5篇)

  
 
 
  1. $posts=Post::model()->published()->recently(3)->findAll(); 

如果我们不指定参数是3,那么就回去返回默认的5篇。

默认空间

一个模型类可以有一个默认的空间,可以用来做任何查询。例如,一个多语言的网页,只想显示用户当前语言的那部分内容。由于可能会有很多关于这个网页内容的查询,我们通过定义一个默认空间来解决该问题。我们重载CActiveRecord::defaultScope这个方法,

  
 
 
  1. class Content extends CActiveRecord  
  2. {  
  3.     public function defaultScope()  
  4.     {  
  5.         return array(  
  6.             'condition'=>"language='".Yii::app()->language."'",  
  7.         );  
  8.     }  

所以,当下面的方法被调用的时候,就会自动调用上面的默认空间:

  
 
 
  1. $contents=Content::model()->findAll(); 

注意!默认空间以及命名空间只支持SELECT的查询动作,不支持INSERT, UPDATE and DELETE。同样的,当声明一个空间(默认或者命名的),AR类不能用于当前的数据库操作。