作者:廖雪峰

 

     虽然Java领域有无数的ORM框架,如Hibernate ,iBatis ,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

Retrieve

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



view source 
    print 
    ? 
   
 
  
 
    1. 
    void delete(String id) { 
 
   
 
    2. 
     // 从数据库读取User: 
 
   
 
    3. 
     User p = load(User. class , u.getId()); 
 
   
 
    4. 
     // 检查当前用户有无权限修改: 
 
   
 
    5. 
     // TODO: ... 
 
   
 
    6. 
     // 删除: 
 
   
 
    7. 
     delete(p); 
 
   
 
    8. 
    }



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

级联读取

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

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

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

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

缓存

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






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



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

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

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

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

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