高效使用JavaEE ORM框架

廖雪峰 / 文章 / ... / Reads: 6791 Edit

虽然Java领域有无数的ORM框架,如HibernateiBatis,TopLink,JDO,JPA……但是这些ORM框架基本上大同小异。很多初学者对JDBC的复杂性望而却步,就简单认为使用ORM就会省时省力,结果恰恰相反,任何好的框架都是给专家准备的,任何急功近利试图偷懒的方法往往适得其反。要正确使用ORM还真不是一件简单的事情。本文仅简单整理一下ORM的原理,基本用法,以及如何避免各种陷阱的基本编程原则。

ORM的原理

先说ORM的实现原理。其实,要实现JavaBean的属性到数据库表的字段的映射,任何ORM框架不外乎是读某个配置文件把JavaBean的属性和数据库表的字段自动关联起来,当从数据库Query时,自动把字段的值塞进JavaBean的对应属性里,当做INSERT或UPDATE时,自动把JavaBean的属性值绑定到SQL语句中。但是,几乎所有的ORM都提供“按需读取”的功能,比如一个User有id,name,email和address这4个字段,但是address字段很少用,于是ORM只读取前3个字段,直到调用User的getAddress()方法时,才去数据库中读取address的值。这个功能显然不能通过User的get/set完成,因此,ORM需要采用某种方式生成一个User类的子类,并且覆写get/set方法,这样,才能在调用get方法时有机会从数据库中读取。类似的对User的修改检测也是这样实现的。

两种增强的方式

ORM为我们自己的JavaBean实现子类的方法很多,这个过程简单称之为“增强”,基本上有两种方法:Hibernate使用CGLIB在加载我们的User类时动态创建了一个子类,而JDO则要求编译完User类后再利用它提供的工具对User类进行改造,以便实现JDO需要的各种接口。请注意:就是这种极其变态的设计导致了使用JDO的极大困难,在我们编译完源码后,还需要额外执行一个增强命令,或者额外编写Ant任务,极大地影响了快速开发和单元测试,所以,凡是采用静态生成持久类的ORM,要在第一时间直接排除,切记!

理解持久和非持久状态

所有的ORM框架都有持久和非持久的概念。简单地说,当我们new一个User实例时,它是非持久对象,当我们调用ORM的save()之类的方法后,这个实例就变成持久对象了。当我们通过ORM从数据库读取到一个User对象时,这个对象是持久对象,当关闭当前的事务后,这个对象变成非持久对象。

虽然这个过程很容易理解,但是,难点在于当我们设计一个方法时,我们必须准确地知道当前操作的对象是持久对象还是非持久对象,否则,各种灵异事件会接踵而来,比如插入了重复记录等等。举例说明,当我们需要创建一个User对象时,save(User)方法必须传入非持久对象,当我们需要更新一个User对象时,update(User)方法必须传入一个持久对象,有些ORM比如Hibernate,为了方便用户,提供了saveOrUpdate()方法,自动判断是否是持久对象,是则更新,否则创建。我的建议是永远不要使用这些看上去很简单的方法,否则将很难判断ORM到底做了什么操作,也就很难追踪到逻辑错误。

正确使用CRUD

正因为我们需要时刻区分一个对象的持久化状态,所以,编写CRUD(Create,Retrieve,Update,Delete的缩写)要严格遵循以下原则:

Create:对于Create操作,传入的永远是非持久化对象,一旦调用了create方法,就变成持久化对象;

Retrieve:所有通过ORM从数据库读取的对象都是持久化对象,直到当前会话结束;

Update:对于Update操作,传入的必须是持久化对象,而通常需要更新的对象是从页面获得的,因此,编写Update方法要按照以下步骤:

从Web页面中获得了一个User对象(包含ID),这个对象肯定是非持久化对象;

当得到该User对象时,千万不可直接做Update操作,因为从Web页面得到的数据都是不可信任的,修改HTTP请求非常简单,有经验的开发人员利用一个FireFox插件就能完成。正确的做法是根据该User对象的ID从数据库中查询到持久化的User对象,然后把待修改的属性复制到持久化的User对象中,最后Update该持久化的User对象,简单的代码如下:

void update(User u) {
    // 从数据库读取User:
    User p = load(User.class, u.getId());
    // 检查当前用户有无权限修改:
    // TODO: ...
    // 复制属性:
    p.setName(u.getName());
    p.setAddress(u.getAddress());
    // 不允许修改的属性例如email就不要复制了,然后更新:
    update(p);
}

Delete:对于Delete操作,ORM通常只关心映射到主键的ID属性,不过,正确的做法仍然是根据ID先通过ORM读取,然后判断权限,最后删除。简单的代码如下:

void delete(String id) {
    // 从数据库读取User:
    User p = load(User.class, u.getId());
    // 检查当前用户有无权限修改:
    // TODO: ...
    // 删除:
    delete(p);
}

严格按照正确的方法做CRUD操作,使用ORM才能事半功倍。

级联读取

数据库表支持外键关联,因此,ORM也可以把多个JavaBean按照数据库的外键关系联系起来,比如可以在读User对象时把其关联的Profile对象也一并读出来,即所谓级联读取。这又是一个使用起来要非常小心谨慎的功能。

首先,我的建议是级联读取的层次最好是0或1,一般不要超过3,千万不可设为无限,否则,一个简单的查询可能就读取了上万条记录,在开发时由于并发用户只有1,往往看不出问题,等到部署了发现服务器经常内存溢出,其实是级联读取太多导致的。

其次,级联有一对多和多对一两种(一对一可以并入第二种),要非常小心地使用一对多,除非有十分的把握确定“多”的一端只有不超过100条记录。比如设计论坛时读取“版面”Board时如果有一对多顺便把“话题”Topic一并读入了,随着Topic越来越多,每次读取Board的内存占用也越来越多,直到最后内存溢出。因此,我的建议是最好不用一对多,凡是有一对多的需求全部采用分页查询的方式解决。

最后,大多数ORM对级联读取都是采用join的方式,在数据量很大的情况时,join操作很慢,而且无法水平分割数据库表。对读取要求很高的应用,最好不要设置级联读取。

缓存

绝大多数ORM都会提供缓存,通常还分为一级缓存和二级缓存。一级缓存只在当前会话内有效,当我们在一个会话里反复读取同一个对象时,只有第一次ORM会从数据库中读取,后续请求会直接从缓存中读取,例如:

int id=12345;
User u1 = load(User.class, id); // 从数据库读
User u2 = load(User.class, id); // 从一级缓存读
System.out.println(u1==u2); // True

实际上,很少有人会写出这样的代码,所以,一级缓存几乎没有什么作用。

而二级缓存就作用于整个应用。不过,当数据量很小的时候,通过增大数据库服务器的内存就和使用缓存没什么区别,当数据量非常大的时候,二级缓存的命中率很低,原因当然是缓存大小不够,因此,针对海量数据通常都要自己动手,用memcached做专门的缓存服务器来提高性能。所以,二级缓存不开也罢。

确定事务范围,小心使用Lasy Loading

使用ORM也需要对数据库事务有一定了解,通常ORM的一个会话对应一个数据库事务,如果事务持续时间长,占用的数据库资源就长,数据库并发处理能力就会降低,所以,事务范围越短越好。对于Web应用,把事务限制在Controller中就比限制在Controller+View中要短,通过MVC框架提供的各种拦截器可以很方便地开启和关闭事务。当事务限制在Controller时,到了View渲染的时候,就无法使用LasyLoading功能了。Lasy Loading虽然简单,但不当使用也会造成严重的性能问题,所以还是不用为妙。

以上对ORM框架的使用做了一个简单的概括,实际应用中还需要通过大量实践慢慢探索。

Comments

Make a comment

Author: 廖雪峰

Publish at: ...

关注公众号不定期领红包:

加入知识星球社群:

关注微博获取实时动态: