Hibernate java Criteria query for instances with multiple collection members like tag

Hibernate java Criteria query for instances with multiple collection members like tag



Please help me write a Java Criteria-object query to find all items with collections that have all desired members. Basically, I need to "and" the condition, not "or" it. This is exactly like SO articles and tags: search for articles with tags "java" and "hibernate", the result should only have articles tagged with both tags (more tags are ok). Like this one :)



My entity is called "Solution" and it has a collection of tag entities mapped via a two-column mapping table. I understand from research below that I need a DetachedQuery. The critical routine (see search service impl below) runs but returns no results in the test case.



Research so far - if I knew how to translate HQL to Criteria better I would be farther ahead :/



EDIT Thanks to @samabcde I corrected the query method to use Restrictions.eqProperty, no more class cast exception.



By turning on debug logging I can see this generated SQL (long because of the eager fetch policy). It doesn't look right, especially the "this_.ID=this_.ID" part - this is trivially true.


select this_.ID as ID1_39_1_, this_.NAME as NAME2_39_1_,
tags2_.SOL_ID as SOL_ID1_38_3_, tag3_.ID as TAG_ID2_38_3_,
tag3_.ID as ID1_40_0_, tag3_.NAME as NAME2_40_0_
from SOLUTION this_
left outer join SOL_TAG_MAP tags2_ on this_.ID=tags2_.SOL_ID
left outer join TAG tag3_ on tags2_.TAG_ID=tag3_.ID
where ? = (select count(t1_.NAME) as y0_ from SOLUTION this_
inner join SOL_TAG_MAP tags3_ on this_.ID=tags3_.SOL_ID
inner join TAG t1_ on tags3_.TAG_ID=t1_.ID
where this_.ID=this_.ID and t1_.NAME in (?, ?))



And I don't get the expected answer - the query result in the test case (see below) is empty, where I expect it to find 1 row.



Sorry for the length, files shown for completeness altho I skipped the import statements for brevity. I'm probably doing something stupid that an expert can point out instantly, thanks in advance.



Entity Solution


@Entity
@Table(name = "SOLUTION")
public class Solution implements Serializable

private static final long serialVersionUID = 745945642089325612L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ID", nullable = false, updatable = false, columnDefinition = "INT")
private Long id;

@Column(name = "NAME", nullable = false, columnDefinition = "VARCHAR(100)")
private String name;

// Fetch eagerly to make serialization easy
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = SolTagMap.TABLE_NAME, //
joinColumns = @JoinColumn(name = SolTagMap.SOL_ID_COL_NAME) , //
inverseJoinColumns = @JoinColumn(name = SolTagMap.TAG_ID_COL_NAME) )
private Set<Tag> tags = new HashSet<>(0);

public Long getId()
return id;


public void setId(Long id)
this.id = id;


public String getName()
return name;


public void setName(String name)
this.name = name;


public Set<Tag> getTags()
return tags;


public void setTags(Set<Tag> tags)
this.tags = tags;


@Override
public String toString()
return this.getClass().getName() + "[id=" + getId() + ", name=" + getName() + ", tags="
+ getTags() + "]";





Entity Map table


@Entity
@IdClass(SolTagMapKey.class)
@Table(name = SolTagMap.TABLE_NAME)
public class SolTagMap implements Serializable

// Define constants so names can be reused in many-many annotation.
/* package */ static final String TABLE_NAME = "SOL_TAG_MAP";
/* package */ static final String SOL_ID_COL_NAME = "SOL_ID";
/* package */ static final String TAG_ID_COL_NAME = "TAG_ID";

private static final long serialVersionUID = -7814665924253912856L;

@Embeddable
public static class SolTagMapKey implements Serializable

private static final long serialVersionUID = -503957020456645384L;
private Long solId;
private Long tagId;

@Override
public boolean equals(Object that)
if (that == null)
return false;
if (!(that instanceof SolTagMapKey))
return false;
SolTagMapKey thatPK = (SolTagMapKey) that;
return Objects.equals(solId, thatPK.solId) && Objects.equals(tagId, thatPK.tagId);


@Override
public int hashCode()
return Objects.hash(solId, tagId);


@Override
public String toString()
return this.getClass().getName() + "[solId=" + solId + ", tagId=" + tagId + "]";




@Id
@Column(name = SolTagMap.SOL_ID_COL_NAME, nullable = false, updatable = false, columnDefinition = "INT")
private Long solId;

@Id
@Column(name = SolTagMap.TAG_ID_COL_NAME, nullable = false, updatable = false, columnDefinition = "INT")
private Long tagId;

public Long getSolId()
return solId;


public void setSolId(Long solId)
this.solId = solId;


public Long getTagId()
return tagId;


public void setTagId(Long tagId)
this.tagId = tagId;


@Override
public boolean equals(Object that)
if (that == null)
return false;
if (!(that instanceof SolTagMap))
return false;
SolTagMap thatObj = (SolTagMap) that;
return Objects.equals(solId, thatObj.solId) && Objects.equals(tagId, thatObj.tagId);


@Override
public int hashCode()
return Objects.hash(solId, tagId);


@Override
public String toString()
return this.getClass().getName() + "[solId=" + solId + ", tagId=" + tagId + "]";





Entity Tag


@Entity
@Table(name = "TAG")
public class Tag implements Serializable

private static final long serialVersionUID = -288462280366502647L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ID", nullable = false, updatable = false, columnDefinition = "INT")
private Long id;

@Column(name = "NAME", nullable = false, columnDefinition = "VARCHAR(100)")
private String name;

public Long getId()
return id;


public void setId(Long id)
this.id = id;


public String getName()
return name;


public void setName(String name)
this.name = name;


@Override
public boolean equals(Object that)
if (that == null)
return false;
if (!(that instanceof Tag))
return false;
Tag thatObj = (Tag) that;
return Objects.equals(id, thatObj.id);


@Override
public int hashCode()
return Objects.hash(id, name);


@Override
public String toString()
return this.getClass().getName() + "[id=" + id + ", name=" + name + "]";





Search Service implementation


@Service("simpleSolutionSearchService")
@Transactional
public class SimpleSolutionSearchServiceImpl implements SimpleSolutionSearchService

@Autowired
private SessionFactory sessionFactory;

// This works fine
public List<Solution> findSolutions()
Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Solution.class);
// Hibernate should coalesce the results, yielding only solutions
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
return criteria.list();


// This throws
public List<Solution> findSolutionsWithTags(String requiredTags)
final String parentAlias = "sol";
final String collFieldAlias = "t";
final String tagValueField = collFieldAlias + ".name";
DetachedCriteria subquery = DetachedCriteria.forClass(Solution.class)
.add(Restrictions.eqProperty("id", parentAlias + ".id"))
.createAlias("tags", collFieldAlias) //
.add(Restrictions.in(tagValueField, requiredTags)) //
.setProjection(Projections.count(tagValueField));
Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Solution.class, parentAlias)
.add(Subqueries.eq((long) requiredTags.length, subquery));
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
return criteria.list();





Solution repository


public interface SimpleSolutionRepository extends CrudRepository<Solution, Long>



Tag repository


public interface SimpleTagRepository extends CrudRepository<Tag, Long>



Test case


@RunWith(SpringRunner.class)
@SpringBootTest
public class SolutionServiceTest

private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

@Autowired
private SimpleSolutionRepository solutionRepository;
@Autowired
private SimpleTagRepository tagRepository;
@Autowired
private SimpleSolutionSearchService searchService;

@Test
public void testRepositories() throws Exception

final String tagName1 = "tag name 1";
final String tagName2 = "tag name 2";

Tag t1 = new Tag();
t1.setName(tagName1);
t1 = tagRepository.save(t1);
Assert.assertNotNull(t1.getId());
logger.info("Created tag ", t1);

Tag t2 = new Tag();
t2.setName(tagName2);
t2 = tagRepository.save(t2);
Assert.assertNotNull(t2.getId());
logger.info("Created tag ", t2);

Solution s1 = new Solution();
s1.setName("solution one tag");
s1.getTags().add(t1);
s1 = solutionRepository.save(s1);
Assert.assertNotNull(s1.getId());
logger.info("Created solution ", s1);

Solution s2 = new Solution();
s2.setName("solution two tags");
s2.getTags().add(t1);
s2.getTags().add(t2);
s2 = solutionRepository.save(s2);
Assert.assertNotNull(s2.getId());
logger.info("Created solution ", s1);

List<Solution> sols = searchService.findSolutions();
Assert.assertTrue(sols.size() == 2);
for (Solution s : sols)
logger.info("Found solution ", s);

String searchTags = tagName1, tagName2 ;
List<Solution> taggedSols = searchService.findSolutionsWithTags(searchTags);
// EXPECT ONE OBJECT BUT GET ZERO
Assert.assertTrue(taggedSols.size() == 1);







What is your desired search query and output?
– Emre Savcı
Aug 23 at 18:44





Hi @Emre the second-to-last statement in the testcase is the desired query, and the last assertion is the desired output - find the one solution that has both tags.
– chrisinmtown
Aug 23 at 18:47





1 Answer
1



Restrictions.eq is for compare property with a value, Restrictions.propertyEq compare property with another property. Hence the code consider parentAlias + ".id" as a String value to compare with the ID property, instead of parent id property and this cause ClassCaseException.


parentAlias + ".id"


ClassCaseException



For problem of no record found,where this_.ID=this_.ID tells the reason. Hibernate considers the id property in the subquery to reference the parent query Solution, instead of the subquery Solution. Alias should be provided to the subquery to distinguish the id property in this case.


where this_.ID=this_.ID


id


Solution


Solution


id


public List<Solution> findSolutionsWithTags(String requiredTags)
final String parentAlias = "sol";
final String childAlias = "subSol";
final String collFieldAlias = "t";
final String tagValueField = collFieldAlias + ".name";

DetachedCriteria subquery = DetachedCriteria.forClass(Solution.class, childAlias)
// Throws ClassCastException; apparently sol.id isn't replaced with an ID value?
// Problem should be due to following line
//.add(Restrictions.eq("id", parentAlias + ".id"))
// Use eqProperty instead
.add(Restrictions.eqProperty(childAlias + ".id", parentAlias + ".id"))
.createAlias("tags", collFieldAlias) //
.add(Restrictions.in(tagValueField, requiredTags)) //
.setProjection(Projections.count(tagValueField));
Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Solution.class, parentAlias)
.add(Subqueries.eq((long) requiredTags.length, subquery));
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
return criteria.list();





Thanks! Using Restrictions.eqProperty fixed the ClassCastException, now the generated SQL looks better and the query runs to completion. But the result is empty, the test case does not find the one solution with two tags.
– chrisinmtown
Aug 24 at 19:02





@chrisinmtown please check if updated answer solve the problem.
– samabcde
Aug 25 at 12:35





Cool with the new child alias the generated SQL is: select this_.ID as ID1_39_1_, this_.NAME as NAME2_39_1_, tags2_.SOL_ID as SOL_ID1_38_3_, tag3_.ID as TAG_ID2_38_3_, tag3_.ID as ID1_40_0_, tag3_.NAME as NAME2_40_0_ from SOLUTION this_ left outer join SOL_TAG_MAP tags2_ on this_.ID=tags2_.SOL_ID left outer join TAG tag3_ on tags2_.TAG_ID=tag3_.ID where ? = (select count(t1_.NAME) as y0_ from SOLUTION kid_ inner join SOL_TAG_MAP tags3_ on kid_.ID=tags3_.SOL_ID inner join TAG t1_ on tags3_.TAG_ID=t1_.ID where kid_.ID=this_.ID and t1_.NAME in (?, ?)) .. and the test PASSES! Thank you!
– chrisinmtown
Aug 26 at 1:03






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

𛂒𛀶,𛀽𛀑𛂀𛃧𛂓𛀙𛃆𛃑𛃷𛂟𛁡𛀢𛀟𛁤𛂽𛁕𛁪𛂟𛂯,𛁞𛂧𛀴𛁄𛁠𛁼𛂿𛀤 𛂘,𛁺𛂾𛃭𛃭𛃵𛀺,𛂣𛃍𛂖𛃶 𛀸𛃀𛂖𛁶𛁏𛁚 𛂢𛂞 𛁰𛂆𛀔,𛁸𛀽𛁓𛃋𛂇𛃧𛀧𛃣𛂐𛃇,𛂂𛃻𛃲𛁬𛃞𛀧𛃃𛀅 𛂭𛁠𛁡𛃇𛀷𛃓𛁥,𛁙𛁘𛁞𛃸𛁸𛃣𛁜,𛂛,𛃿,𛁯𛂘𛂌𛃛𛁱𛃌𛂈𛂇 𛁊𛃲,𛀕𛃴𛀜 𛀶𛂆𛀶𛃟𛂉𛀣,𛂐𛁞𛁾 𛁷𛂑𛁳𛂯𛀬𛃅,𛃶𛁼

Edmonton

Crossroads (UK TV series)