谈谈php里的DAO Model AR

这次要谈的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类如何定义:

可见,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,无论任何数据库在实现时都应该实现这些接口:

现在,我们继承GroupDAO接口,为Group表实现访问Mysql的DAO类:

我们定义了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,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了,例如:

  • 我们让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类,它大概如下(伪代码):

从上面的代码可以看出,查询方法是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方式支持链式的查询语句构造,例如:

好处不言而喻,这里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表关联仅适用于单库不分表场景,对于复杂的拆库拆表甚至分布式数据库部署来说,很难支持。

全文终

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~