我必须处理分布在20个表中的大量数据(总计约500万条记录),并且需要有效地加载它们。
我正在使用Wildfly 14和JPA / Hibernate。
从最后开始,每条记录将由业务逻辑使用(在同一事务中),因此我决定通过以下方式将所需表的全部内容预加载到内存中:
em.createQuery("SELECT e FROM Entity e").size();
此后,每个对象都应该在事务中可用,因此可以通过以下方式使用:
em.find(Entity.class, id);
但这不能以某种方式起作用,并且仍然有很多对数据库的调用,尤其是对于关系。
如何有效加载所需表的全部内容,包括 关系,并确保我掌握了一切/不会再有DB调用了?
我已经尝试过的:
em.find
要注意的一件事是数据是不可变的(至少在特定时间内),并且还可以在其他事务中使用。
我的计划是在@Singleton
bean中加载和管理整个数据。但是我想确保以最有效的方式加载它,并确保加载了整个数据。当业务逻辑正在使用数据时,就不需要进一步的查询。在特定时间(ejb计时器)之后,我将丢弃整个数据并从数据库中重新加载当前状态(始终整个表)。
答案 0 :(得分:6)
请记住,您可能需要64位JVM和大量内存。看一下Hibernate 2nd Level Cache。由于我们没有您的代码,因此需要检查一些事情:
@Cacheable
注释将提示Hibernate,以便该实体可缓存如果您需要以这种方式处理事物,则可能需要考虑更改设计,以使其不依赖于将所有内容存储在内存中,不使用Hibernate / JPA或不使用应用程序服务器。这将使您更好地控制事物的执行方式。这甚至更适合Hadoop之类的东西。没有更多信息,很难说哪个方向最适合您。
答案 1 :(得分:5)
我理解您的要求,但是JPA / Hibernate不会想要为您缓存那么多数据,或者至少我不希望得到它的保证。假设您描述了500万条记录。每条记录的平均长度是多少? 100字节提供500兆字节的内存,这只会使未损坏的JVM崩溃。大概更像是5000个字节,这就是25 gB的内存。您需要考虑自己的要求。
如果您希望将其缓存,则应该自己做,或者做得更好,但是只要有结果就使用它们。如果您想要基于内存的数据访问,则应查看专门用于该技术的技术。 http://www.ehcache.org/似乎很流行,但这取决于您,您应该确保首先了解用例。
如果您想提高数据库效率,那么您应该了解自己的工作,并仔细设计和测试。
答案 2 :(得分:2)
从根本上来说,加载整个表只是一个简单的任务,每个表一个查询并链接对象,但是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会在加载的每个SELECT * FROM EMPLOYEE
的{{1}}之后紧随SELECT * FROM PHONE WHERE OWNER_ID=?
生成SQL语句,这是众所周知的作为 1 + n个问题。
我可以通过使用JPQL查询Employee
来避免n + 1问题,这将导致类似SELECT e FROM Employee e JOIN FETCH e.phones
的情况。
问题是,这对于包含约20个表的复杂数据模型不起作用。
FetchType.LAZY
如果定义为SELECT * FROM EMPLOYEE LEFT OUTER JOIN PHONE ON EMP_ID = OWNER_ID
,查询FetchType.LAZY
只会将所有Employees加载为代理,仅在访问SELECT e FROM Employee e
时才加载相关的Phones,最终将导致1 + n问题。
要避免这种情况,很明显,就是将所有电话都加载到同一会话phones
中。但是访问SELECT p FROM Phone p
时,Hibernate仍将执行phones
,因为Hibernate不知道当前会话中已经有所有电话。
即使使用二级缓存,该语句也会在DB上执行,因为SELECT * FROM PHONE WHERE OWNER_ID=?
是通过二级缓存中的主键而不是Phone
来索引的。
结论
在Hibernate中没有像“仅加载所有数据”这样的机制。
似乎没有其他方法可以保持关系短暂并手动连接,甚至只是使用普通的旧JDBC。
编辑:
我刚刚找到了一个很好的解决方案。我将所有相关的OWNER_ID
和@ManyToMany
定义为@OneToMany
与FetchType.EAGER
组合在一起,而所有@Fetch(FetchMode.SUBSELECT)
与@ManyToOne
组合在一起,这将导致可接受的加载时间。除了向所有实体添加@Fetch(FetchMode.JOIN)
之外,我还向每个相关集合添加了javax.persistence.Cacheable(true)
,从而在第二级缓存中启用了集合缓存。我禁用了第二级缓存超时驱逐,并在服务器启动/部署时通过org.hibernate.annotations.Cache
EJB和@Singleton
结合“预热”了第二级缓存。现在,我已经100%地控制了缓存,直到手动清除缓存之后,再没有其他数据库调用。