1

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 B.a. 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

2
  • It occurs to me that the "left join" in my query creates a sort of view that combines rows from tables A and B. That is, each row of the result set is a combination of the columns from both A and B. So one solution would be to define an entity (I'll call it AB) that represents the flattened combination and arrange it so that Hibernate maps the result set into AB. That way, Hibernate won't see a @OneToMany mapping and won't do those additional selects. (This does seem like a real hack.) Commented Oct 29, 2021 at 4:40
  • you can place your comment as Answer and accept it ;) Commented Oct 29, 2021 at 8:18

2 Answers 2

2

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

Sign up to request clarification or add additional context in comments.

4 Comments

I tried this solution but it did not resolve my problem. I still see the initial select statement (the one I specify to the createNativeQuery method) followed by multiple individual selects, one per "top level" result. I also found that Hibernate threw StackOverflow exceptions because it resolved A->B->A->B as a cycle. I fixed that by dropping the @ManyToOne on the A side and querying B first.
Also, the native query SQL syntax shown above is wrong: it should be more like "select * from A ...". Unless the example is intended as a JPQL query?
I ended up with: final List<B> 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(); where Aref is defined as private A Aref;
I figured out that what you mean by select {a.*},{b.*} is "select each individual field in A and B. I tried that and discovered that I get a runtime SQLGrammerException if I don't list every column in the two tables. Which is not desirable because it means I can't ignore any extra columns even if they aren't mapped to entity fields.
0

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.