如何避免1 + n数据库调用Hibernate中的双向可选一对一关联?

coo*_*xie 5 hibernate jpa spring-data-jpa

拥有以下简化实体:

@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;
}

@Entity
@Table(name = "t_invoice")
public class Invoice extends AbstractEntity {
    @OneToOne(optional = false, fetch = FetchType.EAGER)
    @JoinColumn(name = "order_id")
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private Order order;
}

@Entity
@Table(name = "t_order")
public class Order extends AbstractEntity {
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    @SortNatural
    private SortedSet<OrderLine> orderLines = new TreeSet<>();

    @OneToOne(optional = true, mappedBy = "order", fetch = FetchType.EAGER)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private Invoice invoice;
}
Run Code Online (Sandbox Code Playgroud)

以及使用Spring数据的存储库

public interface InvoiceRepository extends JpaRepository<Invoice, Long> {
    List<Invoice> findDistinctByInvoiceDateBetween(LocalDate from, LocalDate until);
}
Run Code Online (Sandbox Code Playgroud)

使用存储库方法获取发票时1 + n执行SQL语句,如日志中所示:

SELECT DISTINCT i.id, ... FROM t_invoice i WHERE i.invoice_date BETWEEN ? AND ?;
SELECT i.id, ... FROM t_invoice i WHERE i.order_id = ?;
SELECT i.id, ... FROM t_invoice i WHERE i.order_id = ?;
... n
Run Code Online (Sandbox Code Playgroud)

这个 SO答案我了解到,当具有一对一可选关联时,Hibernate需要进行n次数据库调用以确定可选发票是否为空.令我困惑的是,Hibernate已经在初始查询中提取了有问题的发票,那么为什么不使用已经提取的发票中的数据呢?

我也试图避免使用@NamedEntityGraph和@NamedSubgraph,以便热切填充发票n个电话.

因此现在发票实体看起来像:

@Entity
@NamedEntityGraph(
        name = Invoice.INVOICE_GRAPH,
        attributeNodes = {
                @NamedAttributeNode(value = "order", subgraph = "order.subgraph")
        },
        subgraphs = {
                @NamedSubgraph(name = "order.subgraph", attributeNodes = {
                        @NamedAttributeNode("invoice"),
                        @NamedAttributeNode("orderLines")
                }),
        }
)
@Table(name = "t_invoice")
public class Invoice extends AbstractEntity {
@OneToOne(optional = false, fetch = FetchType.EAGER)
    @JoinColumn(name = "order_id")
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private Order order;
}
Run Code Online (Sandbox Code Playgroud)

并且存储库中的方法如下所示:

@EntityGraph(value = Invoice.INVOICE_GRAPH, type = EntityGraph.EntityGraphType.LOAD)
List<Invoice> findDistinctByInvoiceDateBetween(LocalDate from, LocalDate until);
Run Code Online (Sandbox Code Playgroud)

但是,即使第一个sql select子句包含发票数据两次,它仍会进行n次数据库调用,如您所见:

SELECT DISTINCT
    invoice0_.id                      AS id1_13_0_,
    order1_.id                        AS id1_14_2_,
    orderlines4_.id                   AS id1_15_4_,
    invoice5_.id                      AS id1_13_5_,
    invoice0_.created                 AS created2_13_0_,
    order1_.created                   AS created2_14_2_,
    orderlines4_.created              AS created2_15_4_,
    invoice5_.created                 AS created2_13_5_,
FROM t_invoice invoice0_ ... more join clausules ... 
WHERE invoice0_.order_id = order1_.id AND (invoice0_.invoice_date BETWEEN ? AND ?)
Run Code Online (Sandbox Code Playgroud)

那么现在我想知道如何避免n次额外调用以按顺序填充发票?

mer*_*ike 4

我知道当有一对一的可选关联时,Hibernate 需要进行 n 次数据库调用来确定可选发票是否为空

是的。更准确地说,hibernate 不支持可选 ToOne 关联的惰性,因此它将始终加载关联数据。

让我困惑的是,Hibernate 已经在初始查询中获取了有问题的发票,那么为什么它不使用已获取的发票中的数据呢?

Hibernate 没有意识到它已经加载了该发票。为此,它要么必须按其 order_id 保留 Invoice 对象的映射,要么对相互的 OneToOne 关联进行特殊处理。OneToOne 关联很少见,因此没有这样的处理。

可以通过以下任一方法解决此问题:

  • 仅映射关联的一端,并使用查询在另一个方向上导航(即,当您需要订单的发票时,请执行“从发票中选择发票,其中发票.order = ?”)
  • 映射关联的两端,但将可选端设置为主映射,即将外键从订单移动到发票。这样,订单的发票由其主键引用,hibernate 将足够智能,首先在持久性上下文中查找,而发票的订单是惰性的,因此 hibernate 将仅在以下情况下发出冗余查询:属性被实际访问。

对于您的问题,哪一个是更好的解决方案取决于对该数据进行操作的其他查询。一般来说,我倾向于第一个选项,因为对于程序员来说,必须查询未映射的数据比必须执行神秘的技巧来让 JPA 不加载隐式请求的数据更容易理解。