JPA and Hibernate: Count number of related entries in projection - spring

I have a simple one-to-many relation. A Post can have many Comments. My goal is to fetch a post by id and the number (count) of associated comments.
I'm using Kotlin so all code is just a simplified demonstration
#Entity(name = "Post")
#Table(name = "post")
public class Post {
#Id
#GeneratedValue
private Long id;
private String title;
private String text;
#OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Comment> comments = new ArrayList<>();
//Constructors, getters and setters removed for brevity
}
#Entity(name = "Comment")
#Table(name = "comment")
public class Comment {
#Id
#GeneratedValue
private Long id;
private String review;
//Constructors, getters and setters removed for brevity
}
Now I need to fetch the Post with the number of comments. I thought of using a dto projection.
public class PostWithCount {
private Long id;
private String title;
private String text;
private Long numberOfComments;
}
I constructed the following jpql Query, but don't know how to count the number of comments.
#Query("select new my.package.PostWithCount(post.id, post.title, post.text, ???) from Post post left join post.comments comment where post.id = :id")
What would be the best way to count the comments without fetching them all and count in java code? I'm also open to other solutions than DTO Projection.

Use aggregation...
select new my.package.PostWithCount(post.id, post.title, post.text, count(1))
from Post post
left join post.comments comment
where post.id = :id
group by post.id, post.title, post.text

Related

Hibernate search across two entities without an embedded index

I am trying to use Hibernate Search 6 and elastic search
A simple example of what I am trying to build is as follows.
I have a Book entity, which has information like title, authorName, genre, price
I have a Shop entity which has information like shopName, phone, email, location
I have a "joining table" which does a many to many mapping between nooks and shops. ( A book can be at many shops, and a shop can have many books)
I am trying to do a search by name and location, ideally to find a book at a location nearest to the input. The standard book-author example in the documentation requires a IndexedEmbedded annotation, which is not really possible in my case because I am using a joining table.
Is there an alternative approach to solve this problem
My entities
#Indexed
public class Book extends PanacheEntity{
public String title;
public String authorName;
#OneToMany(mappedBy = "book", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE })
public List<BookShopRelation> bookShopRelation = new ArrayList<>();
}
#Indexed
public class Shop extends PanacheEntity{
public String name;
public String city;
#OneToMany(mappedBy = "shop", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE })
private List<BookShopRelation> bookShopRelation = new ArrayList<>();
}
#Indexed
public class BookShopRelation extends PanacheEntity{
#JoinColumn(name = "shop_id")
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#IndexedEmbedded
private Shop shop;
#JoinColumn(name = "offer_id")
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#IndexedEmbedded
private Book book;
}
For me what was key was to understand, was that the relation table could be indexed and used as the basis for the search
List<BookShopRelation> result = Search.session(entityManager)
.search(BookShopRelation.class) .predicate(f ->
pattern == null || pattern.trim().isEmpty() ?
f.matchAll() :
f.simpleQueryString()
.fields("book.title").matching(pattern)
)
.fetchHits(size.orElse(20));

How to filter child collection by one of its attributes in Spring Data JPA

I have a Post entity with a collection of Comment as shown below
#Entity
#Table(name = "post")
public class Post {
#Id
private Long id;
#OneToMany(
mappedBy = "post"
)
private List<Comment> comments= new ArrayList<>();
Comment entity
Entity
#Table(name = "comment")
public class Comment{
#Id
private Long id;
#ManyToOne
#JoinColumn(name = "post_id")
private Post post;
private boolean enabled;
I am using Spring Data JPA Repository findById(Long id) to fetch a Post. Currently, all Comment associated with that Post are returned. But the desired output is to fetch ony those Comment that has enabled attribute equal to true.
Is it possible to filter child collection in Spring Data JPA Repository?
I have tried the following
findByIdAndCommentsEnabledTrue(Long id);
findByIdAndCommentsEnabled(Long id, boolean enabled);
But none of them worked.
Can you try with the below JPA Query?
List<Post> findByCommentsEnabled(boolean enabled);
#Query("select post from Post post
fetch join post.comments comm
where post.id = :postId and com.enabled = :enabled")
Post findPostWithCommentsEnables(#Param("postId") Long postId,#Param("enabled") boolean enabled);
A clean Hibernate based solution is to use #Where. Below is the sample code
#Entity
#Table(name = "post")
public class Post {
#Id
private Long id;
#OneToMany(
mappedBy = "post"
)
#Where(clause = "enabled = true")
private List<Comment> comments= new ArrayList<>();

Duplicated entities in Bidirection OneToMany relationship when fetching with Spring Data JPA

I have two entities defined. Both of them are connected through a bidirectional #OneToMany.
Here are my two entities
#Entity(name = "Post")
#Table(name = "post")
public class Post {
#Id
#GeneratedValue
private Long id;
private String title;
#OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();
//Constructors, getters and setters removed for brevity
public void addComment(PostComment comment) {
comments.add(comment);
comment.setPost(this);
}
public void removeComment(PostComment comment) {
comments.remove(comment);
comment.setPost(null);
}
}
#Entity(name = "PostComment")
#Table(name = "post_comment")
public class PostComment {
#Id
#GeneratedValue
private Long id;
private String review;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "post_id")
private Post post;
//Constructors, getters and setters removed for brevity
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PostComment )) return false;
return id != null && id.equals(((PostComment) o).getId());
}
#Override
public int hashCode() {
return Objects.hash(id);
}
}
I am using Spring Data JPA to fetch / save entities.
Saving works fine and for example if I save 1 post and 4 post comments I can see the entries in the database. The database I am using is PostgreSQL.
When I am fetching all the posts through my repository using the findAll method, then I receive the post with the 4 comments.
The issue is when I am fetching only one post through the getOne method, the post is found, but for some reason the entity contains 7 post comments. The first entry is duplicated 3 times and the second one is duplicated two times.
I don't understand why this is happening and how can I fix this.
Any help is appreciated.
Thanks
You need to change List to Set.
#OneToMany(mappedBy = "post",cascade = CascadeType.ALL,orphanRemoval = true)
private Set<PostComment> comments = new HashSet<>();

Spring data JPA derived query for multiple #OneToMany entities and inner entity localization

I am trying to do a simple task with Spring Data JPA derived queries and am unable to get the desired results from the query. Basically I have a Book which can have one or many Chapters with localization support for the Book as well as the Chapter. I want to create a query which would fetch a language specific book (with chapters) based on the Locale. Here are my four entities.
#Entity
#Getter
#Setter
public class Book {
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int noOfPages;
/**
* Both mappings below are unidirectional #OneToMany
*/
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "BOOK_ID", referencedColumnName = "ID")
private List<BookTranslation> bookTranslations;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "BOOK_ID", referencedColumnName = "ID")
private List<Chapter> chapters;
/**
* Constructor for JPA
*/
protected Book() {
}
public Book(int noOfPages, List<BookTranslation> bookTranslations, List<Chapter> chapters) {
this.noOfPages = noOfPages;
this.bookTranslations = bookTranslations;
this.chapters = chapters;
}
}
#Entity
#Getter
#Setter
public class BookTranslation {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Enumerated(EnumType.STRING)
private Language language;
private String name;
/**
* Constructor for JPA
*/
protected BookTranslation() {
}
public BookTranslation(Language language, String name) {
this.language = language;
this.name = name;
}
}
#Entity
#Getter
#Setter
public class Chapter {
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int chapterNumber;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name = "CHAPTER_ID", referencedColumnName = "ID")
private List<ChapterTranslation> chapterTranslations;
/**
* Constructor for JPA
*/
protected Chapter() {
}
public Chapter(int chapterNumber, List<ChapterTranslation> chapterTranslations) {
this.chapterNumber = chapterNumber;
this.chapterTranslations = chapterTranslations;
}
}
#Entity
#Getter
#Setter
public class ChapterTranslation {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Enumerated(EnumType.STRING)
private Language language;
private String title;
/**
* Constructor for JPA
*/
protected ChapterTranslation() {
}
public ChapterTranslation(Language language, String title) {
this.language = language;
this.title = title;
}
}
public enum Language {
EN, FR
}
Below is the sample code, I am using to persist these entities. Ignore the #GetMapping please, this is just a sample.
#GetMapping("/persist-book")
public void persistBook() {
ChapterTranslation enChapter = new ChapterTranslation(Language.EN, "What is Java persistence?");
ChapterTranslation frChapter = new ChapterTranslation(Language.FR, "Qu'est-ce que la persistance Java?");
List<ChapterTranslation> chapterOneTranslation = new ArrayList<>();
chapterOneTranslation.add(enChapter);
chapterOneTranslation.add(frChapter);
Chapter chapterOne = new Chapter(1, chapterOneTranslation);
List<Chapter> chapters = new ArrayList<>();
chapters.add(chapterOne);
BookTranslation enBook = new BookTranslation(Language.EN, "JPA WikiBook in English");
BookTranslation frBook = new BookTranslation(Language.FR, "JPA WikiBook in French");
List<BookTranslation> bookTranslations = new ArrayList<>();
bookTranslations.add(enBook);
bookTranslations.add(frBook);
Book book = new Book(500, bookTranslations, chapters);
bookRepository.save(book);
}
My BookRepository looks as follows:
public interface BookRepository extends CrudRepository<Book, Long> {
List<Book> findBooksByBookTranslations_LanguageAndChapters_ChapterTranslations_Language(Language lang1, Language lang2);
}
Sample code I am using to retrieve the result.
#GetMapping("/english-book")
public List<Book> retrieveEnglishBook() {
return bookRepository.findBooksByBookTranslations_LanguageAndChapters_ChapterTranslations_Language(
Language.EN, Language.EN
);
}
My expected output is as attached in the image below.
One thing that I noticed from the Hibernate logs is that Hibernate makes a total of four select queries and the first query output is exactly what I need. However, since this a method name based query I don't suppose I can control that.
EDIT 1: Before trying out the answer, I was getting all books with all their locales returned, after changing my query to the one given in the accepted answer I was able to get the Book with the selected locale.
Please note: I also had to change all collections from using a List to a Set, more on this can be read about in the accepted answers link.
What you describe as a desired result is a single database result.
I guess what you mean by that is you expect to get all the books but only with the translations in a single language.
You don't describe what you actually get, so assume you are getting the book with all available translations.
Your desired result is beyond the capabilities of derived queries.
The different predicates of a derived queries all limit the root entities to be returned Book in your case. They should still have all references in tact.
You could achieve your goal with an annotated query like this:
public interface BookRepository extends CrudRepository<Book, Long> {
#Query("SELECT b FROM Book b
JOIN FETCH b.bookTranslations as bt
JOIN FETCH b.chapter as c
JOIN FETCH c.chapterTranslation as ct
WHERE bt.language = :lang
AND ct.language = :lang")
List<Book> findBooksByLanguage(Language lang);
}
See also How to filter child collection in JPQL query?
Side note: query derivation should only be used when the resulting method name is VERY similar to what you would have named the method anyway.

Can't hibernate search sort in #OneToMany association?

I try to sort a list of Items for a customer by ordered Date. The Date is only avalable through Item.orderPositions.order.orderDate . But #IndexedEmbedded doesn't work. There's no Exeption or Error but the result is only sorted by HS-logic.
#Entity
#Indexed
public class Item{
#Id
private long id;
#Field(index = Index.YES, store = Store.YES, analyse = Analyse.YES, analyser = #Analyzer(definition = Constant.ANALYSER))
private String description;
#OneToMany(mappedBy = "item")
#IndexedEmbedded
private List<OrderPosition> orderPositions;
#ManyToOne
#IndexedEmbedded
private Company company;
//getter&setter
}
#Entity
public class OrderPosition{
#Id
private long id;
#ManyToOne
private Item item;
#ManyToOne
#IndexedEmbedded
private Order order;
//getter&setter
}
#Entity
public class Order{
#Id
private long id;
#ManyToOne
private Customer customer;
#Field(index = Index.NO, store = Store.NO, analyze = Analyze.NO)
#SortableField
private String orderDate;
//getter&setter
}
#Entity
public class Company{
#Id
private long id;
#Field(index = Index.NO, store = Store.NO, analyze = Analyze.NO)
#SortableField
private String name;
//getter&setter
}
If I sort the List by Item.company.name it works fine.
queryService.buildFullTextQuery("searchText", Item.class, "description", "company.name").getResultList();
If I sort the List by Item.orderPosition.order.orderDate it's sorted by default(HS-logic)
queryService.buildFullTextQuery("searchText", Item.class, "description", "orderPositions.order.orderDate").getResultList();
I build the FullTextQuery this way:
public FullTextQuery buildFullTextQuery(#NonNull String searchText, #NonNull Class<?> clazz, #NonNull String... fields) throws Exception {
FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(getEntityManager());
QueryBuilder qb = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(clazz).get();
Query query = qb.keyword().onField(fields[0]).matching(searchText).createQuery();
SortField sortField = new SortField(fields[1], SortField.Type.STRING, false);
Sort sort = new Sort(sortField);
return fullTextEntityManager.createFullTextQuery(query, clazz).setSort(sort);
}
I think HS can't find the association for #OneToMany. Is there a way to solve this prob?
Thank you in advance
I can't tell you what's going on exactly without the results of your queries, but you're definitely doing something wrong here: you are trying to sort on a multi-valued field. One item is linked to multiple orders, each having its own date. So there is multiple dates per item.
When you ask to compare two items that each have three dates, what should Hibernate Search do? Compare only the latest dates? Compare only the earliest dates? You didn't say, so your query is bound to return inconsistently ordered results.
Thing is, there is no way to tell Hibernate Search which value to pick in multi-valued fields, so your easiest way out is to explicitly create a single-valued field to sort on.
For instance, you could add a getter on Item to return the latest order, and add the #IndexedEmbedded there:
#Entity
#Indexed
public class Item{
#Id
private long id;
#Field(index = Index.YES, store = Store.YES, analyse = Analyse.YES, analyser = #Analyzer(definition = Constant.ANALYSER))
private String description;
#OneToMany(mappedBy = "item")
#IndexedEmbedded
private List<OrderPosition> orderPositions;
#ManyToOne
#IndexedEmbedded
private Company company;
#javax.persistence.Transient
public Order getLatestOrder() {
Order latestOrder;
// ... compute the latest order ...
return latestOrder;
}
//getter&setter
}
Then sort on latestOrder.orderDate and you should be good.

Resources