关于AR的垂直分表的疑难杂症,包括多库(多连接对象)的疑难杂症

有些需求必须把表垂直拆开,一个表变成几个表;在做 model[AR] 的时候,又想做成像一个表一样的接口的 model,这样底层接口看起来就比较简单;最关键要解决的难题是多表操作的事务的实现,现在的解决方案是利用 beforeSave() 和 afterSave() ,在前面开始事务,后面提交事务,如果中间出现异常,则再自定义一个 ErrorHandler,在出错的情况下,对所有事务进行回滚(我还不同模块不同数据库的拆分的,所以有多个dbconnection)。

垂直拆表(或者多表操作)是比较常见的, qiang是否官方提供一些标准的方案呢?

以下是我的一些代码




<?php

/**

 * 数据库连接对象

 *

 * 支持多层事务

 *

 * @author terry39

 */

class DbConnection extends CDbConnection

{

	protected $_attributes = array();

	protected $_pdo;


	protected function createPdoInstance()

	{

		$pdoClass='PDOExt';

		if(($pos=strpos($this->connectionString,':'))!==false)

		{

			$driver=strtolower(substr($this->connectionString,0,$pos));

			if($driver==='mssql' || $driver==='dblib')

				$pdoClass='CMssqlPdoAdapter';

		}

		return $this->_pdo = new $pdoClass($this->connectionString,$this->username,

									$this->password,$this->_attributes);

	}


	public function setAttribute($name,$value)

	{

		if($this->_pdo instanceof PDO)

			$this->_pdo->setAttribute($name,$value);

		else

			$this->_attributes[$name]=$value;

	}


	/**

	 * 回滚所有未提交的事务

	 */

	public function rollBackAll()

	{

		if($this->_pdo instanceof PDOExt){

			$this->_pdo->rollBackAll();

		}

	}

}


/**

 * 扩展的PDO

 *

 * 支持多层事务

 *

 * @author terry39

 */

class PDOExt extends PDO

{

    // Database drivers that support SAVEPOINTs.

    protected static $savepointTransactions = array("pgsql", "mysql");


    // The current transaction level.

    protected $transLevel = 0;


    protected function nestable() {

        return in_array($this->getAttribute(PDO::ATTR_DRIVER_NAME),

                        self::$savepointTransactions);

    }


    public function beginTransaction() {

        if(!$this->nestable() || $this->transLevel == 0) {

            parent::beginTransaction();

        } else {

            $this->exec("SAVEPOINT LEVEL{$this->transLevel}");

        }


        $this->transLevel++;

    }


    public function commit() {

        $this->transLevel--;


        if(!$this->nestable() || $this->transLevel == 0) {

            parent::commit();

        } else {

            $this->exec("RELEASE SAVEPOINT LEVEL{$this->transLevel}");

        }

    }


    public function rollBack() {

        $this->transLevel--;


        if(!$this->nestable() || $this->transLevel == 0) {

            parent::rollBack();

        } else {

            $this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->transLevel}");

        }

    }


	/**

	 * 回滚所有未提交的事务

	 */

	public function rollBackAll()

	{

		while($this->transLevel >= 0){

			$this->rollBack();

		}

	}

}

?>






<?php

/**

 * 数据库连接管理器

 *

 * 可以从这里获得指定 connId 的 CDbConnection 对象

 *

 * 在 main.php 中 配置

 * 

 * 'components'	=> array(

 *     'dbConnectionsManager' => array(

 *        'class' => 'DbConnectionsManager',

 *        'connections' => array(

 *            'connId' => array(

 *					'connectionString'	=> 'mysql:host=localhost;dbname=db_name',

 *					'username'			=> 'username',

 *					'password'			=> 'password',

 *					'charset'			=> 'utf8',

 *             ),

 *            'connId2' => array( ... ),

 *            '...',

 *        ),

 *     ),

 * ),

 *

 * @FIXME 如 authManager 等直接通过 Yii::app()->getComponent(component id) 获得连接对象的情况,会因为对象未实例化而导致异常。

 * 

 *

 * @author terry39 <gamezoom@gmail.com>

 */

class DbConnectionsManager extends CApplicationComponent

{

	private $_connectionConfigs = array();

	private $_dbConnections = array();

	private $_connectionClass = 'CDbConnection';


	// 是否允许 Application 默认的 CDBConnection 对象

	private $_useAppDb = false;


	/**

	 * 只写属性 useAppDb

	 */

	public function setUseAppDb($value)

	{

		$this->_useAppDb = $value;

	}


	/**

	 * 只读属性 connectionClass

	 * 默认的连接对象类

	 */

	public function setConnectionClass($cc)

	{

		$this->_connectionClass = $cc;

	}


	/**

	 * set property of connections

	 */

	public function setConnections($connectionConfigs)

	{

		$this->_connectionConfigs = $connectionConfigs;

	}


	/**

	 * is a connection id

	 */

	public function isConnectionId($connId)

	{

		return isset($this->_connectionConfigs[$connId]);

	}


	/**

	 * 回滚所有未提交的事务

	 */

	public function rollBackAll()

	{

		foreach($this->_dbConnections as $conn)

		{

			if($conn instanceof DbConnection){

				$conn->rollBackAll();

			}

		}

	}


	/**

	 * 根据 connId 获得 CDBConnection 对象

	 */

	public function getConnection($connId)

	{

		if(!isset($this->_dbConnections[$connId])){

			if(!isset($this->_connectionConfigs[$connId])){

				return $this->_useAppDb ? Yii::app()->db : null;

			}

			$config = $this->_connectionConfigs[$connId];

			$config['class'] = isset($config['class']) ? $config['class'] : $this->_connectionClass;

			$this->_dbConnections[$connId] = $db = Yii::createComponent($config);

			Yii::app()->setComponent($connId, $db);

		}


		return $this->_dbConnections[$connId];

	}

}






<?php

class ErrorHandler extends CErrorHandler

{

	/**

	 * 响应错误事件

	 *

	 * 回滚所有未数据库连接对象的 所有未提交的事务

	 * 然后再由父分类的方法来响应事件

	 */

	public function handle($event)

	{

		try{

			Yii::app()->dbConnectionsManager->rollBackAll();

			$db = Yii::app()->getComponent('db', false);

			if($db && $db instanceof PDOExt){

				$db->rollBackAll();

			}

		}catch(Exception $e){}


		parent::handle($event);

	}

}

?>



多谢分享你的工作。目前我们对分表分库的支持还处在调研阶段。提供某种特定的解决方案并不难,我们希望提供的是一套能适应大多数需求的解决方案。

期待对分表分库的支持!我们现在对于按月分表的情况,只能直接使用createCommand去访问数据库了。。

@qing,可以参考下Doctrine的DAL(http://www.doctrine-project.org/projects/dbal)