Hibernate默认将实体及所有关联设为只读的实现方案
针对你的需求,结合Hibernate 3.6和Spring 3.2的技术栈,我整理了几种实用的方案,既能实现默认只读的行为,也能支持按需开启修改持久化的能力:
1. 查询层面设置只读(最灵活的方案,推荐)
Spring的HibernateTemplate和Hibernate原生Session都支持标记查询出的实体为只读状态,这是最符合你“默认只读、可配置开启修改”需求的方式。
实现方式:
在DAO层的查询方法中,默认开启只读模式,需要修改时提供重载方法关闭只读:
// 默认只读的查询方法 public List<Foo> getList() { return getList(true); } // 可配置只读状态的重载方法 public List<Foo> getList(boolean readOnly) { HibernateTemplate template = getHibernateTemplate(); template.setReadOnly(readOnly); List<Foo> fooList = template.find("from Foo"); // 显式标记关联的Customer为只读(确保关联对象也被控制) if (readOnly) { for (Foo foo : fooList) { // 初始化懒加载的Customer(如果fetch是LAZY) Hibernate.initialize(foo.getCustomer()); template.setReadOnly(foo.getCustomer(), true); } } return fooList; }
或者用原生Hibernate Session操作:
public List<Foo> getList(boolean readOnly) { Session session = getSession(); try { List<Foo> fooList = session.createQuery("from Foo").list(); for (Foo foo : fooList) { Hibernate.initialize(foo.getCustomer()); session.setReadOnly(foo, readOnly); session.setReadOnly(foo.getCustomer(), readOnly); } return fooList; } finally { releaseSession(session); } }
原理:标记为只读的实体,即使在Session范围内,Hibernate也不会跟踪其修改,调用setter方法的变更不会被持久化到数据库。需要修改时,只需调用getList(false)即可恢复正常的持久化能力。
2. 实体级注解实现默认只读(@Immutable)
如果希望Foo实体默认全局只读,可以使用Hibernate的@Immutable注解:
@Entity @Immutable // 标记整个Foo实体为只读 public class Foo{ @ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name = "customer_id") private Customer customer; @Basic @Column(name = "firstname") private String firstName; // getter方法... // 即使有setter方法,修改也不会被持久化 }
注意点:
@Immutable仅作用于当前实体(Foo),关联的Customer不会自动变为只读。如果需要Customer也只读,可以给Customer类也加@Immutable,但这会影响所有使用Customer的场景,需要谨慎评估。- 这种方式是全局生效的,无法灵活开关,适合确实不需要修改的实体。
3. 关联对象的不可变代理(严格禁止修改)
如果你希望调用Customer的setter时直接抛出异常(而不是静默不生效),可以通过动态代理包装关联对象:
public class Foo{ @ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name = "customer_id") private Customer customer; // 默认返回只读代理 public Customer getCustomer() { return getCustomer(true); } // 可配置是否返回只读代理 public Customer getCustomer(boolean readOnly) { if (!readOnly) { return customer; } // 初始化懒加载的Customer Hibernate.initialize(customer); // 创建动态代理,拦截setter方法 return (Customer) Proxy.newProxyInstance( Customer.class.getClassLoader(), new Class[]{Customer.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().startsWith("set")) { throw new UnsupportedOperationException("Customer对象为只读,无法修改"); } return method.invoke(customer, args); } } ); } }
优点:能明确阻止修改操作,避免开发者无意识的修改;缺点:需要处理懒加载初始化,且代理对象无法直接用于Hibernate的持久化操作(如果需要修改的话,要调用重载方法返回原对象)。
4. 投影查询/DTO(彻底隔离持久化层)
如果你的场景只需要展示数据,不需要将实体对象用于后续的持久化操作,可以使用投影查询返回DTO(数据传输对象):
// 定义FooDTO类 public class FooDTO { private String firstName; private String customerSomeField; // 构造方法用于Hibernate投影查询 public FooDTO(String firstName, String customerSomeField) { this.firstName = firstName; this.customerSomeField = customerSomeField; } // getter方法... // 可以不提供setter,或者setter仅修改内存数据 } // DAO层查询方法 public List<FooDTO> getFooDTOList() { String hql = "select new com.example.FooDTO(f.firstName, c.someField) from Foo f join f.customer c"; return getHibernateTemplate().find(hql); }
优点:完全脱离Hibernate的持久化上下文,绝对安全,而且只查询需要的字段,性能更好;缺点:无法直接将DTO转换为持久化实体,如果需要修改数据,需要额外处理。
对你当前evict方案的补充
你现在手动调用evict的方式确实能让对象脱离Session,修改不会被持久化,但存在两个问题:
- 脱离Session后,懒加载的关联对象无法再初始化;
- 如果之后该对象被重新关联到Session(比如通过
merge或update),修改还是会被持久化。
相比之下,使用setReadOnly的方式更可靠,因为对象仍在Session中,既可以正常使用懒加载,又不会被持久化修改。
内容的提问来源于stack exchange,提问作者Christoph

