大型项目如何选择ORM:Active Record VS Data Mappers

在大型Web项目中ORM有着举足轻重的作用,非常考验架构师的设计水平,我见过的失败项目大部分都是ORM模块出问题导致的。最近在重构一个大型项目,借此机会和大家聊聊ORM。

ORM(Object Relational Mapping)对象关系映射,是一种程序技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换,简单点说就是将数据库里面的一条数据映射成一个对象,要对某条数据增删改查时直接操作对应的对象即可。这样带来的好处是不言而喻的,比如要insert一条记录,原始的做法是这样:

1
2
INSERT INTO `user` (`id`, `account`, `password`) 
VALUES (1, 'it2048', '123456');

这样做会有一些问题:

  1. 手写SQL很费时,遇到几十上百个字段的表,一句insert要耗费半天精力。
  2. 每次都要看着数据库客户端,不然属性名称没法写。
  3. 容易把字段的类型弄错,varchar类型的属性传入了int。
  4. 容易写出SQL注入漏洞。

为了解决这些问题,ORM顺势而生,使用ORM之后的代码如下:

1
2
3
4
5
6
<?php
$model = new User();
$model->id = 1;
$model->account = 'it2048';
$model->password = '123456';
$model->save();

对比一下会发现,使用ORM之后上面那些问题都迎刃而解,接下来看看他是如何实现的。

比如MySQL里面的User表如下:

id account password
1 it2048 123456

对应的ORM如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class User extends Model{

protected $id;
protected $account;
protected $password;

public function setAccount($account){
$this->account = $account;
}
public function getAccount($account){
return $this->account;
}
}

需要插入一条记录只需要new一个User类,然后操作User对象给属性赋值,最后调用save()方法将User对象转换成insert语存储到MySQL。大部分操作都可以在父类Model中封装,比如save()方法,这就是ActiveRecord(ORM的一种思想)的实现方式。

一. ORM的两种实现哲学

我们将ORM的思想拆分之后会发现它就两个功能。

  1. 数据操作 - 对数据对象做变更,就是我们常说的业务逻辑。

  2. 数据持久化 - 将数据落地,比如存储到MySQL,MongoDB等不同的数据库。

有两个功能,就有了吵架的理由了。于是大家分成了两派,一派认为应该把两个功能合在一起,简单方便,易上手,名字都想好了就叫 ActiveRecord。另一派认为两个功能必须分开,扩展灵活,逼格高,名字也想好了就叫 Data Mappers。两派高手吵了很久,Talk is cheap,Show me the code ,咱们github见。

二. ActiveRecord

从面向对象的角度来说,将数据操作与数据持久化两个功能放一起违反了单一功能原则。回顾一下什么是单一功能原则?每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。比如业务逻辑和存储逻辑是两个独立的模块,两者在功能上不依赖,如果把两个完全独立的功能封装在一起会导致代码耦合,这也是面向对象程序设计时要规避的。

话虽然这么说,但规定是死的,人是活的,在实际项目中又不一样了。ActiveRecord在实际项目中风驰电掣,发展迅猛,主流的编程框架基本都选择它作为ORM。用ActiveRecord ORM的PHP框架有Laravel, Yii, CodeIgniter, CakePHP等。其他语言用的有 Ruby on Rails,Django等。

ActiveRecord上手非常快,业务逻辑和持久化逻辑在一个对象里一起解决,封装越好的框架持久化逻辑对编程人员越透明,程序员甚至不用知道底层数据库使用的是MySQL还是MongoDB。

看一个调用实例:

1
2
3
4
5
6
<?php
$model = new User();
$model->id = 1;
$model->account = 'it2048';
$model->password = '123456';
$model->save();

$model 属性的修改属于业务逻辑,调用save()方法属于持久化逻辑。使用者完全不用关心save()方法执行后数据是存储到MySQL还是MongoDB,在开发过程中可以将精力全部放到业务逻辑,开发速度非常快。

三. Data Mappers

从面向对象的角度来说,将数据操作与数据持久化两个功能分开符合单一功能原则。这样设计出来的代码低耦合,扩展性强,性能有保证。对于理论派开发者来说Data Mappers肯定是首选。

但是在实际项目中Data Mappers的发展并不好,主要是出活慢。简单点说就是一个对象可以解决的事情,现在不得不用两个对象来解决,其中还有一个是全局对象(持久化逻辑)。对于代码的封装来说,全局对象的初始化和传递是大问题。初始化需要依赖框架,传递需要显示传递。这就导致我们封装的package不通用,只能在特定框架下传递特定对象才能使用。另一个问题是扩展性强就要求有大量的参数配置,开发者需要在代码层面关心具体用哪个数据库,怎样使用SQL语句性能好等,对开发者要求较高。

Data Mappers带来的好处主要体现在后期,比如需要优化性能,我们可以将一次请求中的所有SQL批量执行,这些SQL统一放在全局持久化对象中,很方便就能实现批量处理操作。这在ActiveRecord中很难做到。拿到持久化对象之后对数据的干预也会非常方便,例如MySQL表中的字段类型从枚举变成了int,在ActiveRecord中你需要查找所有代码,将该字段修正。而Data Mappers只需要在持久化对象中做个替换。

看一个调用实例:

1
2
3
4
5
6
7
8
9
10
<?php
$model = new User();
$model->setId(1);
$model->setAccount('it2048');
$model->setPassword('123456');

$entityManager = EntityManager::create($connection, $config);
$entityManager->persist($model);
//flush通常在请求结束后执行
$entityManager->flush();

$model 对象属性的修改属于业务逻辑,$entityManager对象涵括持久化逻辑。通常$entityManager对象是全局的,达到统一管理数据的目的。flush()save()方法类似,但flush()是对$entityManager中所有数据的存储,一般在请求结束时调用。

使用Data Mappers的框架数量相比ActiveRecord要少很多,主要有Java Hibernate,PHP Doctrine,SQLAlchemy in Python,EntityFramework for Microsoft .NET。

Data Mappers极大的增强了项目在ORM模块的扩展性,对在ORM模块踩过坑的开发者来说是一剂良药,但是良药苦口。

四. 如何选择ORM

上面把ActiveRecord和Data Mappers都介绍清楚了,选择哪一个需要根据实际业务需求来。如果是我的话,我会更多的考虑当前公司的发展情况,如果公司处于发展期,业务需求多,那肯定选择ActiveRecord,保证高产出最重要。如果公司处于技术沉淀期,比如开始还技术债,那就选择Data Mappers,一是可以沉淀很多技术,二是能将项目的性能与扩展性提升。

一般项目初期会选择ActiveRecord,如果项目比较成功,有一天发现ActiveRecord优化起来很吃力,要改造它的时候想到有Data Mappers,然后从ActiveRecord过度到Data Mappers,完成项目优化。这也是程序员正常的成长路径。架构被程序员开发,同样也会帮助程序员成长。

五. 参考文档

https://www.thoughtfulcode.com/orm-active-record-vs-data-mapper/

六. 最后

最近遇到很多”洗稿”的人,而且这些人的粉丝还非常多,更恶心的是这些人洗稿之后还反过来举报原创抄袭。对于原创来说打击太大了,我写文章不为挣钱,觉得不错的点个喜欢就是对作者最大的安慰。