源码分析|详解数据库操作方法都是如何执行的
作为全网最详细的框架底层源码分析文章,我们怎么可能会没有数据库方法的详细解释呢?
在上一篇文章 中,我们比较详细的分析了数据库连接和使用的方法,那么,对于一个封装好的数据库方法,底层究竟是如何解析和执行呢?在今天的文章中,我们将会揭开数据库封装方法的神秘面纱。
正如,我之前说的那种,精通了一个框架的底层,我们就可以很轻松的去研究其他框架了,因为很多WEB框架,底层都是差不多的,你精通了一个, 就可以举一反三。
在之前的文章中,我们知道Yii框架封装数据库方法的是CActiveRecord类,那么我们可以看看这个类都提供了哪些方法呢?
tableName()
primaryKey()
relations()
getAttributeLabel()
getDbConnection()
getActiveRelation()
getTableSchema()
getCommandBuilder()
hasAttribute()
getAttribute()
setAttribute()
addRelatedRecord()
getAttributes()
save()
insert()
update()
saveCounters()
delete()
getPrimaryKey()
query()
find()
findAll()
findByPk()
findAllByPk()
findByAttributes()
findAllByAttributes()
findBySql()
findAllBySql()
count()
countByAttributes()
countBySql()
exists()
updateByPk()
updateAll()
updateCounters()
deleteByPk()
deleteAll()
deleteAllByAttributes()
// 数据库基本操作
// 插入数据到users表中
insert into users(`user_name`,`user_id`) values("mamba1",1234567);
// 查询数据从users表
select id,user_name,user_id from users where id = 1;
// 更新users表数据
update users set user_name = 'mamba' where id = 1;
// 删除数据从users表
delete from users where id = ;
// 统计数量
select count(*) from users;
上面是几个数据表的基础操作,包括增删改查统计等SQL,而我们的Yii框架的CActiveRecord类中的方法,就是拼接出这样的SQL,然后交给PDO去执行SQL,我们主要分析的就是这个拼接和执行的过程,我们可以举例说明:
我们使用updateAll方法来详细分析一下:
public static function updateInfo(){
$con = self::model();
$ret = $con->updateAll(
array("user_name" => "mamba3"), // column
"id=1"
);
return $ret;
}
我们操作了CActiveRecord类中的updateAll()方法,然后我们看看这个方法都做了哪些操作:
public function updateAll($attributes,$condition='',$params=array())
{
Yii::trace(get_class($this).'.updateAll()','system.db.ar.CActiveRecord');
$builder=$this->getCommandBuilder();
$criteria=$builder->createCriteria($condition,$params);
$command=$builder->createUpdateCommand($this->getTableSchema(),$attributes,$criteria);
return $command->execute();
}
这个方法有三个参数和四个重要的操作:
$attributes
$condition=''
$params=array()
$attributes:要更新的属性列表,也就是要更新的字段列表,这是一个key->value类型的数组类型,是我们set user_name = "mamba"中的 user_name = "mamba"
$conditions:这是where条件中的参数,这是一个字符串类型
$params:使用占位符进行查询,这是一个数组类型
这三个参数非常重要,需要在使用的时候,注意入参都是什么,我们继续往下看,这几个参数都用在了什么地方
$builder=$this->getCommandBuilder();
$builder是数据库连接对象,我们看下面的代码,具体做了什么事情
public function getCommandBuilder()
{
return $this->getDbConnection()->getSchema()->getCommandBuilder();
}
// 返回数据库连接对象
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.'));
}
}
// CDbConnection类
// 选择数据库驱动返回一个schema对象
public function getSchema()
{
if($this->_schema!==null)
return $this->_schema;
else
{
$driver=$this->getDriverName();
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)));
}
}
// 返回数据库连接
public function getCommandBuilder()
{
if($this->_builder!==null)
return $this->_builder;
else
return $this->_builder=$this->createCommandBuilder();
}
// 创建一个数据CommandBuilder对象
protected function createCommandBuilder()
{
return new CDbCommandBuilder($this);
}
// CDbCommandBuilder类是提供了为表创建查询命令的基本方法
重要结论:$builder是类CDbCommandBuilder的实例
$criteria=$builder->createCriteria($condition,$params);
我们再看看createCriteria()方法是干什么的
public function createCriteria($condition='',$params=array())
{
if(is_array($condition))
$criteria=new CDbCriteria($condition);
elseif($condition instanceof CDbCriteria)
$criteria=clone $condition;
else
{
$criteria=new CDbCriteria;
$criteria->condition=$condition;
$criteria->params=$params;
}
return $criteria;
}
$criteria是返回一个CDbCriteris对象,在实例化的时候,将condition里面的参数赋值给了CDbCriteris的属性。
public function __construct($data=array())
{
foreach($data as $name=>$value)
$this->$name=$value;
}
$command=$builder->createUpdateCommand($this->getTableSchema(),
$attributes,$criteria)
接下来我们看看$command变量是如何得来的,可以看出来这就是一个拼接SQL的过程,这个SQL凭借出来了
// 创建一个更新的SQL
public function createUpdateCommand($table,$data,$criteria)
{
$this->ensureTable($table);
$fields=array();
$values=array();
$bindByPosition=isset($criteria->params[0]);
$i=0;
foreach($data as $name=>$value)
{
if(($column=$table->getColumn($name))!==null)
{
if($value instanceof CDbExpression)
{
$fields[]=$column->rawName.'='.$value->expression;
foreach($value->params as $n=>$v)
$values[$n]=$v;
}
elseif($bindByPosition)
{
$fields[]=$column->rawName.'=?';
$values[]=$column->typecast($value);
}
else
{
$fields[]=$column->rawName.'='.self::PARAM_PREFIX.$i;
$values[self::PARAM_PREFIX.$i]=$column->typecast($value);
$i++;
}
}
}
if($fields===array())
throw new CDbException(Yii::t('yii','No columns are being updated for table "{table}".',
array('{table}'=>$table->name)));
$sql="UPDATE {$table->rawName} SET ".implode(', ',$fields);
$sql=$this->applyJoin($sql,$criteria->join);
$sql=$this->applyCondition($sql,$criteria->condition);
$sql=$this->applyOrder($sql,$criteria->order);
$sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset);
$command=$this->_connection->createCommand($sql);
$this->bindValues($command,array_merge($values,$criteria->params));
return $command;
}
return $command->execute();
// 执行SQL
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');
$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)
{
if($this->_connection->enableProfiling)
Yii::endProfile('system.db.CDbCommand.execute('.$this->getText().$par.')','system.db.CDbCommand.execute');
$errorInfo=$e instanceof PDOException ? $e->errorInfo : null;
$message=$e->getMessage();
Yii::log(Yii::t('yii','CDbCommand::execute() failed: {error}. The SQL statement executed was: {sql}.',
array('{error}'=>$message, '{sql}'=>$this->getText().$par)),CLogger::LEVEL_ERROR,'system.db.CDbCommand');
if(YII_DEBUG)
$message.='. The SQL statement executed was: '.$this->getText().$par;
throw new CDbException(Yii::t('yii','CDbCommand failed to execute the SQL statement: {error}',
array('{error}'=>$message)),(int)$e->getCode(),$errorInfo);
}
}
执行SQL语句,返回受影响的行,$this->_statement是pdo资源
$this->_statement =
$this->getConnection()
->getPdoInstance()
->prepare($this->getText());
prepare()方法是pdo的预处理方法,用作准备语句的查询与以住使用的查询略有区别,因为对于每次执行迭代中要改变的值,必须使用占位符而不是具体的列值
到这里我们其实就明白了,一个数据库操作方法,就是根据查询条件拼接出SQL,然后执行SQL的过程,底层使用的PDO方式来操作的,关于PDO的知识,我们再来了解一下
PHP 数据对象 (PDO) 扩展为PHP访问数据库定义了一个轻量级的一致接口。
PDO 提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据。
下面是PDO连接数据库的操作:
<?php
$dbms='mysql'; //数据库类型
$host='localhost'; //数据库主机名
$dbName='test'; //使用的数据库
$user='root'; //数据库连接用户名
$pass=''; //对应的密码
$dsn="$dbms:host=$host;dbname=$dbName";
try {
$dbh = new PDO($dsn, $user, $pass); //初始化一个PDO对象
echo "连接成功<br/>";
/*你还可以进行一次搜索操作
foreach ($dbh->query('SELECT * from FOO') as $row) {
print_r($row); //你可以用 echo($GLOBAL); 来看到这些值
}
*/
$dbh = null;
} catch (PDOException $e) {
die ("Error!: " . $e->getMessage() . "<br/>");
}
//默认这个不是长连接,如果需要数据库长连接,需要最后加一个参数:array(PDO::ATTR_PERSISTENT => true) 变成这样:
$db = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true));
?>
PDO操作的函数:
PDO::beginTransaction — 启动一个事务
PDO::commit — 提交一个事务
PDO::__construct — 创建一个表示数据库连接的 PDO 实例
PDO::errorCode — 获取跟数据库句柄上一次操作相关的 SQLSTATE
PDO::errorInfo — 返回最后一次操作数据库的错误信息
PDO::exec — 执行一条 SQL 语句,并返回受影响的行数
PDO::getAttribute — 取回一个数据库连接的属性
PDO::getAvailableDrivers — 返回一个可用驱动的数组
PDO::inTransaction — 检查是否在一个事务内
PDO::lastInsertId — 返回最后插入行的ID或序列值
PDO::prepare — 备要执行的SQL语句并返回一个 PDOStatement 对象
PDO::query — 执行 SQL 语句,返回PDOStatement对象,可以理解为结果集
PDO::quote — 为SQL语句中的字符串添加引号。
PDO::rollBack — 回滚一个事务
PDO::setAttribute — 设置属性
PDOStatement 类:
PDOStatement::bindColumn — 绑定一列到一个 PHP 变量
PDOStatement::bindParam — 绑定一个参数到指定的变量名
PDOStatement::bindValue — 把一个值绑定到一个参数
PDOStatement::closeCursor — 关闭游标,使语句能再次被执行。
PDOStatement::columnCount — 返回结果集中的列数
PDOStatement::debugDumpParams — 打印一条 SQL 预处理命令
PDOStatement::errorCode — 获取跟上一次语句句柄操作相关的 SQLSTATE
PDOStatement::errorInfo — 获取跟上一次语句句柄操作相关的扩展错误信息
PDOStatement::execute — 执行一条预处理语句
PDOStatement::fetch — 从结果集中获取下一行
PDOStatement::fetchAll — 返回一个包含结果集中所有行的数组
PDOStatement::fetchColumn — 从结果集中的下一行返回单独的一列。
PDOStatement::fetchObject — 获取下一行并作为一个对象返回。
PDOStatement::getAttribute — 检索一个语句属性
PDOStatement::getColumnMeta — 返回结果集中一列的元数据
PDOStatement::nextRowset — 在一个多行集语句句柄中推进到下一个行集
PDOStatement::rowCount — 返回受上一个 SQL 语句影响的行数
PDOStatement::setAttribute — 设置一个语句属性
PDOStatement::setFetchMode — 为语句设置默认的获取模式。
PDO数据库操作类,提供了一系列的操作,读者朋友可以自己尝试一下,这是属于WEB开发的基础操作了,这里就不再多说了。