zoukankan      html  css  js  c++  java
  • Yii源码阅读笔记

    2014-11-14 五

    By youngsterxyf

    概述

    Yii中,对Model层的使用,有两种方式:

    1. 通过类CDbConnection和CDbCommand来操作
    2. 使用ORM形式:编写model类继承自抽象类CActiveRecord

    第1种方式的示例如下:

    <?php
    $connection = Yii::app()->db;  // 或者Yii::app()->getComponent('db');
    $queryResult = $connection->createCommand($sql)->queryRow();
    

    第2种方式中编写的model类可能需要实现方法getDbConnectionmodeltableName

    在实现上,第2种方式是基于第1种方式的,即第2种方式的抽象程度更高。Yii没有屏蔽第1种方式,这样能让开发者按需选择。 但我个人并不喜欢这样,两种方式同时存在,会导致应用的model实现稍显混乱。

    分析

    Yii框架model层的入口为CDbConnection类,该类有很多public的属性可供配置,如connectionStringusernamepassword等。

    根据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种方式来完成数据库操作:

    1. 对于普通的数据库查询操作,得到CDbCommand对象后,链式调用方法select、from、where等(之所以能够链式调用,是因为这些方法的最后都返回了对象本身),并且链式调用的最后调用query一类方法来执行数据库操作。

    2. 对于会对数据表结构或数据产生变更的操作,得到CDbCommand对象后,可以直接调用方法insert、update、delete、createTable等来执行操作。

    3. 可以在得到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);
    }
    

    注:本文为草稿状态

  • 相关阅读:
    python基础——操作系统简介
    python 3 输入和输出
    C++学习笔记——C++简介
    ML机器学习导论学习笔记
    Linux常用基本指令——文件处理命令
    Oracle数据库从入门到精通-分组统计查询
    PL/SQL编程基础——PL/SQL简介
    django 第五课自定义模板过滤器与标签
    Python之GUI的最终选择(Tkinter)
    Python利用pandas处理Excel数据的应用
  • 原文地址:https://www.cnblogs.com/sunscheung/p/4864152.html
Copyright © 2011-2022 走看看