【问题标题】:Why does JPA/Eclipselink fire redundant database queries even with the BatchFetchType.IN hint?为什么即使使用 BatchFetchType.IN 提示,JPA/Eclipselink 也会触发冗余数据库查询?
【发布时间】:2014-06-17 05:22:14
【问题描述】:

总结:

我正在尝试尽量减少基于 JPA 的 Java 应用程序对数据库的查询次数。我指定了 @BatchFetch(BatchFetchType.IN) 优化提示,但我仍然看到一些我认为多余且不必要的额外查询。

详情:

考虑一个简单的域模型:我们有发票管理系统。 Invoice 与 Order 具有 OneToOne 关系。我们也有客户,它与订单有 OneToMany 关系。 (客户 1->M 订单 1。查找更多details here。找到完整的source code here。 这是当前的实体定义:

Client.java(不包括 getter 和 setter):

  @Entity(name = "CUSTOMER") 
  public class Customer {
    @Id //signifies the primary key
    @Column(name = "CUST_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long custId;

    @Column(name = "FIRST_NAME", length = 50)
    private String firstName;

    @OneToMany(mappedBy="customer",targetEntity=Order.class,
            fetch=FetchType.LAZY)
    private Collection<Order> orders;
    }

Order.java(不包括 getter 和 setter):

 @Entity(name = "ORDERS")
 public class Order {
    @Id
    @Column(name = "ORDER_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long orderId;

    @Column(name = "TOTAL_PRICE", precision = 2)
    private double totPrice;

    @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL,  mappedBy = "order")
    private Invoice invoice;

    @ManyToOne(optional = false)
    @JoinColumn(name = "CUST_ID", referencedColumnName = "CUST_ID")
    private Customer customer;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "ORDER_DETAIL", joinColumns = @JoinColumn(name = "ORDER_ID",    referencedColumnName = "ORDER_ID"), inverseJoinColumns = @JoinColumn(name = "PROD_ID",    referencedColumnName = "PROD_ID"))
    private List<Product> productList;

 }

Invoice.java(不包括 getter 和 setter):

@Entity(name = "ORDER_INVOICE")
public class Invoice {

    @Id
    // signifies the primary key
    @Column(name = "INVOICE_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long invoiceId;

    @Column(name = "AMOUNT_DUE", precision = 2)
    private double amountDue;

    @OneToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "ORDER_ID")
    private Order order;

 }

有了这个模型,我运行了一个简单的测试来获取客户的所有订单。

EntityManagerFactory entityManagerFactory =  Persistence.createEntityManagerFactory("testjpa");     
    EntityManager em = entityManagerFactory.createEntityManager();

    Customer customer = em.find(Customer.class, 100L);

    Collection<Order> orders = customer.getOrders();

    for(Order order: orders){
        System.out.println(order.getInvoice().getInvoiceId());
    }

    em.close();

由于所有内容都是惰性获取的,因此我们得到了四个查询,如下所示:

1398882535950|1|1|statement|SELECT CUST_ID, APPT, CITY, EMAIL, FIRST_NAME, LAST_NAME, STREET, LAST_UPDATED_TIME, ZIP_CODE FROM CUSTOMER WHERE (CUST_ID = ?)|SELECT CUST_ID, APPT, CITY, EMAIL, FIRST_NAME, LAST_NAME, STREET, LAST_UPDATED_TIME, ZIP_CODE FROM CUSTOMER WHERE (CUST_ID = 100)

1398882535981|0|1|statement|SELECT ORDER_ID, OREDER_DESC, ORDER_DATE, TOTAL_PRICE, LAST_UPDATED_TIME, CUST_ID FROM ORDERS WHERE (CUST_ID = ?)|SELECT ORDER_ID, OREDER_DESC, ORDER_DATE, TOTAL_PRICE, LAST_UPDATED_TIME, CUST_ID FROM ORDERS WHERE (CUST_ID = 100)

1398882535995|1|1|statement|SELECT INVOICE_ID, AMOUNT_DUE, DATE_CANCELLED, DATE_RAISED, DATE_SETTLED, LAST_UPDATED_TIME, ORDER_ID FROM ORDER_INVOICE WHERE (ORDER_ID = ?)|SELECT INVOICE_ID, AMOUNT_DUE, DATE_CANCELLED, DATE_RAISED, DATE_SETTLED, LAST_UPDATED_TIME, ORDER_ID FROM ORDER_INVOICE WHERE (ORDER_ID = 111)

1398882536004|0|1|statement|SELECT INVOICE_ID, AMOUNT_DUE, DATE_CANCELLED, DATE_RAISED, DATE_SETTLED, LAST_UPDATED_TIME, ORDER_ID FROM ORDER_INVOICE WHERE (ORDER_ID = ?)|SELECT INVOICE_ID, AMOUNT_DUE, DATE_CANCELLED, DATE_RAISED, DATE_SETTLED, LAST_UPDATED_TIME, ORDER_ID FROM ORDER_INVOICE WHERE (ORDER_ID = 222)

由于我不希望 N+1 次调用来获取发票,因此我想到了使用批量提取并将总查询数减少到 4(一个查询以获取所有客户订单的发票)。为此,我更新了我的 Order 实体,如下所示:

更新 - Order.java,为发票添加 BatchFetch。 (不包括 getter 和 setter):

 @Entity(name = "ORDERS")
 public class Order {
    @Id
    @Column(name = "ORDER_ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long orderId;

    @Column(name = "TOTAL_PRICE", precision = 2)
    private double totPrice;

    @BatchFetch(BatchFetchType.IN)
    @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL,  mappedBy = "order")
    private Invoice invoice;

    @ManyToOne(optional = false)
    @JoinColumn(name = "CUST_ID", referencedColumnName = "CUST_ID")
    private Customer customer;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "ORDER_DETAIL", joinColumns = @JoinColumn(name = "ORDER_ID",    referencedColumnName = "ORDER_ID"), inverseJoinColumns = @JoinColumn(name = "PROD_ID",    referencedColumnName = "PROD_ID"))
    private List<Product> productList;

 }

我再次运行了相同的测试,并假设将有 3 个查询来获取数据。(一个用于客户,一个用于订单,一个用于批量获取发票)。但是,eclipselink 生成 5 个相同的查询。以下是查询:

1398883197009|1|1|statement|SELECT CUST_ID, APPT, CITY, EMAIL, FIRST_NAME, LAST_NAME, STREET, LAST_UPDATED_TIME, ZIP_CODE FROM CUSTOMER WHERE (CUST_ID = ?)|SELECT CUST_ID, APPT, CITY, EMAIL, FIRST_NAME, LAST_NAME, STREET, LAST_UPDATED_TIME, ZIP_CODE FROM CUSTOMER WHERE (CUST_ID = 100)

1398883197030|0|1|statement|SELECT ORDER_ID, OREDER_DESC, ORDER_DATE, TOTAL_PRICE, LAST_UPDATED_TIME, CUST_ID FROM ORDERS WHERE (CUST_ID = ?)|SELECT ORDER_ID, OREDER_DESC, ORDER_DATE, TOTAL_PRICE, LAST_UPDATED_TIME, CUST_ID FROM ORDERS WHERE (CUST_ID = 100)

1398883197037|1|1|statement|SELECT INVOICE_ID, AMOUNT_DUE, DATE_CANCELLED, DATE_RAISED, DATE_SETTLED, LAST_UPDATED_TIME, ORDER_ID FROM ORDER_INVOICE WHERE (ORDER_ID IN (?,?))|SELECT INVOICE_ID, AMOUNT_DUE, DATE_CANCELLED, DATE_RAISED, DATE_SETTLED, LAST_UPDATED_TIME, ORDER_ID FROM ORDER_INVOICE WHERE (ORDER_ID IN (111,222))

1398883197042|1|1|statement|SELECT ORDER_ID, OREDER_DESC, ORDER_DATE, TOTAL_PRICE, LAST_UPDATED_TIME, CUST_ID FROM ORDERS WHERE (ORDER_ID = ?)|SELECT ORDER_ID, OREDER_DESC, ORDER_DATE, TOTAL_PRICE, LAST_UPDATED_TIME, CUST_ID FROM ORDERS WHERE (ORDER_ID = 222)

1398883197045|0|1|statement|SELECT INVOICE_ID, AMOUNT_DUE, DATE_CANCELLED, DATE_RAISED, DATE_SETTLED, LAST_UPDATED_TIME, ORDER_ID FROM ORDER_INVOICE WHERE (ORDER_ID = ?)|SELECT INVOICE_ID, AMOUNT_DUE, DATE_CANCELLED, DATE_RAISED, DATE_SETTLED, LAST_UPDATED_TIME, ORDER_ID FROM ORDER_INVOICE WHERE (ORDER_ID = 222)

我不明白为什么会生成最后两个查询。任何解释正在发生的事情的帮助都会有所帮助。

谢谢!

【问题讨论】:

  • 尝试添加一些调试语句以查看最后两个查询何时执行(即是在构建订单列表时,还是在调用 order.getInvoice() 时)。还要检查订单、客户或发票中没有任何方法可以访问对象不完整的关系——例如在 toString 方法中。
  • 所有查询都在 order.getInvoice() 语句上执行。每个对象的 toString 方法中都使用了所有字段(包括关系);但是由于订单 (111,222) 已经在第二个查询中加载,为什么在第四个查询中再次显式调用订单 222?
  • 您的意思是最后 3 个在 First order.getInvoice() 调用上执行?尝试更改您的 toString 方法以防止加载惰性属性,和/或添加调试以查看它是否是触发查询的原因。您可以将日志记录设置为最好,以便显示 EclipseLink 消息以及 SQL
  • 我很困惑。源代码 zip 文件包含对 OpenJPA(而不是 EclipseLink)的构建路径和 persistence.xml 引用。为什么你一直在谈论 EclipseLink?您是否只是对不同的 JPA 实现感到困惑,当您说 EclipseLink 时,您实际上是想说 OpenJPA,还是您不小心提供了错误的源代码文件,因为您也使用 OpenJPA 进行了测试,但在其中发现了同样的问题,因此打算忽略是吗?
  • 是的,我将提供程序更改为 EclipseLink,而不是继续使用 OpenJPA。共享代码是为了为您提供实际的数据模型,但我更改了 persistence.xml 并将提供程序更新为 EclipseLink。

标签: performance jpa optimization eclipselink


【解决方案1】:

看起来像 EclipseLink 中的一个错误/问题,因为对象模型中的急切关系的遍历允许在加载引用它的 Order 之前加载“in”中的第二个 Invoice。这会强制 Invoice 在数据库中查询 Order,而不是在缓存中查找。

您可以通过对 Invoice to Order 关系使用延迟获取来解决此问题。此延迟将允许 EclipseLink 完全构建对象模型,以便在访问时它会在缓存中。问题中的代码显示此关系被标记为惰性,但这只是对 JPA 提供者的提示,如果不使用此处所述的代理或字节码编织,则无法在 EclipseLink 中工作: https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Advanced_JPA_Development/Performance/Weaving https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Advanced_JPA_Development/Performance/Weaving/Dynamic_Weaving

惰性集合不需要编织,仅用于 1:1 和其他优化。

【讨论】:

  • 是的,使用懒惰的作品。问题仅在于急切获取发票->订单关系时。如果它被标记为惰性并使用了编织,那么它不会出现在我使用你的类的测试中。
  • 近期有什么计划在 EclipseLink 中解决这个问题?即使在 EclipseLink 2.6.3 中,这个 bug 仍然存在。
猜你喜欢
  • 1970-01-01
  • 2021-10-07
  • 2013-10-03
  • 1970-01-01
  • 2016-05-14
  • 2015-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多