vlambda博客
学习文章列表

源码分析|详解数据库操作方法都是如何执行的

大家好,我是 mamba架构算法 康师兄

作为全网最详细的框架底层源码分析文章,我们怎么可能会没有数据库方法的详细解释呢?


在上一篇文章 中,我们比较详细的分析了数据库连接和使用的方法,那么,对于一个封装好的数据库方法,底层究竟是如何解析和执行呢?在今天的文章中,我们将会揭开数据库封装方法的神秘面纱。


正如,我之前说的那种,精通了一个框架的底层,我们就可以很轻松的去研究其他框架了,因为很多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开发的基础操作了,这里就不再多说了。




精彩推荐



      

源码分析|详解数据库操作方法都是如何执行的

新学期,新开始
一起加油!



分享,点赞,在看,
都在这儿,点我不香吗?