这次要谈的3个关键字:DAO、Model、AR,是我们在做web应用时常见的几个概念,也被称作设计模式(design pattern),先简单看看它们的全拼和中文:
- DAO:Data Access Object 数据访问对象
- Model:数据模型
- AR:Active Record 活动记录
几乎所有的web开发框架在设计的时候,都或多或少用到了这些设计模式来实现了MVC中的M层,通过为开发者提供强有力的类库,简单便捷的完成数据库访问。
很多同学对这些概念的理解相对模糊,因此下面我将通过几个例子循序渐进的描述这3个概念,希望与大家分享与讨论我的认识。
M
这里的M就是MVC框架中的M,下面我会先通过DAO和Model两个设计模式来实现一个M层,它提供了对Mysql数据库中的Group表的访问能力。
Model
model是一个类,它生成的1个对象代表了数据库中的一行记录。既然要代表1行记录,那么它应该有若干和数据库列对应的属性,这里就拿一个Group表为例,看看Model类如何定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * Object represents table 'groups' * * @author: http://phpdao.com * @date: 2009-10-17 02:53 */ class Group{ var $id; var $name; } ?> |
可见,Group就是一个Model类,它定义了2个属性对应了数据库表Group中的2个列,这个类的1个对象就代表数据库中的一行记录,就这么简单。
DAO
Data Access Object,数据访问对象,意思就是通过这个类的对象可以实现对数据库的真实访问。
前面我们也看到了,Model仅仅代表了一行数据库记录,但是没有任何与数据库打交道的代码,这是因为DAO才是真正负责与数据库进行通讯的”人”呐…
我们知道,对于任何一张表来说,它们都有一些统一的常见操作,例如:按主键查询,插入,查询,删除。
观察这些操作,我们可以知道无论是查询还是修改操作,其实都是在与数据库里的记录行交互,对应到程序里也就等价于之前看到的Model对象了。因此,我们基本可以想象到DAO的职责:
- 通过向DAO对象的新增/修改方法,传入model对象,实现对数据库中对应记录的新增/修改。
- 通过DAO对象的查询方法,从数据库读取数据,并返回model对象作为结果。
但是我们知道,数据库可能是Mysql,Mongodb,Oracle等等,因此DAO的实现可能不止一种,所以通常会为DAO定义一个通用的接口,并为不同的数据库提供不同的实现。这里,我们为Group model设计一个DAO的interface,无论任何数据库在实现时都应该实现这些接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
/* * Intreface DAO * * @author: http://phpdao.com * @date: 2009-10-17 02:53 */ interface GroupsDAO{ /** * Get Domain object by primry key * * @param String $id primary key * @Return Groups */ public function load($id); /** * Get all records from table */ public function queryAll(); /** * Get all records from table ordered by field * @Param $orderColumn column name */ public function queryAllOrderBy($orderColumn); /** * Delete record from table * @param group primary key */ public function delete($id); /** * Insert record to table * * @param Groups group */ public function insert($group); /** * Update record in table * * @param Groups group */ public function update($group); /** * Delete all rows */ public function clean(); public function queryByName($value); public function deleteByName($value); } ?> |
现在,我们继承GroupDAO接口,为Group表实现访问Mysql的DAO类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
/* * Class that operate on table 'groups'. Database Mysql. * * @author: http://phpdao.com * @date: 2009-10-17 02:53 */ class GroupsMySqlDAO implements GroupsDAO{ /** * Get Domain object by primry key * * @param String $id primary key * @return GroupsMySql */ public function load($id){ $sql = 'SELECT * FROM groups WHERE id = ?'; $sqlQuery = new SqlQuery($sql); $sqlQuery->setNumber($id); return $this->getRow($sqlQuery); } /** * Get all records from table */ public function queryAll(){ $sql = 'SELECT * FROM groups'; $sqlQuery = new SqlQuery($sql); return $this->getList($sqlQuery); } /** * Get all records from table ordered by field * * @param $orderColumn column name */ public function queryAllOrderBy($orderColumn){ $sql = 'SELECT * FROM groups ORDER BY '.$orderColumn; $sqlQuery = new SqlQuery($sql); return $this->getList($sqlQuery); } /** * Delete record from table * @param group primary key */ public function delete($id){ $sql = 'DELETE FROM groups WHERE id = ?'; $sqlQuery = new SqlQuery($sql); $sqlQuery->setNumber($id); return $this->executeUpdate($sqlQuery); } /** * Insert record to table * * @param GroupsMySql group */ public function insert($group){ $sql = 'INSERT INTO groups (name) VALUES (?)'; $sqlQuery = new SqlQuery($sql); $sqlQuery->set($group->name); $id = $this->executeInsert($sqlQuery); $group->id = $id; return $id; } /** * Update record in table * * @param GroupsMySql group */ public function update($group){ $sql = 'UPDATE groups SET name = ? WHERE id = ?'; $sqlQuery = new SqlQuery($sql); $sqlQuery->set($group->name); $sqlQuery->setNumber($group->id); return $this->executeUpdate($sqlQuery); } /** * Delete all rows */ public function clean(){ $sql = 'DELETE FROM groups'; $sqlQuery = new SqlQuery($sql); return $this->executeUpdate($sqlQuery); } public function queryByName($value){ $sql = 'SELECT * FROM groups WHERE name = ?'; $sqlQuery = new SqlQuery($sql); $sqlQuery->set($value); return $this->getList($sqlQuery); } public function deleteByName($value){ $sql = 'DELETE FROM groups WHERE name = ?'; $sqlQuery = new SqlQuery($sql); $sqlQuery->set($value); return $this->executeUpdate($sqlQuery); } /** * Read row * * @return GroupsMySql */ protected function readRow($row){ $group = new Group(); $group->id = $row['id']; $group->name = $row['name']; return $group; } protected function getList($sqlQuery){ $tab = QueryExecutor::execute($sqlQuery); $ret = array(); for($i=0;$i $ret[$i] = $this->readRow($tab[$i]); } return $ret; } /** * Get row * * @return GroupsMySql */ protected function getRow($sqlQuery){ $tab = QueryExecutor::execute($sqlQuery); return $this->readRow($tab[0]); } /** * Execute sql query */ protected function execute($sqlQuery){ return QueryExecutor::execute($sqlQuery); } /** * Execute sql query */ protected function executeUpdate($sqlQuery){ return QueryExecutor::executeUpdate($sqlQuery); } /** * Insert row to table */ protected function executeInsert($sqlQuery){ return QueryExecutor::executeInsert($sqlQuery); } } ?> |
我们定义了GroupMysqlDAO类并实现了所有的接口方法,2个比较典型的DAO行为如下:
- load($id)方法:传入主键id,从mysql查询回记录,并最终通过readRow($row)方法将数据库的记录行转换成了一个Group model对象返回。
- insert($group)方法:传入一个Group model对象,方法中将其name属性取出拼成insert语句,最终插入到了mysql中。
这里见到的SqlQuery和QueryExecutor对象,都是对Mysql基础API的进一步封装,因此不做深入,可以认为它们与PDO库功能类似。
通过这个DAO类的对象,我们可以非常方便的操作Group model对象来与mysql进行数据交换,对于上述的简单数据库操作来说,开发者根本不需要写任何SQL即可完成开发,仅仅利用GroupMysqlDAO和Group2个类既可完成业务逻辑的开发。
然而现实情况并没有这么简单,通常我们需要按其他索引进行数据库查询或者排序,按照一些复杂的条件进行数据库的记录批量更新,甚至进行多表Join,这些能力当前的DAO并没有实现,因此我们自然想到对这个GroupMysqlDAO类进行扩展,添加一些我们想要的函数,这些函数可以接受任意参数,返回任意值,这里我并不考虑新函数的通用性(例如排序规则是用户传入的)。
这里拿一个另外的Content model为例,我们继承它的DAO添加2个特殊的查询方法,并且添加1个联查User表的方法,它返回若干UserContent model对象,然而这个UserContent model对象并不对应数据库中真实存在的表,这是没有任何问题的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
/* * Class that operate on table 'content'. Database Mysql. * Here you can write non standard sql queries * * @author: http://phpdao.com * @date: 2009-10-17 03:43 */ class ContentMySqlExtDAO extends ContentMySqlDAO{ function queryByContentAndCreatedBy($content, $createdBy){ $sql = "SELECT * FROM content WHERE title like '%".$content."%' AND created_by=?"; $sqlQuery = new SqlQuery($sql); $sqlQuery->setNumber($createdBy); return $this->getList($sqlQuery); } /** * Get rows count where column created_by is equal to method param */ function getCountByCreatedBy($createdBy){ $sql = "SELECT count(*) FROM content WHERE created_by=?"; $sqlQuery = new SqlQuery($sql); $sqlQuery->setNumber($createdBy); return $this->querySingleResult($sqlQuery); } /** * This method returns array of object UserContent. * Here sql query gets data from two tables. * Developer must loop by variable tab and create for all rows objec UserContent * and add this object to new array */ function getUserNameAndContentTitle(){ $sql = "SELECT u.name, c.title FROM users u, content c WHERE c.created_by=u.id"; $sqlQuery = new SqlQuery($sql); $tab = $this->execute($sqlQuery); $ret = array(); for($i=0;$i $userContent = new UserContent(); $userContent->username = $tab[$i]["name"]; $userContent->title = $tab[$i]["title"]; $ret[$i] = $userContent; } return $ret; } } |
1 2 3 4 5 6 7 8 |
/** * Non standard transfer object */ class UserContent{ var $username; var $title; } ?> |
综上,
1,DAO对象实现了Model和数据库Row之间的互相转化能力。
2,Model不一定必须对应数据库中真实存在的表,但它通常应该对应。
3,DAO的实现并不一定是访问数据库,它的数据来源可以是任意的,例如一个远程的网络服务,只要通过DAO接口可以实现model与数据源之间的互相转换既可,这就是它的抽象能力所在了。
4,DAO+model是实现M层的最基础模式。
AR
Active Record是一种在DAO+model基础上演化出来的东西,像一个混血儿。
它首先是一个model,也就是AR的1个对象对应了数据库中的一行记录,它有若干属性对应了数据库中的列。
此前我们知道,DAO和model是2个类,相当于在2个层级上,彼此分离。而AR则将DAO的能力挪到了model本身,也就是说原本这样的调用:
$model = new Model(); $model->name = “owen”; $dao = new Dao(); $dao->insert($model);
现在变成了这样:
$ar = new AR(); $ar->name = “owen”; $ar->insert();
从直观上来看,AR模式使用起来更加方便,如果你仔细观察2者的差异,你会发现AR模式并不神秘,你只需要简单的将DAO和Model组合一下就可以实现AR了,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class AR extens Model { // 继承自model的属性 // public $id; // public $name; // dao对象 private static $dao_ = new Dao(); // 封装dao方法 public function insert() { self::$dao_->insert($this); } } |
- 我们让AR继承model,这样AR类的对象就可以进行属性赋值:$ar->name = “owen” 了。
- 其次,我们将DAO放在static属性实现单例,因为它仅仅是一个工具类。
- 为AR类添加实例方法insert,给dao对象传入$this(相当于$model),这样通过:$ar->insert() 既可完成数据的插入。
观察上述AR实现,有2个问题需要解决:
- AR的每个操作方法,都是对DAO对象的再次封装,为何不能把DAO的方法直接实现在AR类中呢?
- AR通过对象属性保存$id,$name的方式不具备通用性,换一张表就无法适用,能否进一步抽象呢?
因此明确我们的设计目标:首先,AR是一个model,代表了db表中一行记录;其次,AR直接具备DAO方法,可以操作记录自身(此前的DAO与model是分离的2个东西)。
有了这两个目标,我们可以设计一个兼具DAO和Model的BaseActiveRecord类,它大概如下(伪代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
<?php class BaseActiveRecord { // 这里保存model的属性 private $attributes = []; // 这里指定主键,子类覆写 private static $primaryKey = 'id'; // 这里返回表名, 子类覆写 public static function tableName() { return ''; } // 这里返回数据库连接,默认取框架配置的DB连接, 子类可覆写 public static function getDb() { return \App::get("db"); } // 下面3个方法可以让我们通过$ar->name这种方式读写model属性 public function __set($name, $value) { $this->attributes[$name] = $value; } public function __get($name) { return isset($this->attributes[$name]) ? $this->attributes[$name] : null; } public function __isset($name) { return isset($this->attributes[$name]); } // DAO实例方法 public function insert() { // 假设getDb()返回的是PDO类, 下面根据attributes属性创建一条记录 $success = static::getDb()->insert(static::tableName(), $this->attributes); // 这里保存一下产生的自增主键 $id = static::getDb()->lastInsertId(); if (!empty($id)) { $this->attributes[static::$primaryKey] = $id; } return $success; } // DAO实例方法 public function update($fields) { // 保存$fields数组指定的attributes中的字段 $updateAttr = []; foreach ($fields as $field) { $updateAttr[$field] = $this->attributes[$field]; } $affectRows = static::getDb()->update( static::tableName(), $this->attributes[static::$primaryKey], // 按主键更新对应行的属性 $updateAttr ); return $affectRows; } // DAO静态方法 public static function find($condition, $groupBy, $orderBy) { // 调用PDO查询回结果 $rows = static::getDb()->query(static::tableName(), $condition, $groupBy, $orderBy); // 转换成AR对象 $retObjs = []; foreach ($rows as $row) { $obj = new static(); $obj->setAttribtes($row); $retObjs[] = $obj; } // 返回AR对象数组 return $retObjs; } } |
从上面的代码可以看出,查询方法是static的,这是因为此时还没有行记录产生,一旦有了ar记录则剩余方法都是成员方法。另外,通过__set,__get,attributes的方式保存属性,这样就不会局限于特定的表结构了。
经过重新设计,BaseActiveRecord类具备了一定的通用性,业务开发者只需要继承BaseActiveRecord类覆写一下tableName和primaryKey,就可以开始增删改查特定的数据表了。
ORM
看完AR的原理,我觉得有必要谈谈ORM又是什么概念。其实ORM是DAO的一种实现方式,但是因为其语义的特点原因,通常ORM特指为AR的一种实现方式。
关于ORM的历史背景我也不是特别了解,我仅谈谈我的理解:
- 上述1个AR代表1个表,但是表与表之间的关联关系,没有体现也没有实现,这意味着我只能:
- 使用AR:先查A,再根据A的字段查询B。
- 不使用AR:直接裸写SQL语句,利用数据库join进行关联,这相当于彻底抛弃了AR。
- 上述AR不够抽象:
- 拿find来说,它通过参数接受了3个SQL要素,这里还没有考虑更多的SQL要素(比如:limit),所以参数传递的方式不够漂亮,扩展性不足,不够面向对象。
按我的理解,ORM相当于对AR进行了重新的设计,解决了此前方案的不足。
对于抽象问题来说,ORM通过QueryBuilder方式支持链式的查询语句构造,例如:
1 |
AR::find()->where(['=', 'name', 'owenliang'])->groupBy('age')->orderBy('count desc')->all(); |
好处不言而喻,这里AR类的find函数不直接接受SQL参数而是返回一个QueryBuilder对象,这个对象支持链式的构造SQL语句,最终通过all()即可拼装各个SQL要素,产生真实的SQL查询,是不是很爽?
对于表关联问题来说,ORM直接在AR类中定义表关联关系,在查询A表数据时可以根据我们定义的关联关系自动的将依赖的B表的数据自动获取回来,这个就不做演示了,后续我会在一篇专门的博客里来讲解ORM的用法。
不过要注意,ORM的表关联能力有优点也有缺点,主要缺点如下:
- ORM生成SQL是背地里完成的,导致我们没法针对SQL调优,比如:
- A表关联B表,ORM虽然能够自动获取A表依赖的B表数据,但它只能将B表的所有字段读回来填充到B表的AR对象中,而我们也许只需要其中的个别字段而已。
- ORM表关联用法略微复杂,对开发者要求不仅会SQL还要会ORM,难上加难。
- ORM表关联仅适用于单库不分表场景,对于复杂的拆库拆表甚至分布式数据库部署来说,很难支持。
全文终
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

在yii中,就区分了model和ar,但是在laravel中,并没有区分它们。在yii中,model是数据库无关的一种对象,model的属性,就是参数,方法一般围绕这些参数实现数据的处理。ar实例代表了数据库中的一行记录,ar又是扩展自model,具有model的特性,这就让ar的职能变得模糊了,有时候并不是一行记录该具有的方法,却也塞给了这个ar实例,这实在不应该。yii中的model可以包装用户的请求数据,再写些方法针对这些数据实现一些业务逻辑。
我已经心已经偏向laravel了,因为yii实现的功能不够多,查询构造器也远不如laravel强大。laravel的命令行工具用得非常顺心,facade这种东西,写着也很顺手,虽然yii也有类似的实现,就是应用组件,应用组件就类似laravel的facade。laravel有很多取巧的设计,这让开发者很舒心,例如查询构造器的各种where函数。但是laravel的配置系统真心不好,至少我喜欢yii的更多点。yii的配置系统的实现,是我非常喜欢的一点,为什么呢,因为记录对象属性的配置,可以让我容易知道有哪些配置项可以配置,还有不对存在拼写错误。