Search code examples
hibernatemany-to-manycriteriahibernate-mappinghibernate-criteria

Hibernate ManyToMany and Restrictions.in


I have Article and Tag entities:

@Entity
@Table(name = "articles")
public class Article implements Serializable{
   //other things

   @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
   private List<Tag> tags;
}

@Entity
@Table(name = "tags")
public class Tag implements Serializable{
    //other things

    @ManyToMany(mappedBy="tags" ,cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private List<Article> articles;
}

And the idea is: I have 3 Articles when First has first and second tags. Second has second and third tags. Second has first and third tags. When I filter Articles by first tag, I get two Articles - because first and third Articles was tagged with first tag. When I filter Articles by second tag and third, I got all 3 Articles - because every Article was tagged with one of them. Generally, the goal is to filter Articles by one of specified Tags. I wrote a Unit test which shows what I want to achieve:

@Test
public void test_WhenTagIdsAreSpecified_ArticlesShouldBeFilteredByOneOfTags() throws Exception {
    AuthorDTO author = getExpectedAuthor();
    List<TagDTO> tags = saveThreeTags();
    TagDTO firstTag = tags.get(0);
    TagDTO secondTag = tags.get(1);
    TagDTO thirdTag = tags.get(2);
    service.save(new ArticleDTO(null, author,
            Arrays.asList(firstTag, secondTag), articleContent + "_firstAndSecondTag"));
    service.save(new ArticleDTO(null, author,
            Arrays.asList(secondTag, thirdTag), articleContent + "_secondAndThirdTag"));
    service.save(new ArticleDTO(null, author,
            Arrays.asList(firstTag, thirdTag), articleContent + "_firstAndThirdTag"));

    assertEquals(3, tagService.getAll().size());
    Collection<Long> tagIdsContainingFirst = Collections.singletonList(firstTag.getId());
    List<ArticleDTO> articlesByFirstTag = service.getByTags(tagIdsContainingFirst);
    Collection<Long> tagIdsContainingSecondOrThirdTag = 
        Arrays.asList(secondTag.getId(), thirdTag.getId());
    List<ArticleDTO> articlesBySecondOrThirdTag =
        service.getByTags(tagIdsContainingSecondOrThirdTag);

    assertEquals(2, articlesByFirstTag.size());
    assertEquals(articleContent + "_firstAndSecondTag", articlesByFirstTag.get(0).getContent());
    assertEquals(articleContent + "_firstAndThirdTag", articlesByFirstTag.get(1).getContent());
    assertEquals(3, articlesBySecondOrThirdTag.size()); //it fails
}

But instead of 3, I got 4 Articles which is pretty weird cause I have only 3 in database. That is how I try to filter Articles:

@Override
public List<Article> getByTags(Collection<Long> tagIds) {
    return (List<Article>)createCriteria()
            .createAlias("tags", "t")
            .add(Restrictions.in("t.id", tagIds))
            .list();
}

Second tag is counted twice (I think because it has second and third tags). It seems like it "loops" in every Tag, then check if it is in (second, third) and then if it's true add Article related with Tag. I also tried to add Restrinction.in for every tag ID separately:

criteria.createAlias("tags", "t");
for (Long tagId: tagIds) {
    criteria.add(Restrictions.in("t.id", Collections.singleton(tagIds)));
}

But then result was 0, no expected 3 - IMO also because it doesn't look on all Tags, bot on every Tag separately. Is there any idea how to "look on all tags at once" (if you know what I mean)? When I want to achieve something like that in normal language it would be a little bit like (Java-like pseudocode):

for (Article article: articles){
    List<Tag> tags = article.getTags();
    // As many conditions as tags to filter specified
    if ( tags.contains(second) || tags.contains(third) ... ) {
        filteredArticles.add(article);
    }
}

Can anybody help me with this problem? Thank you in advance for every answer.


Solution

  • I found a solution on my own:

    @Override
    public List<Article> getByTags(Collection<Long> tagIds) {
        return (List<Article>)createCriteria()
                .createAlias("tags", "t")
                .add(Restrictions.in("t.id", tagIds))
                .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
                .list();
    }
    

    .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) was missing.