基本上,使用一条查询语句加载整个表并链接对象应该是一个非常简单的任务,但是JPA的工作方式与此不同,如下例所示。
最大的问题在于@OneToMany
/@ManyToMany
关系:
@Entity
public class Employee {
@Id
@Column(name="EMP_ID")
private long id;
...
@OneToMany(mappedBy="owner")
private List<Phone> phones;
...
}
@Entity
public class Phone {
@Id
private long id;
...
@ManyToOne
@JoinColumn(name="OWNER_ID")
private Employee owner;
...
}
FetchType.EAGER
如果定义为FetchType.EAGER
,并且查询SELECT e FROM Employee e
,Hibernate将生成SQL语句SELECT * FROM EMPLOYEE
,紧接着是SELECT * FROM PHONE WHERE OWNER_ID=?
,对于每个加载的Employee
,通常称为1 + n问题。
我可以通过使用JPQL查询SELECT e FROM Employee e JOIN FETCH e.phones
来避免n + 1问题,这将导致类似于SELECT * FROM EMPLOYEE LEFT OUTER JOIN PHONE ON EMP_ID = OWNER_ID
的结果。
问题在于,这对于涉及约20个表的复杂数据模型不起作用。
FetchType.LAZY
如果定义为FetchType.LAZY
,查询SELECT e FROM Employee e
将仅将所有员工作为代理加载,在访问phones
时才加载相关电话,最终也会导致1 + n问题。
要避免这种情况,很明显只需将所有电话加载到同一个会话中SELECT p FROM Phone p
。但是当访问phones
时,Hibernate仍将执行SELECT * FROM PHONE WHERE OWNER_ID=?
,因为Hibernate不知道当前会话中已经有所有电话。
即使使用第二级缓存,语句也将在数据库上执行,因为Phone
在第二级缓存中是按其主键索引的,而不是按OWNER_ID
索引的。
结论
Hibernate中没有像“只加载所有数据”这样的机制。
似乎没有其他方法,只能保持关系瞬态并手动连接它们,甚至只使用普通的JDBC。
编辑:
我刚刚找到了一个非常好的解决方案。我将所有相关的@ManyToMany
和@OneToMany
定义为FetchType.EAGER
,并与@Fetch(FetchMode.SUBSELECT)
组合,将所有@ManyToOne
与@Fetch(FetchMode.JOIN)
组合,这导致了可以接受的加载时间。除了将javax.persistence.Cacheable(true)
添加到所有实体之外,我还将org.hibernate.annotations.Cache
添加到每个相关集合中,这启用了第二级缓存中的集合缓存。我禁用了第二级缓存超时驱逐,并通过@Singleton
EJB与@Startup
在服务器启动/部署时“预热”第二级缓存。现在我可以100%控制缓存,直到我手动清除它,不会有进一步的数据库调用。