使用 JPA 将包括关系在内的整个表加载到内存中

Load entire tables including relationships into memory with JPA

我必须处理分布在 20 个表上的大量数据(总计约 500 万条记录),我需要高效地加载它们。

我正在使用 Wildfly 14 和 JPA/Hibernate。

由于最后,每条记录都会被业务逻辑使用(在同一个事务中),我决定通过简单的方式将所需表的全部内容预加载到内存中:

em.createQuery("SELECT e FROM Entity e").size();

之后,每个对象都应该在交易中可用,因此可以通过以下方式使用:

em.find(Entity.class, id);

但这在某种程度上不起作用,仍然有很多对数据库的调用,尤其是对于关系。

如何有效地加载所需表格的全部内容,包括 关系并确保我得到了一切/不会有进一步的数据库调用?

我已经尝试过的:

需要注意的一件事是数据是不可变的(至少在特定时间内)并且也可以用于其他事务。

编辑:

我的计划是在 @Singleton bean 中加载和管理整个数据。但我想确保我以最有效的方式加载它,并确保加载了整个数据。当业务逻辑正在使用数据时,应该不需要进一步的查询。在特定时间(ejb 计时器)之后,我将丢弃整个数据并从数据库重新加载当前状态(总是 whole tables)。

我明白您的要求,但 JPA/Hibernate 不会想要为您缓存那么多数据,或者至少我不希望它能提供保证。假设您描述了 500 万条记录。每条记录的平均长度是多少? 100 字节提供 500 兆字节的内存,这只会让您未经调整的 JVM 崩溃。平均可能更像是 5000 字节,即 25 gB 内存。你需要考虑你要的是什么。

如果你想缓存它,你应该自己做,或者最好只在你有结果时使用它们。如果你想要一个基于内存的数据访问,你应该看看专门针对它的技术。 http://www.ehcache.org/ 似乎很受欢迎,但这取决于您,您应该确保首先了解您的用例。

如果你想提高数据库效率,那么你应该了解你在做什么,仔细设计和测试。

请记住,您可能需要 64 位 JVM 和大量内存。看看Hibernate 2nd Level Cache。由于我们没有您的代码,因此需要检查一些事项:

  1. @Cacheable 注释将提示 Hibernate,以便实体可缓存
  2. 配置二级缓存以使用类似 ehcache 的缓存,并将最大内存元素设置为足够大以适合您的工作集
  3. 确保您没有在代码中不小心使用多个会话。

如果您需要以这种方式处理事情,您可能需要考虑更改您的设计以不依赖于将所有内容都存储在内存中,不使用 Hibernate/JPA,或者不使用应用程序服务器。这将使您更好地控制事情的执行方式。这甚至可能更适合 Hadoop 之类的东西。没有更多信息,很难说哪个方向最适合你。

基本上,加载 整个 tables 应该是一项非常简单的任务,每个 table 和 link 对象都需要一个查询,但 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 table 的复杂数据模型。

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。

即使使用二级缓存,语句也会在数据库上执行,因为 Phone 在二级缓存中由其主键而不是 OWNER_ID 索引。

结论

在 Hibernate 中没有像 "just load all data" 这样的机制。

似乎没有其他办法,只能保持暂时的关系并手动连接它们,甚至只使用普通的旧 JDBC。

编辑:

我刚刚找到了一个非常有效的解决方案。我将所有相关的 @ManyToMany@OneToMany 定义为 FetchType.EAGER@Fetch(FetchMode.SUBSELECT) 的组合以及所有 @ManyToOne@Fetch(FetchMode.JOIN) 的组合,这导致了 acceptable 加载时间。在向所有实体添加 javax.persistence.Cacheable(true) 之后,我向每个相关集合添加了 org.hibernate.annotations.Cache,这使得在二级缓存中启用集合缓存。我在服务器启动/部署时通过 @Singleton EJB 结合 @Startup 禁用了二级缓存超时驱逐和 "warm up" 二级缓存。现在我对缓存有 100% 的控制权,在我手动清除它之前没有进一步的数据库调用。