12.4 业务逻辑层
12.4.1 事务脚本
事务脚本通过自己处理请求,而不是委托给特定的对象来完成。事务脚本的好处在于你可以很快就得到想要的结果,每个脚本都能很好的处理输入的数据并操作数据库来保证想要的结果。
我们仍然看之前的场景,一个场所(venue)中有多个空间(space),每个空间(space)中都会发生多个事件(event)。
数据表结构:
CREATE TABLE 'venue' (
'id' int(11) NOT NULL auto_increment,
'name' text,
PRIMARY KEY ('id')
)
CREATE TABLE 'space' (
'id' int(11) NOT NULL auto_increment,
'venue' int(11) default NULL,
'name' text,
PRIMARY KEY('id')
)
CREATE TABLE 'event' (
'id' int(11) NOT NULL auto_increment,
'space' int(11) default NULL,
'start' mediumtext,
'duration' int(11) default NULL,
'name' text,
PRIMARY KEY('id')
)
显然,我们的系统需要有添加场所和事件的功能,这每一个功能称之为事务,每一个功能的代码即事务脚本。
在系统设计上,我们将所有方法放到一个类中,但作为继承体系的一部分。为什么要定义一个基类呢?因为对于任何规模的项目,我们都需要添加更多具体的子类到继承体系中,由于这些子类大多数要和数据库打交道,所以把一些核心的数据库访问功能放到基类里是一个很好的选择。
在基类中获取一个 PDO 对象,然后存储在静态属性中,并提供用于缓存数据库语句和查询数据库的各种类方法。
abstract class Base {
static $DB;
static $stmts = array();
function __construct() {
$dsn = ApplicationRegistry::getDSN();
if (is_null($dsn)) {
throw new AppException("No dsn");
}
self::$DB = PDO($dsn);
self::$DB->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
}
function prepareStatment($stmt_s) {
if (isset(self::$stmts[$stmt_s])) {
return self::$stmts[$stmt_s];
}
$stmt_handle = self::$DB->prepare($stmt_s);
self::$stmts[$stmt_s] = $stmt_handle;
return $stmt_handle;
}
protected function doStatement($stmt_s, $value_a) {
$sth = $this->prepareStatment($stmt_s);
$sth->closeCursor();
$db_result = $sth->excute($value_a);
return $sth;
}
}
prepareStatememt() 方法调用 PDO 类的 prepare() 方法,返回一个 SQL 语句的句柄,该句柄最终将被传递给 execute() 方法。在prepareStatememt() 方法中,我们把数据资源缓存在一个静态数组 $stmts 中。
doStatement() 方法的参数是一个 SQL 语句和一个数组,该数组中包含在 SQL 语句执行时传递给数据库的值,通过该方法可获得到语句资源。
接下来我们来构建事务脚本,在构建事务脚本中我们只需要构造出 SQL 语句并继续关注业务逻辑即可。
下面是 VenueManger 类的开始部分:
class VenueManager extends Base {
static $add_venue = "INSERT INTO venue
( name )
value( ? )";
static $add_space = "INSERT INTO space
( name, venue )
value( ?, ? )";
static $check_slot = "SELECT id, name
FROM event
WHERE space=?
AND (start+duration)>?
AND start<?";
static $add_event = "INSERT INTO event
( name, space, start, duration )
value( ?, ?, ?, ? )";
//...
这部分设置我们将要使用的 SQL 语句,它们被构造成能够被 PDO 的 prepare() 方法接受的格式,问号代表占位符,值将被传递给 execute() 方法。
接下来我们看一个添加场所的方法 addVenue() :
function addVenue($name, $space_array) {
$ret = array();
$ret['venue'] = array($name);
$this->doStatement(self::$add_venue, $ret['venue']);
$v_id = self::$DB->lastInsertId();
$ret['spaces'] = array();
foreach ($space_array as $space_name) {
$values = array($space_name, $v_id);
$this->doStatement(self::$add_space, $values);
$s_id = self::$DB->lastInsertId();
array_unshift($values, $s_id);
$ret['spaces'][] = $values;
}
return $ret;
}
该方法的参数是一个场所名称和一个包含多个空间名称的数组,该方法使用这两个参数来为 venue 表和 space 表赋值,同时创建关联表信息,关联 space 记录和 venue 记录。
第二个事务,用于添加一个事件到 events 表中,和一个空间关联。
function bookEvent($space_id, $name, $time, $duration) {
$values = array($space_id, $time, $time+$duration );
$stmt = $this->doStatement(self::$check_slot ,$values, false);
if ($result=$stmt->fetch()) {
throw new AppException("double booked! try again!");
}
$this->doStatement(self::$add_event, array($name, $space_id, $time, $duration));
}
事务脚本是快速获得结果的有效途径,尤其是在开发一个小型项目时使用。但这种方式使项目不太容易扩展,因为事务脚本总是不可避免的相互渗入,从而导致代码重复。