Search code examples
hibernatecriteriahibernate-criteria

Hibernate query - select one Object which have only one Set element which meets criteria


I have those 3 classes:

public class A{
  private String name;
}

public class B{
  private A aObj;
}

public class C{
  private Set<B> bObj;
}

and the following Criteria:

Session session = openSession();
Criteria c1 = session.createCriteria(C.class);
Criteria c2 = c1.createCriteria("bObj");
Criteria c3 = c2.createCriteria("aObj");
c2.add(Restrictions.eq("name",name));

which work good and the c1.uniqueResult() is the expected one (name is unique)

And now the question: Is any way to get the C object with only one element in Set which contain only that bObj which meets the criteria c2? (supposing that the Set bObj have more than 1 element)

UPDATE 1: actual result is (C.class as JSON)

{ bObj : [ 
   1: { aObj : 
     { name : name1}},
   2: { aObj : 
     { name : name2}},
   3: { aObj : 
     { name : name3}}
 ]} 

expected result for name = name1 is :

{ bObj : [ 
   1: { aObj : 
     { name : name1}}
   ]
}

so after query the bObj will have only one element in list which meets the criteria name = name1


Solution

  • There are many ways to do that, maybe you can use one of the following which fits your needs best.

    E.g. if you have an example instance of B handy, use the CriteriaBuilder. Just pass the example object (bObj) which you expect to be a member of the association and specify the associations size:

    final Root<C> selection = createQuery.from(C.class);
    createQuery.select(selection).where(
                criteriaBuilder.isMember(bObj, selection.<Set<B>>get("bObj")),
                criteriaBuilder.equal(criteriaBuilder.size(selection.<Set<B>>get("bObj")), 1));
    

    If you like Criteria more, just define the (implicit) join behavior for the selection and add your restrictions:

    final Criteria criteria = entityManager.unwrap(Session.class).createCriteria(C.class, "c");
    criteria.createAlias("c.bObj", "b");
    criteria.createAlias("b.aObj", "a");
    criteria.add(Restrictions.sizeEq("c.bObj", 1));
    criteria.add(Restrictions.eq("a.name", "test"));
    

    Or if you like JPQL, just use something like:

    final TypedQuery<C> query = entityManager.createQuery(
                    "SELECT c from C c JOIN c.bObj b JOIN b.aObj a where c.bObj.size = 1 AND a.name = :name ", C.class);
    query.setParameter("name", "test");
    

    If you need something to play with, here is a test to try that out. Just change the name ("test") or uncomment the second association member to test the restrictions:

    import static org.hamcrest.CoreMatchers.is;
    import static org.junit.Assert.assertThat;
    
    import java.util.List;
    import java.util.Set;
    
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    import javax.persistence.TypedQuery;
    import javax.persistence.criteria.CriteriaBuilder;
    import javax.persistence.criteria.CriteriaQuery;
    import javax.persistence.criteria.Root;
    
    import org.hibernate.Criteria;
    import org.hibernate.Session;
    import org.hibernate.criterion.Restrictions;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.transaction.annotation.Transactional;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    @Transactional
    public class ListTests {
    
        @PersistenceContext
        private EntityManager entityManager;
    
        @Test
        public void criteriaBuilderTest() {
            final A aObj = new A();
            aObj.setName("test");
            entityManager.persist(aObj);
    
            final B bObj = new B();
            bObj.setaObj(aObj);
            entityManager.persist(bObj);
    
            final B bObj2 = new B();
            bObj2.setaObj(aObj);
            entityManager.persist(bObj2);
    
            final C cObj = new C();
            cObj.getbObj().add(bObj);
            // cObj.getbObj().add(bObj2);
            entityManager.persist(cObj);
    
            final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
            final CriteriaQuery<C> createQuery = criteriaBuilder.createQuery(C.class);
            final Root<C> selection = createQuery.from(C.class);
            createQuery.select(selection).where(criteriaBuilder.isMember(bObj, selection.<Set<B>>get("bObj")),
                    criteriaBuilder.equal(criteriaBuilder.size(selection.<Set<B>>get("bObj")), 1));
            final List<C> resultList = entityManager.createQuery(createQuery).getResultList();
            assertThat("Should get exactly one result!", resultList.size(), is(1));
        }
    
        @Test
        public void criteriaTest() {
            final A aObj = new A();
            aObj.setName("test");
            entityManager.persist(aObj);
    
            final B bObj = new B();
            bObj.setaObj(aObj);
            entityManager.persist(bObj);
    
            final B bObj2 = new B();
            bObj2.setaObj(aObj);
            entityManager.persist(bObj2);
    
            final C cObj = new C();
            cObj.getbObj().add(bObj);
            // cObj.getbObj().add(bObj2);
            entityManager.persist(cObj);
    
            final Criteria criteria = entityManager.unwrap(Session.class).createCriteria(C.class, "c");
            criteria.createAlias("c.bObj", "b");
            criteria.createAlias("b.aObj", "a");
            criteria.add(Restrictions.sizeEq("c.bObj", 1));
            criteria.add(Restrictions.eq("a.name", "test"));
            final List<C> resultList = criteria.list();
    
            assertThat("Should get exactly one result!", resultList.size(), is(1));
    
        }
    
        @Test
        public void jpqlTest() {
            final A aObj = new A();
            aObj.setName("test");
            entityManager.persist(aObj);
    
            final B bObj = new B();
            bObj.setaObj(aObj);
            entityManager.persist(bObj);
    
            final B bObj2 = new B();
            bObj2.setaObj(aObj);
            entityManager.persist(bObj2);
    
            final C cObj = new C();
            cObj.getbObj().add(bObj);
            // cObj.getbObj().add(bObj2);
            entityManager.persist(cObj);
    
            final TypedQuery<C> query = entityManager.createQuery(
                    "SELECT c from C c JOIN c.bObj b JOIN b.aObj a where c.bObj.size = 1 AND a.name = :name ", C.class);
            query.setParameter("name", "test");
            final List<C> resultList = query.getResultList();
    
            assertThat("Should get exactly one result!", resultList.size(), is(1));
        }
    
    }
    

    The mapping to test with is something like:

    @Entity
    public class A {
    
        @Id
        @GeneratedValue
        private long id;
    
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
    }
    
    @Entity
    public class B {
    
        @Id
        @GeneratedValue
        private long id;
    
        @OneToOne
        private A aObj;
    
        public void setaObj(A aObj) {
            this.aObj = aObj;
        }
    
    }
    
    @Entity
    public class C {
    
        @Id
        @GeneratedValue
        private long id;
    
        @OneToMany
        private Set<B> bObj = new HashSet<>();
    
        public Set<B> getbObj() {
            return bObj;
        }
    
        public void setbObj(Set<B> bObj) {
            this.bObj = bObj;
        }
    
    }
    

    I would tend to a typesafe solution, maybe the first or the last one. Or think about using the MetaModel Generator.

    UPDATE:

    After nailing down the problem in the comments I will add another test to make the problem more clear (and keep the origin answer for history purposes).

    Accessing the unfiltered association will always give you all results (either eager on query time or lazy on accessing it). One solution would be to filter it before the access and convert it into a Projection or something. Maybe you can play around with this test to find a way for that:

    @Test
    public void jpqlTestWithDelayedFilterQuery() {
        final String filterFieldName = "name";
        final String filterFieldValue = "test";
        // this is the one we want to get the Bs for
        final A aObj = new A();
        aObj.setName(filterFieldValue);
        entityManager.persist(aObj);
    
        // this is the B which should be pass the filter
        final B bObj = new B();
        bObj.setaObj(aObj);
        entityManager.persist(bObj);
    
        // A not matching the filter
        final A aObj2 = new A();
        aObj2.setName("testXXX");
        entityManager.persist(aObj2);
    
        // we don't want to get that B here
        final B bObj2 = new B();
        bObj2.setaObj(aObj2);
        entityManager.persist(bObj2);
    
        // only another B to test the first query
        final B bObj3 = new B();
        bObj3.setaObj(aObj2);
        entityManager.persist(bObj3);
    
        // this is the one returned by first query
        final C cObj = new C();
        cObj.getbObj().add(bObj);
        cObj.getbObj().add(bObj2);
        entityManager.persist(cObj);
    
        // only another C to test the first query
        final C cObj2 = new C();
        cObj2.getbObj().add(bObj3);
        entityManager.persist(cObj2);
    
        // let's get only the Cs we need
        final Session session = entityManager.unwrap(Session.class);
        final Query cQuery = session.createQuery(
                "SELECT c from C c INNER JOIN c.bObj b INNER JOIN b.aObj a where a.name = :name ");
        cQuery.setParameter(filterFieldName, filterFieldValue);
        final List cResults = cQuery.list();
        assertThat("Should get exactly one C result!", cResults.size(), is(1));
    
        // sadly the B collection is initialized fully, at latest on accessing it, so two Bs here :/
        final C cResult = (C)cResults.iterator().next();
        assertThat("Should get two already initialized B results here!", cResult.getbObj().size(), is(2));
    
        // the only way is getting our needed Bs with a filter (imagine you did not use it before)
        final Query query = session.createFilter(cResult.getbObj(), "where this.aObj.name = :name");
        query.setParameter(filterFieldName, filterFieldValue);
        final List bResult = query.list();
        assertThat("Should get exactly one filtered B result here!", bResult.size(), is(1));
    }