数据源架构模式之活动记录
- 问题
大多数WEB应用将信息持续保存在数据库中。有将数据库操作抽象化,以达到简化表数据存取和对业务逻辑的集成存取方法吗?
- 解决方案
理论上,活动记录模式是最简化的有关数据库的设计模式。动态记录模式包含了如何在类中直接实现与数据库交互的相关知识。
活动记录模式在程序代码与数据库结构之间产生了一种很高的结合度,在一些相对简单的应用环境中,就能比采用别的复杂方案更容易解决这种因结合所产生的一些固有问题。动态记录模式也能满足许多初级的数据库项目。只有当复杂性增加而难以用动态记录模式处理时,你才有必要使用表数据网关模式,或是数据地图模式或是别的数据库设计模式。
ActiveRecord也属于ORM层,由Rails最早提出,遵循标准的ORM模型:表映射到记录,记录映射到对象,字段映射到对象属性。配合遵循的命名和配置惯例,能够很大程度的快速实现模型的操作,而且简洁易懂。
【活动记录的意图】
一个对象,它包装数据表或视图中某一行,封装数据库访问,并在这些数据上增加了领域逻辑。
【活动记录的适用场景】
适用于不太复杂的领域逻辑,如CRUD操作等。
【活动记录的运行机制】
对象既有数据又有行为。其使用最直接的方法,将数据访问逻辑置于领域对象中。
活动记录的本质是一个领域模型,这个领域模型中的类和基数据库中的记录结构应该完全匹配,类的每个域对应表的每一列。
一般来说,活动记录包括如下一些方法:
1、由数据行构造一个活动记录实例;
2、为将来对表的插入构造一个新的实例;
3、用静态查找方法来包装常用的SQL查询和返回活动记录;
4、更新数据库并将活动记录中的数据插入数据库;
5、获取或设置域;
6、实现部分业务逻辑。
【活动记录的优点和缺点】
优点:
1、简单,容易创建并且容易理解。
2、在使用事务脚本时,减少代码复制。
3、可以在改变数据库结构时不改变领域逻辑。
4、基于单个活动记录的派生和测试验证会很有效。
缺点:
1、没有隐藏关系数据库的存在。
2、仅当活动记录对象和数据库中表直接对应时,活动记录才会有效。
3、要求对象的设计和数据库的设计紧耦合,这使得项目中的进一步重构很困难
ActiveRecord的主要思想是:
- 每一个数据库表对应创建一个类,类的每一个对象实例对应于数据库中表的一行记录;通常表的每个字段在类中都有相应的Field;
- ActiveRecord同时负责把自己持久化,在ActiveRecord中封装了对数据库的访问,即CURD;;
- ActiveRecord是一种领域模型(Domain Model),封装了部分业务逻辑;
ActiveRecord比较适用于:
- 业务逻辑比较简单,当你的类基本上和数据库中的表一一对应时, ActiveRecord是非常方便的,即你的业务逻辑大多数是对单表操作;
- 当发生跨表的操作时, 往往会配合使用事务脚本(Transaction Script),把跨表事务提升到事务脚本中;
- ActiveRecord最大优点是简单, 直观。 一个类就包括了数据访问和业务逻辑. 如果配合代码生成器使用就更方便了;
这些优点使ActiveRecord特别适合WEB快速开发。
ActiveRecord不适合于:
- ActiveRecord虽然有业务逻辑, 但基本上都是基于单表的. 跨表逻辑一般会放到当发生跨表的操作时, 往往会配合使用事务脚本(Transaction Script)中. 如果对象间的关联越来越多, 你的事务脚本越来越庞大, 重复的代码越来越多, 你就要考虑Domain Model + O/R Mapper了;
- ActiveRecord保存了数据, 使它有时候看上去像数据传输对象(DTO). 但是ActiveRecord有数据库访问能力, 不要把它当DTO用. 尤其在跨越进程边界调用的时候, 不能传递ActiveRecord对象。
【活动记录与其它模式】
数据源架构模式之行数据入口:活动记录与行数据入口十分类似。二者的主要差别是行数据入口 仅有数据库访问而活动记录既有数据源逻辑又有领域逻辑。
<?php
/**
* 定单类
*/
class Order {
/**
* 定单ID
* @var <type>
*/
private $_order_id;
/**
* 客户ID
* @var <type>
*/
private $_customer_id;
/**
* 定单金额
* @var <type>
*/
private $_amount;
public function __construct($order_id, $customer_id, $amount) {
$this->_order_id = $order_id;
$this->_customer_id = $customer_id;
$this->_amount = $amount;
}
/**
* 实例的删除操作
*/
public function delete() {
$sql = "DELETE FROM Order SET WHERE order_id = " . $this->_order_id . " AND customer_id = " . $this->_customer_id;
return DB::query($sql);
}
/**
* 实例的更新操作
*/
public function update() {
}
/**
* 插入操作
*/
public function insert() {
}
public static function load($rs) {
return new Order($rs['order_id'] ? $rs['order_id'] : NULL, $rs['customer_id'], $rs['amount'] ? $rs['amount'] : 0);
}
}
class Customer {
private $_name;
private $_customer_id;
public function __construct($customer_id, $name) {
$this->_customer_id = $customer_id;
$this->_name = $name;
}
/**
* 用户删除定单操作 此实例方法包含了业务逻辑
* 通过调用定单实例实现
* 假设此处是对应的删除操作(实际中可能是一种以某字段来标记的假删除操作)
*/
public function deleteOrder($order_id) {
$order = Order::load(array('order_id' => $order_id, 'customer_id' => $this->_customer_id));
return $order->delete();
}
/**
* 实例的更新操作
*/
public function update() {
}
/**
* 入口类自身拥有插入操作
*/
public function insert() {
}
public static function load($rs) {
/* 此处可加上缓存 */
return new Customer($rs['customer_id'] ? $rs['customer_id'] : NULL, $rs['name']);
}
/**
* 根据客户ID 查找
* @param integer $id 客户ID
* @return Customer 客户对象
*/
public static function find($id) {
return CustomerFinder::find($id);
}
}
/**
* 人员查找类
*/
class CustomerFinder {
public static function find($id) {
$sql = "SELECT * FROM person WHERE customer_id = " . $id;
$rs = DB::query($sql);
return Customer::load($rs);
}
}
class DB {
/**
* 这只是一个执行SQL的演示方法
* @param string $sql 需要执行的SQL
*/
public static function query($sql) {
echo "执行SQL: ", $sql, " <br />";
if (strpos($sql, 'SELECT') !== FALSE) { // 示例,对于select查询返回查询结果
return array('customer_id' => 1, 'name' => 'Martin');
}
}
}
/**
* 客户端调用
*/
class Client {
/**
* Main program.
*/
public static function main() {
header("Content-type:text/html; charset=utf-8");
/* 加载客户ID为1的客户信息 */
$customer = Customer::find(1);
/* 假设用户拥有的定单id为 9527*/
$customer->deleteOrder(9527);
}
}
Client::main();
?>
同前面的文章一样,这仅仅是一个活动记录的示例,关于活动记录模式的应用,可以查看Yii框架中的DB类,在其源码中有一个CActiveRecord抽象类,从这里可以看到活动记录模式的应用
另外,如果从事务脚本中创建活动记录,一般是首先将表包装为入口,接着开始行为迁移,使表深化成为活动记录。
对于活动记录中的域的访问和设置可以如yii框架一样,使用魔术方法__set方法和__get方法