【问题标题】:Hibernate native query also gets JPA selectHibernate 本机查询也获得 JPA 选择
【发布时间】:2021-12-14 03:34:37
【问题描述】:

我有两个数据库表,“A”和“B”,@OneToMany(mappedBy = "a") 在实体 A 的 List 字段上,@ManyToOne 在字段 B.a 上。我在对 A 进行默认查询时遇到了“N+1”问题,所以我正在尝试一个本机查询,例如:

@Query(value="select * from A as a left join B as b " +
            "on a.ID = b.b ",
            nativeQuery=true)

这在数据按预期映射回实体的意义上是有效的。

我的问题是我可以看到 Hibernate 正在为每个 B 进行单独的选择,而不是使用连接的结果。也就是说,我在控制台中看到了以下序列:

  • 我指定的选择
  • 对于 A 的每个实例,使用 A 中的 ID 为 B 选择另一个实例

换句话说,我还有“n+1”的问题。

我认为 @OneToMany 和 @ManyToOne 注释可能会导致 Hibernate 执行这些额外的选择,但是当我将它们取出时,我的 IDE (IntelliJ) 会说:

'Basic' attribute should not be a container

...在 A 中的 List 属性上。

如何使用连接将结果映射回单个选择?我应该放弃 Hibernate 和 JPA 吗?

我正在使用 spring-boot-start-data-jpa.2.5.4

【问题讨论】:

  • 我突然想到,我的查询中的“左连接”创建了一种视图,它将表 A 和 B 中的行组合在一起。也就是说,结果集中的每一行都是列的组合来自 A 和 B。因此,一种解决方案是定义一个实体(我称之为 AB),它表示扁平组合并对其进行排列,以便 Hibernate 将结果集映射到 AB。这样一来,Hibernate 就不会看到 @OneToMany 映射,也不会进行那些额外的选择。 (这似乎是一个真正的黑客攻击。)
  • 您可以将您的评论作为答案并接受它;)

标签: java spring-boot hibernate jpa


【解决方案1】:

native@Query没有足够的映射能力,看来必须要Hibernate原生查询。

import java.util.ArrayList;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.hibernate.Session;
import org.hibernate.transform.BasicTransformerAdapter;
import org.springframework.stereotype.Repository;

// https://docs.spring.io/spring-data/jpa/docs/2.5.6/reference/html/#repositories.custom-implementations
@Repository
public class CustomizedARepositoryImpl implements CustomizedARepository {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<A> getAll() {
        // https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#sql-entity-associations-query
        final Session sess = (Session) entityManager.getDelegate();
        final List<A> res = sess
            // If no duplicate column names, original sql can be used, too.
            .createNativeQuery("select {a.*},{b.*} from A as a left join B as b on a.ID = b.a ")
            .addEntity("a", A.class)
            .addJoin("b", "a.bs")
            .setResultTransformer(DistinctResultTransformer.INSTANCE)
            .list();

        return res;
    }

    // https://stackoverflow.com/q/12071014/4506703
    static class DistinctResultTransformer extends BasicTransformerAdapter {
        private static final long serialVersionUID = 1L;

        static final DistinctResultTransformer INSTANCE = new DistinctResultTransformer();

        @Override
        public List transformList(final List collection) {
            final List<Object> res = new ArrayList<>();
            for (final Object[] obj : (List<Object[]>) collection) {
                if (!res.contains(obj[0])) {
                    res.add(obj[0]);
                }
            }
            return res;
        }
    }
}

以上代码执行 1 个查询:

select a.id as id1_0_0_, a.name as name2_0_0_,b.a as a3_1_0__, b.id as id1_1_0__, b.id as id1_1_1_, b.a as a3_1_1_, b.name as name2_1_1_
from A as a left join B as b on a.ID = b.a

full sample code


您可以使用一些方法来避免 N+1 问题。

使用 JPQL 获取,而不是原生查询:

    @Query("select distinct a from A a left join fetch a.bs")
    List<A> getAllJpqlFetch();

以上代码执行 1 个查询:

select distinct a0_.id as id1_0_0_, bs1_.id as id1_1_1_, a0_.name as name2_0_0_, bs1_.a as a3_1_1_, bs1_.name as name2_1_1_, bs1_.a as a3_1_0__, bs1_.id as id1_1_0__ 
from a a0_ left outer join b bs1_ on a0_.id=bs1_.a

diff


使用JPA Criteria fetch,相当于上面的JPQL:

@Repository
public class CustomizedARepositoryImpl implements CustomizedARepository {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<A> getAllCriteria() {
        // https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#criteria-from-fetch
        final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        final CriteriaQuery<A> criteria = builder.createQuery(A.class);
        final Root<A> root = criteria.from(A.class);
        root.fetch("bs", JoinType.LEFT);
        criteria.select(root).distinct(true);
        return entityManager.createQuery(criteria).getResultList();
    }

以上代码执行 1 个查询:

select distinct a0_.id as id1_0_0_, bs1_.id as id1_1_1_, a0_.name as name2_0_0_, bs1_.a as a3_1_1_, bs1_.name as name2_1_1_, bs1_.a as a3_1_0__, bs1_.id as id1_1_0__ 
from a a0_ left outer join b bs1_ on a0_.id=bs1_.a

diff


使用@Fetch(FetchMode.SUBSELECT):

import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
// ...

@Entity
public class A {

    @OneToMany(mappedBy = "a")
    @Fetch(FetchMode.SUBSELECT)
    private List<B> bs;

    // ...
}
// findAll() method implementation is auto-generated by Spring Data JPA
// https://docs.spring.io/spring-data/jpa/docs/2.5.6/reference/html/#repositories.core-concepts
repository.findAll();

以上代码执行 2 个查询(根实体及其关系实体):

select a0_.id as id1_0_, a0_.name as name2_0_ from a a0_

select bs0_.a as a3_1_1_, bs0_.id as id1_1_1_, bs0_.id as id1_1_0_, bs0_.a as a3_1_0_, bs0_.name as name2_1_0_ 
from b bs0_ where bs0_.a in (select a0_.id from a a0_)

diff

【讨论】:

  • 我尝试了这个解决方案,但它没有解决我的问题。我仍然看到最初的选择语句(我指定给 createNativeQuery 方法的那个),然后是多个单独的选择,每个“顶级”结果一个。我还发现 Hibernate 抛出了 StackOverflow 异常,因为它将 A->B->A->B 解析为一个循环。我通过将 @ManyToOne 放在 A 侧并首先查询 B 来解决此问题。
  • 另外,上面显示的原生查询 SQL 语法是错误的:它应该更像“select * from A ...”。除非该示例旨在作为 JPQL 查询?
  • 我最终得到:final List&lt;B&gt; b = session .createNativeQuery("select * from B as b " + "left join A as a " + "on b.Aref = a.ID ") .addEntity("B", B.class) .addJoin("A", "B.Aref") .list();,其中 Aref 定义为 private A Aref;
  • 我发现select {a.*},{b.*} 的意思是“选择 A 和 B 中的每个单独的字段。我试过了,发现如果我没有在这两个表。这是不可取的,因为这意味着我不能忽略任何额外的列,即使它们没有映射到实体字段。
【解决方案2】:

我最终使用了上面 DEWA Kazuyuki 提供的以下解决方案。我在这里复制它是因为 DEWA 提出了几个答案,我认为确定对我有用的特定答案很有用。谢谢,DEWA。

@Repository
public class CustomizedARepositoryImpl implements CustomizedARepository {

@PersistenceContext
private EntityManager entityManager;

@Override
public List<A> getAllCriteria() {
    // https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#criteria-from-fetch
    final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    final CriteriaQuery<A> criteria = builder.createQuery(A.class);
    final Root<A> root = criteria.from(A.class);
    root.fetch("bs", JoinType.LEFT);
    criteria.select(root).distinct(true);
    return entityManager.createQuery(criteria).getResultList();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-19
    • 2011-03-09
    相关资源
    最近更新 更多