2014-11-14 五
By youngsterxyf概述
Yii中,对Model层的使用,有两种方式:
- 通过类CDbConnection和CDbCommand来操作
- 使用ORM形式:编写model类继承自抽象类CActiveRecord
第1种方式的示例如下:
<?php
$connection = Yii::app()->db; // 或者Yii::app()->getComponent('db'); $queryResult = $connection->createCommand($sql)->queryRow();
第2种方式中编写的model类可能需要实现方法getDbConnection
、model
、tableName
。
在实现上,第2种方式是基于第1种方式的,即第2种方式的抽象程度更高。Yii没有屏蔽第1种方式,这样能让开发者按需选择。 但我个人并不喜欢这样,两种方式同时存在,会导致应用的model实现稍显混乱。
分析
Yii框架model层的入口为CDbConnection类,该类有很多public的属性可供配置,如connectionString
、username
、password
等。
根据Yii源码阅读笔记 - 组件集成一文可知,组件初始化时会调用init方法。 类CDbConnection的init类实现如下:
public function init()
{
parent::init();
// 属性autoConnect默认为true
if($this->autoConnect)
$this->setActive(true); }
其中调用的setActive方法实现如下:
public function setActive($value)
{
// 当$value为true,而_active为false(表示数据库连接未打开),则打开数据库连接
// 当$value为false, 而_active为true(表示数据库连接已打开),则关闭数据库连接
if($value!=$this->_active)
{ if($value) $this->open(); else $this->close(); } }
方法open实现如下:
/**
* Opens DB connection if it is currently not
* @throws CException if connection fails
*/
protected function open()
{ if($this->_pdo===null) { // 所以需要配置connectionString if(empty($this->connectionString)) throw new CDbException('CDbConnection.connectionString cannot be empty.'); try { Yii::trace('Opening DB connection','system.db.CDbConnection'); // 基于PDO类建立数据库连接(对于某些数据库不使用PDO) $this->_pdo=$this->createPdoInstance(); // 设置数据库连接的一些属性,如字符编码等 $this->initConnection($this->_pdo); // 标志位设置为已打开 $this->_active=true; } catch(PDOException $e) { // 省略 } } }
方法close实现如下:
/**
* Closes the currently active DB connection.
* It does nothing if the connection is already closed.
*/
protected function close()
{ Yii::trace('Closing DB connection','system.db.CDbConnection'); $this->_pdo=null; $this->_active=false; $this->_schema=null; }
open方法中调用的方法createPdoInstance实现如下:
/**
* Creates the PDO instance.
* When some functionalities are missing in the pdo driver, we may use
* an adapter class to provide them.
* @throws CDbException when failed to open DB connection
* @return PDO the pdo instance */ protected function createPdoInstance() { // 属性pdoClass默认为PDO $pdoClass=$this->pdoClass; if(($pos=strpos($this->connectionString,':'))!==false) { // 取出数据库驱动类型 $driver=strtolower(substr($this->connectionString,0,$pos)); if($driver==='mssql' || $driver==='dblib') $pdoClass='CMssqlPdoAdapter'; elseif($driver==='sqlsrv') $pdoClass='CMssqlSqlsrvPdoAdapter'; } if(!class_exists($pdoClass)) throw new CDbException(Yii::t('yii','CDbConnection is unable to find PDO class "{className}". Make sure PDO is installed correctly.', array('{className}'=>$pdoClass))); // 实例化类PDO,可能失败 @$instance=new $pdoClass($this->connectionString,$this->username,$this->password,$this->_attributes); if(!$instance) throw new CDbException(Yii::t('yii','CDbConnection failed to open the DB connection.')); return $instance; }
从中可以看出,对于MySQL数据库而言,方法createPdoInstance返回的是一个PDO对象,赋值给CDbConnection对象的_pdo属性。
方法initConnection的实现如下:
/**
* Initializes the open db connection.
* This method is invoked right after the db connection is established.
* The default implementation is to set the charset for MySQL and PostgreSQL database connections.
* @param PDO $pdo the PDO instance
*/ protected function initConnection($pdo) { // 设置数据库连接的一些属性 $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); if($this->emulatePrepare!==null && constant('PDO::ATTR_EMULATE_PREPARES')) $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,$this->emulatePrepare); if($this->charset!==null) { $driver=strtolower($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)); if(in_array($driver,array('pgsql','mysql','mysqli'))) $pdo->exec('SET NAMES '.$pdo->quote($this->charset)); } // 执行一些初始化的SQL语句 // public $initSQLs : array list of SQL statements that should be executed right after the DB connection is established. if($this->initSQLs!==null) { foreach($this->initSQLs as $sql) $pdo->exec($sql); } }
由于第2种方式是基于第1中方式实现的,所以我们先看看第1种方式的实现。
类CDbConnection中方法createCommand的实现如下:
public function createCommand($query=null)
{
$this->setActive(true);
return new CDbCommand($this,$query);
}
其中实例化的类CDbCommand的构造方法实现如下:
/**
* Constructor.
* @param CDbConnection $connection the database connection
* @param mixed $query the DB query to be executed. This can be either
* a string representing a SQL statement, or an array whose name-value pairs
* will be used to set the corresponding properties of the created command object. * * For example, you can pass in either <code>'SELECT * FROM tbl_user'</code> * or <code>array('select'=>'*', 'from'=>'tbl_user')</code>. They are equivalent * in terms of the final query result. * * When passing the query as an array, the following properties are commonly set: * {@link select}, {@link distinct}, {@link from}, {@link where}, {@link join}, * {@link group}, {@link having}, {@link order}, {@link limit}, {@link offset} and * {@link union}. Please refer to the setter of each of these properties for details * about valid property values. This feature has been available since version 1.1.6. * * Since 1.1.7 it is possible to use a specific mode of data fetching by setting * {@link setFetchMode FetchMode}. See {@link http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php} * for more details. */ public function __construct(CDbConnection $connection,$query=null) { $this->_connection=$connection; if(is_array($query)) { foreach($query as $name=>$value) $this->$name=$value; } else $this->setText($query); }
可以看到参数$query并不是必须提供,如果提供,则$query参数值可以是字符串也可以是数组。如果是字符串也就是一个原生的SQL语句(可能有参数需要填充),如果是数组, 则可以为CDbCommand对象的select、distinct、from等属性赋值。
方法setText实现如下:
/**
* Specifies the SQL statement to be executed.
* Any previous execution will be terminated or cancel.
* @param string $value the SQL statement to be executed
* @return CDbCommand this command instance
*/ public function setText($value) { if($this->_connection->tablePrefix!==null && $value!='') $this->_text=preg_replace('/{{(.*?)}}/',$this->_connection->tablePrefix.'\1',$value); else $this->_text=$value; // 因为是要新执行一条语句,所以需要重置状态,将属性_statement置为null。 $this->cancel(); return $this; }
如果调用类CDbConnection的createCommand方法时提供了$query
参数值,则在得到CDbCommand对象后,即可调用CDbCommand对象的execute方法(对于增、删、改操作)或query、queryAll、queryRow等方法(对于查询操作)来执行数据库操作。
如果调用createCommand时未提供$query
参数值,则有3种方式来完成数据库操作:
-
对于普通的数据库查询操作,得到CDbCommand对象后,链式调用方法select、from、where等(之所以能够链式调用,是因为这些方法的最后都返回了对象本身),并且链式调用的最后调用query一类方法来执行数据库操作。
-
对于会对数据表结构或数据产生变更的操作,得到CDbCommand对象后,可以直接调用方法insert、update、delete、createTable等来执行操作。
-
可以在得到CDbCommand对象后,调用方法setText来设置待执行的SQL语句,setText方法的$value参数类型应为字符串。如果调用setText方法前$value参数类型是关联数组,则可以先调用方法buildQuery从关联数组生成一个SQL语句字符串,再调用setText方法;或者直接调用setSelect、setFrom、setWhere等方法来设置SQL语句的各个组成部分。最后调用execute方法或query一类方法。可以看出相比传递
$query
参数,这种方式只不过是显式地设置SQL语句。通常这是不推荐的(搞这么麻烦,何必呢?呵呵)。
这样一看,类CDbCommand的实现稍显混乱。本意上额外的第3种方式应该是提供给类CActiveRecord使用的,所以不建议使用。
接下来看看方法execute方法及query一类(仅以方法query、queryAll为例)方法的实现:
/**
* Executes the SQL statement.
* This method is meant only for executing non-query SQL statement.
* No result set will be returned.
* @param array $params input parameters (name=>value) for the SQL execution. This is an alternative
* to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing * them in this way can improve the performance. Note that if you pass parameters in this way, * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. * Please also note that all values are treated as strings in this case, if you need them to be handled as * their real data types, you have to use {@link bindParam} or {@link bindValue} instead. * @return integer number of rows affected by the execution. * @throws CDbException execution failed */ public function execute($params=array()) { if($this->_connection->enableParamLogging && ($pars=array_merge($this->_paramLog,$params))!==array()) { $p=array(); foreach($pars as $name=>$value) $p[$name]=$name.'='.var_export($value,true); $par='. Bound with ' .implode(', ',$p); } else $par=''; Yii::trace('Executing SQL: '.$this->getText().$par,'system.db.CDbCommand'); try { if($this->_connection->enableProfiling) Yii::beginProfile('system.db.CDbCommand.execute('.$this->getText().$par.')','system.db.CDbCommand.execute'); // 以通过setText设置的SQL语句为参数间接调用PDO对象的prepare方法,并将返回的PDOStatement对象赋值给当前CDbCommand对象的_statement属性。 $this->prepare(); if($params===array()) // 无参执行 $this->_statement->execute(); else // 带参执行 $this->_statement->execute($params); // 操作影响的数据表行数 $n=$this->_statement->rowCount(); if($this->_connection->enableProfiling) Yii::endProfile('system.db.CDbCommand.execute('.$this->getText().$par.')','system.db.CDbCommand.execute'); return $n; } catch(Exception $e) { // 省略 } } public function query($params=array()) { return $this->queryInternal('',0,$params); } public function queryAll($fetchAssociative=true,$params=array()) { return $this->queryInternal('fetchAll',$fetchAssociative ? $this->_fetchMode : PDO::FETCH_NUM, $params); }
可以看到query一类方法都是间接调用方法queryInternal来完成操作的。queryInternal方法实现如下:
private function queryInternal($method,$mode,$params=array())
{
$params=array_merge($this->params,$params);
if($this->_connection->enableParamLogging && ($pars=array_merge($this->_paramLog,$params))!==array())
{
$p=array(); foreach($pars as $name=>$value) $p[$name]=$name.'='.var_export($value,true); $par='. Bound with '.implode(', ',$p); } else $par=''; Yii::trace('Querying SQL: '.$this->getText().$par,'system.db.CDbCommand'); // 先尝试从缓存中读取查询结果 // 这里涉及CDbConnection类的三个属性queryCachingCount、queryCachingDuration、queryCacheID // 另外对于方法query(调用queryInternal时提供的method参数为空字符串)的操作不会缓存 if($this->_connection->queryCachingCount>0 && $method!=='' && $this->_connection->queryCachingDuration>0 && $this->_connection->queryCacheID!==false && ($cache=Yii::app()->getComponent($this->_connection->queryCacheID))!==null) { $this->_connection->queryCachingCount--; $cacheKey='yii:dbquery'.$this->_connection->connectionString.':'.$this->_connection->username; $cacheKey.=':'.$this->getText().':'.serialize(array_merge($this->_paramLog,$params)); if(($result=$cache->get($cacheKey))!==false) { Yii::trace('Query result found in cache','system.db.CDbCommand'); return $result[0]; } } try { if($this->_connection->enableProfiling) Yii::beginProfile('system.db.CDbCommand.query('.$this->getText().$par.')','system.db.CDbCommand.query'); $this->prepare(); if($params===array()) $this->_statement->execute(); else $this->_statement->execute($params); // $method对应PDOStatement的结果获取方法,如果未提供$method,自然无法直接通过PDOStatement获取查询结果。 if($method==='') // 这个细看一下 $result=new CDbDataReader($this); else { $mode=(array)$mode; call_user_func_array(array($this->_statement, 'setFetchMode'), $mode); // 调用PDOStatement对应的方法 $result=$this->_statement->$method(); $this->_statement->closeCursor(); } if($this->_connection->enableProfiling) Yii::endProfile('system.db.CDbCommand.query('.$this->getText().$par.')','system.db.CDbCommand.query'); // 如果设置了$cache和$cacheKey // 将查询结果存入缓存 if(isset($cache,$cacheKey)) $cache->set($cacheKey, array($result), $this->_connection->queryCachingDuration, $this->_connection->queryCachingDependency); return $result; } catch(Exception $e) { // 省略 } }
可以看到query方法调用queryInternal时,结果是通过CDbDataReader对象来获取的。类CDbDataReader的构造方法实现如下:
public function __construct(CDbCommand $command)
{
$this->_statement=$command->getPdoStatement();
$this->_statement->setFetchMode(PDO::FETCH_ASSOC);
}
得到CDbDataReader对象,即可通过它的read一类方法迭代获取查询结果。这些方法实际上是调用PDOStatement的fetch一类方法来获取结果的。
再来看看Yii框架Model层的第2种使用方式。
CActiveRecord的用法是,对于数据库的每个数据表,创建一个model类,如UserModel,这个类继承自CActiveRecord类。model类名可以和数据表名一致,也可以不一致,如果不一致,则需要重写CActiveRecord类的tableName方法,标明该model类对应的数据表名。方法tableName默认的实现如下:
/**
* Returns the name of the associated database table.
* By default this method returns the class name as the table name.
* You may override this method if the table is not named after this convention.
* @return string the table name
*/ public function tableName() { return get_class($this); }
假设UserModel类对应的数据表名为User,则应如下重写tableName:
public function tableName()
{
return 'User';
}
使用CActiveRecord方式,若想向数据表中插入一条新纪录,则需要实例化当前model类。以UserModel类为例,若想向User表中插入一条新纪录,则需要先实例化UserModel,得到一个对象,然后对该对象的属性进行赋值指明对应数据表新记录每个字段的值。对象的属性名即数据表的字段名,赋值完毕,调用save或insert即向数据表中存入该新纪录。
CActiveRecord类的构造方法如下所示:
public function __construct($scenario='insert')
{
if($scenario===null) // internally used by populateRecord() and model()
return;
$this->setScenario($scenario);
$this->setIsNewRecord(true); $this->_attributes=$this->getMetaData()->attributeDefaults; $this->init(); $this->attachBehaviors($this->behaviors()); $this->afterConstruct(); }
$scenario='insert',表示新实例化的对象处于待插入数据表的状态,在调用save等方法时,会检测该状态。在新实例化的对象插入到数据表后,该状态立即会变为"update",表示之后对该对象的数据库写入操作,属于update操作。
CActiveRecord类的save方法的实现如下:
public function save($runValidation=true,$attributes=null)
{
// 若需要则对某些属性做校验
if(!$runValidation || $this->validate($attributes))
// 如果是新纪录,则插入,否则更新
return $this->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes); else return false; }
而insert、update方法的实现如下所示:
public function insert($attributes=null)
{
if(!$this->getIsNewRecord())
throw new CDbException(Yii::t('yii','The active record cannot be inserted to database because it is not new.'));
if($this->beforeSave())
{ Yii::trace(get_class($this).'.insert()','system.db.ar.CActiveRecord'); // ... $builder=$this->getCommandBuilder(); // ... $table=$this->getMetaData()->tableSchema; $command=$builder->createInsertCommand($table,$this->getAttributes($attributes)); if($command->execute()) { $primaryKey=$table->primaryKey; if($table->sequenceName!==null) { if(is_string($primaryKey) && $this->$primaryKey===null) $this->$primaryKey=$builder->getLastInsertID($table); elseif(is_array($primaryKey)) { foreach($primaryKey as $pk) { if($this->$pk===null) { $this->$pk=$builder->getLastInsertID($table); break; } } } } $this->_pk=$this->getPrimaryKey(); $this->afterSave(); $this->setIsNewRecord(false); $this->setScenario('update'); return true; } } return false; } public function update($attributes=null) { if($this->getIsNewRecord()) throw new CDbException(Yii::t('yii','The active record cannot be updated because it is new.')); if($this->beforeSave()) { Yii::trace(get_class($this).'.update()','system.db.ar.CActiveRecord'); if($this->_pk===null) $this->_pk=$this->getPrimaryKey(); // 间接调用updateByPk来完成操作 $this->updateByPk($this->getOldPrimaryKey(),$this->getAttributes($attributes)); $this->_pk=$this->getPrimaryKey(); $this->afterSave(); return true; } else return false; }
insert方法中调用的getCommandBuilder方法实现如下:
public function getCommandBuilder()
{
return $this->getDbConnection()->getSchema()->getCommandBuilder();
}
其中getDbConnection方法实现如下:
public function getDbConnection()
{
if(self::$db!==null)
return self::$db;
else
{ self::$db=Yii::app()->getDb(); if(self::$db instanceof CDbConnection) return self::$db; else throw new CDbException(Yii::t('yii','Active Record requires a "db" CDbConnection application component.')); } }
Yii中默认的DB组件名为db,所以如果你使用的是默认的db数据库,那么不用重写这个方法。否则,需要重写该方法,指明需要使用的数据库连接。
getDbConnection方法返回的是一个CDbConnection对象,其getSchema方法实现如下:
public function getSchema()
{
if($this->_schema!==null)
return $this->_schema;
else
{ // 返回数据库配置信息connectionString字段中的数据库驱动类型,如mysql $driver=$this->getDriverName(); /* driverMap属性的定义: public $driverMap=array( 'pgsql'=>'CPgsqlSchema', // PostgreSQL 'mysqli'=>'CMysqlSchema', // MySQL 'mysql'=>'CMysqlSchema', // MySQL 'sqlite'=>'CSqliteSchema', // sqlite 3 'sqlite2'=>'CSqliteSchema', // sqlite 2 'mssql'=>'CMssqlSchema', // Mssql driver on windows hosts 'dblib'=>'CMssqlSchema', // dblib drivers on linux (and maybe others os) hosts 'sqlsrv'=>'CMssqlSchema', // Mssql 'oci'=>'COciSchema', // Oracle driver ); */ if(isset($this->driverMap[$driver])) // 加载对应的数据库驱动组件 return $this->_schema=Yii::createComponent($this->driverMap[$driver], $this); else throw new CDbException(Yii::t('yii','CDbConnection does not support reading schema for {driver} database.', array('{driver}'=>$driver))); } }
以mysql的CMysqlSchema为例,Yii::createComponent($this->driverMap[$driver], $this)
一句会实例化yii/framework/db/schema/mysql/CMysqlSchema.php中定义的CMysqlSchema类。该类自身无构造方法,继承自抽象类CDbSchema,CDbSchema的构造方法实现如下:
public function __construct($conn)
{
// 保存着当前CDbConnection数据库连接对象
$this->_connection=$conn;
foreach($conn->schemaCachingExclude as $name)
$this->_cacheExclude[$name]=true; }
得到CMysqlSchema对象后,调用其getCommandBuilder方法,该方法定义于类CDbSchema中,实现如下:
public function getCommandBuilder()
{
if($this->_builder!==null)
return $this->_builder;
else
return $this->_builder=$this->createCommandBuilder(); }
其中调用的方法createCommandBuilder实现如下:
protected function createCommandBuilder()
{
return new CDbCommandBuilder($this);
}
实例化的CDbCommandBuilder类的构造方法如下所示:
public function __construct($schema)
{
$this->_schema=$schema;
$this->_connection=$schema->getDbConnection();
}
这样insert方法中调用的getCommandBuilder最终返回了一个CDbCommandBuilder对象。
而insert方法中$table=$this->getMetaData()->tableSchema
一句调用的getMetaData方法实现如下:
public function getMetaData()
{
$className=get_class($this);
if(!array_key_exists($className,self::$_md))
{
self::$_md[$className]=null; // preventing recursive invokes of {@link getMetaData()} via {@link __get()} self::$_md[$className]=new CActiveRecordMetaData($this); } return self::$_md[$className]; }
其中实例化的类CActiveRecordMetaData,构造方法如下所示:
public function __construct($model)
{
// 当前model类名
$this->_modelClassName=get_class($model);
// 得到表名
$tableName=$model->tableName(); // 调用_schema(以mysql为例,其值为CMysqlSchema对象)的getTable方法 if(($table=$model->getDbConnection()->getSchema()->getTable($tableName))===null) throw new CDbException(Yii::t('yii','The table "{table}" for active record class "{class}" cannot be found in the database.', array('{class}'=>$this->_modelClassName,'{table}'=>$tableName))); if($table->primaryKey===null) { $table->primaryKey=$model->primaryKey(); if(is_string($table->primaryKey) && isset($table->columns[$table->primaryKey])) $table->columns[$table->primaryKey]->isPrimaryKey=true; elseif(is_array($table->primaryKey)) { foreach($table->primaryKey as $name) { if(isset($table->columns[$name])) $table->columns[$name]->isPrimaryKey=true; } } } // 将数据表结构信息存于属性tableSchema $this->tableSchema=$table; $this->columns=$table->columns; foreach($table->columns as $name=>$column) { if(!$column->isPrimaryKey && $column->defaultValue!==null) $this->attributeDefaults[$name]=$column->defaultValue; } foreach($model->relations() as $name=>$config) { $this->addRelation($name,$config); } }
其中调用的getTable方法,定义于抽象类CDbSchema中,实现如下:
public function getTable($name,$refresh=false)
{
if($refresh===false && isset($this->_tables[$name]))
return $this->_tables[$name];
else
{ if($this->_connection->tablePrefix!==null && strpos($name,'{{')!==false) $realName=preg_replace('/\{\{(.*?)\}\}/',$this->_connection->tablePrefix.'$1',$name); else $realName=$name; // temporarily disable query caching if($this->_connection->queryCachingDuration>0) { $qcDuration=$this->_connection->queryCachingDuration; $this->_connection->queryCachingDuration=0; } // 先尝试从缓存中取数据表结构信息 // CDbConnection类的schemaCachingDuration属性默认为0,如果不配置该属性,那么就不会使用缓存,那么每次增、删、改、查操作都需要loadTable, // 对数据库的压力,以及性能影响是不是很大?!但如果加了缓存,那么当对数据表的结构做变更时会不会有问题? if(!isset($this->_cacheExclude[$name]) && ($duration=$this->_connection->schemaCachingDuration)>0 && $this->_connection->schemaCacheID!==false && ($cache=Yii::app()->getComponent($this->_connection->schemaCacheID))!==null) { $key='yii:dbschema'.$this->_connection->connectionString.':'.$this->_connection->username.':'.$name; $table=$cache->get($key); // 没取到或者需要刷新缓存,则从数据库获取,并更新缓存 if($refresh===true || $table===false) { $table=$this->loadTable($realName); if($table!==null) $cache->set($key,$table,$duration); } $this->_tables[$name]=$table; } else // 直接从数据库获取数据表结构信息 $this->_tables[$name]=$table=$this->loadTable($realName); if(isset($qcDuration)) // re-enable query caching $this->_connection->queryCachingDuration=$qcDuration; return $table; } }
其中调用的loadTable方法最终是通过执行SQL语句:
'SHOW FULL COLUMNS FROM '.$table->rawName
'SHOW CREATE TABLE '.$table->rawName
来获取数据表信息,并将信息存于一个CMysqlColumnSchema对象中(以mysql为例)。
insert方法中$command=$builder->createInsertCommand($table,$this->getAttributes($attributes))
一句createInsertCommand方法定义于CDbCommandBuilder类中,实现如下:
public function createInsertCommand($table,$data)
{
$this->ensureTable($table);
$fields=array();
$values=array();
$placeholders=array(); $i=0; foreach($data as $name=>$value) { if(($column=$table->getColumn($name))!==null && ($value!==null || $column->allowNull)) { $fields[]=$column->rawName; if($value instanceof CDbExpression) { $placeholders[]=$value->expression; foreach($value->params as $n=>$v) $values[$n]=$v; } else { $placeholders[]=self::PARAM_PREFIX.$i; $values[self::PARAM_PREFIX.$i]=$column->typecast($value); $i++; } } } if($fields===array()) { $pks=is_array($table->primaryKey) ? $table->primaryKey : array($table->primaryKey); foreach($pks as $pk) { $fields[]=$table->getColumn($pk)->rawName; $placeholders[]=$this->getIntegerPrimaryKeyDefaultValue(); } } $sql="INSERT INTO {$table->rawName} (".implode(', ',$fields).') VALUES ('.implode(', ',$placeholders).')'; $command=$this->_connection->createCommand($sql); foreach($values as $name=>$value) $command->bindValue($name,$value); return $command; }
另外还有与update、delete等操作对应的方法createUpdateCommand、createDeleteCommand等。
update方法与insert方法的逻辑基本是一致的。
CActiveRecord中所有数据库增、删、改、查操作,在构建出目标sql语句后,都是调用CDbConnection类的方法createCommand来得到一个CDbCommand类的对象,最后调用该对象的execute、query、queryAll等方法来完成查询获取结果。
对于model类实例对象的属性赋值,依赖于CActiveRecord类的方法__set,实现如下:
public function __set($name,$value)
{
if($this->setAttribute($name,$value)===false)
{
if(isset($this->getMetaData()->relations[$name]))
$this->_related[$name]=$value; else parent::__set($name,$value); } }
其中调用的setAttribute方法的实现如下:
public function setAttribute($name,$value)
{
if(property_exists($this,$name))
$this->$name=$value;
elseif(isset($this->getMetaData()->columns[$name]))
$this->_attributes[$name]=$value; else return false; return true; }
基于CActiveRecord类,如果想进行读取操作,那么子类需要重写model方法。CActiveRecord类的model方法实现如下:
/**
* Returns the static model of the specified AR class.
* The model returned is a static instance of the AR class.
* It is provided for invoking class-level methods (something similar to static class methods.)
*
* EVERY derived AR class must override this method as follows,
* <pre>
* public static function model($className=__CLASS__)
* {
* return parent::model($className);
* }
* </pre>
*
* @param string $className active record class name.
* @return CActiveRecord active record model instance.
*/
public static function model($className=__CLASS__)
{
if(isset(self::$_models[$className]))
return self::$_models[$className];
else
{
$model=self::$_models[$className]=new $className(null);
$model->attachBehaviors($model->behaviors());
return $model;
}
}
根据该方法的注释可知道,所有的子类必须如下重写model方法:
public static function model($className = __CLASS__)
{
return parent::model($className);
}
为什么必须重写model方法呢?因为__CLASS__
指的并不是当前对象所属的类,而是方法定义所在的类。
隔壁的哥们告诉我,在 PHP 5.3 之后,如果CActiveRecord的model方法如下实现,就可以不用这样使用需要重写了。
public static function model()
{
$className = get_called_class();
if(isset(self::$_models[$className]))
return self::$_models[$className];
else { $model=self::$_models[$className]=new $className(null); $model->attachBehaviors($model->behaviors()); return $model; } }
在得到$model后,就可以调用对象的query、find、findAll、findByPk、findAllByPk、findByAttributes、findAllByAttributes、findBySql、findAllBySql等方法来查询数据。其中find、findAll、findByPk、findAllByPk、findByAttributes、findAllByAttributes最终是通过调用query方法来实现查询的。query方法的第一个参数是一个CDbCriteria类实例对象,这就意味着调用query方法来实现查询的方法需要根据参数实例化一个CDbCriteria类对象。如find方法实现如下:
/**
* Finds a single active record with the specified condition.
* @param mixed $condition query condition or criteria.
* If a string, it is treated as query condition (the WHERE clause);
* If an array, it is treated as the initial values for constructing a {@link CDbCriteria} object;
* Otherwise, it should be an instance of {@link CDbCriteria}. * @param array $params parameters to be bound to an SQL statement. * This is only used when the first parameter is a string (query condition). * In other cases, please use {@link CDbCriteria::params} to set parameters. * @return CActiveRecord the record found. Null if no record is found. */ public function find($condition='',$params=array()) { Yii::trace(get_class($this).'.find()','system.db.ar.CActiveRecord'); // 实例化一个CDbCriteria对象 $criteria=$this->getCommandBuilder()->createCriteria($condition,$params); return $this->query($criteria); }
而query方法的实现如下所示:
protected function query($criteria,$all=false)
{
$this->beforeFind();
$this->applyScopes($criteria);
if(empty($criteria->with))
{ if(!$all) $criteria->limit=1; // createFindCommand在上面提过 $command=$this->getCommandBuilder()->createFindCommand($this->getTableSchema(),$criteria,$this->getTableAlias()); return $all ? $this->populateRecords($command->queryAll(), true, $criteria->index) : $this->populateRecord($command->queryRow()); } else { $finder=$this->getActiveFinder($criteria->with); return $finder->query($criteria,$all); } }
对于查询操作,很多时候需要多表关联查询。那么基于CActiveRecord如何实现多表关联查询(隐式自动地)呢?
CActiveRecord类有个方法relations():
public function relations()
{
return array();
}
这个方法的注释非常长,说明如何通过该方法声明当前model对应的数据表有哪些关联数据表,是何种关联关系。继承自CActiveRecord类的model类若想使用隐式的多表关联查询,则需要重写该方法。
举例来说,有数据表UserContacts、Users,UserContacts中有外键字段user_id关联到Users。如果基于CActiveRecord在查询UserContacts记录时,需要便捷地获取关联Users的记录。那么可以在UserContacts数据表对应的model类中,这样重写relations方法:
public function relations()
{
return array(
'user' => array(self::BELONGS_TO, 'Users', 'user_id'),
);
}
那么在得到一条记录对象($uc)时,通过调用$uc->user,即可得到与该UserContacts记录关联的Users记录。不过这条关联记录的获取可能是在调用$uc->user时才去查询数据库的。
若想提前将关联记录查询出来准备好,则可以再调用find、findAll等查询方法之前先调用with方法,如self::model()->with('user')->find(array('usercontact_id' => 1))
,或者这样调用find方法self::model()->find(array('usercontact_id' => 1, 'with' => 'user'))
。
那么在调用$uc->user
时,如何知道user是一个关联项,而不是一个当前对象的属性?如果当前对象对应的数据表已有一个名为user的字段,是否会屏蔽掉关联项?且看CActiveRecord类的__get方法:
public function __get($name)
{
// 先查看当前model类对应的数据表是否有名为$name的字段
if(isset($this->_attributes[$name]))
return $this->_attributes[$name];
elseif(isset($this->getMetaData()->columns[$name])) return null; // 查看是否有名为$name的关联项 elseif(isset($this->_related[$name])) return $this->_related[$name]; elseif(isset($this->getMetaData()->relations[$name])) return $this->getRelated($name); else return parent::__get($name); }
注:本文为草稿状态