简体   繁体   中英

Hibernate native query also gets JPA select

I have a two database tables, "A" and "B" with @OneToMany(mappedBy = "a") on a List<B> field in entity A, and a @ManyToOne on field Ba I ran into the "N+1" problem when doing default queries on A, so I am trying a native query such as:

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

This works in the sense that the data is mapped back to the entities as expected.

My problem is that I can see that Hibernate is doing a separate select for each B rather than using the results of the join. That is, I see in the console a sequence of:

  • The select that I specified
  • For each instance of A, another select for B using the ID from A

In other words, I've still got the "n+1" problem.

I figured that the @OneToMany and @ManyToOne annotations might be causing Hibernate to do these extra selects, but when I take them out, my IDE (IntelliJ) says:

'Basic' attribute should not be a container

... on the List property in A.

How can I get it to map the results back in a single select with join? Should I just give up on Hibernate and JPA?

I am using spring-boot-start-data-jpa.2.5.4

Native @Query doesn't have sufficient mapping power, so it seems that Hibernate native query must be needed.

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;
        }
    }
}

Above code executes 1 query:

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


You can use some methods avoiding N+1 problem.

Using JPQL fetch, instead of native-query:

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

Above code executes 1 query:

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


Using JPA Criteria fetch, is equivalent to above 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();
    }

Above code executes 1 query:

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


Using @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();

Above code executes 2 queries(root entities and their relational entities):

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

I ended up using the following solution, given by DEWA Kazuyuki, above. I'm copying it here because DEWA suggested several answers and I thought it useful to identify the particular one that worked for me. Thanks, 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();
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM