0

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

Mark
  • 83
  • 2
  • 6
  • 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.) – Mark Oct 29 '21 at 04:40
  • you can place your comment as Answer and accept it ;) – Nikolai Shevchenko Oct 29 '21 at 08:18

2 Answers2

1

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 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. – Mark Oct 29 '21 at 23:36
  • 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? – Mark Oct 29 '21 at 23:44
  • I ended up with: `final List 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;` – Mark Oct 29 '21 at 23:49
  • 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. – Mark Oct 30 '21 at 00:33
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();
}
Mark
  • 83
  • 2
  • 6