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

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.

Related

Automatic JPA refresh ManyToOne objects with #Version feature

I'm getting an exception:
org.hibernate.TransientPropertyValueException:
object references an unsaved transient instance
- save the transient instance before flushing :
com.example.jpamapstruct.entity.Member.club ->
com.example.jpamapstruct.entity.Club
while saving the member entity:
#Transactional
public MemberDto save(MemberDto memberDto){
Member entity = memberMapper.toEntity(memberDto);
return memberMapper.toDto(repository.save(entity));
}
How to fix this case in a proper way?
Possible solution:
I can get and set a club object before saving a member but is it only one and the best approach in such scenario?
Member entity = memberMapper.toEntity(memberDto);
clubRepository.getReferencedById(memberDto.getClubId()).ifPresent(entity::setClub);
return memberMapper.toDto(repository.save(entity));
Questions:
Should I put this getReferencedById code explicity? I mean what if we have several child objects (unidirectional ManyToOne), for each we need to get data from DB.
Is there any way to handle this by JPA (Spring Data/JPA) "automatically"?
Maybe it is possible to hit DB only one time with f.e join fetch somehow for all childs (with using custom #Query or querydsl or criteria/specification)?
Next, hoow to handle collections (unidirectional manyToMany)? In my case set of events in member object. Also need to loop thru and get all objects one by one before saving member?
Where should I put such logic in a service or maybe better in a mapstuct mapper?
If so, how to use repositories in such mapper?
#Mapper(componentModel = "spring")
public interface MemberMapper extends EntityMapper<MemberDto, Member> {
#AfterMapping
default void afterMemberMapping(#MappingTarget Member m, MemberDto dto) {
var club = clubRepo.findById(m.getClub().getId())
m.setClub(club)
}
Source code:
#Entity
public class Club extends AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Long id;
}
public class ClubDto extends AbstractDto {
private Long id;
}
#Entity
public class Member {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Long id;
// commented out as don't want to save child object as it should already exist
// #ManyToOne(cascade = CascadeType.ALL)
#ManyToOne
Club club;
#ManyToMany
#JoinTable(name = "member_events",
joinColumns = #JoinColumn(name = "member_id"),
inverseJoinColumns = #JoinColumn(name = "event_id")
)
List<Event> events = new ArrayList<>();
}
public class MemberDto {
private Long id;
private ClubDto club;
}
#MappedSuperclass
public abstract class AbstractEntity {
#Version
private Integer version;
}
public abstract class AbstractDto {
private Integer version;
}
//MemberMapper above

fetch list based on id present in another entity

this is my order entity,
#Data
#NoArgsConstructor
#AllArgsConstructor
#ToString
#Entity
#Table(name = "ordertab")
public class Order {
#Id
private int orderId;
private String orderDate;
#ManyToMany(targetEntity = Medicine.class,cascade = CascadeType.ALL)
#JoinTable(name="ord_med",
joinColumns = {#JoinColumn(name="ord_id")},
inverseJoinColumns = {#JoinColumn(name="med_id")})
private List<Medicine> medicineList;
private String dispatchDate;
private float totalCost;
#ManyToOne(targetEntity = Customer.class,cascade = CascadeType.ALL)
#JoinColumn(name= "custord_fk",referencedColumnName = "customerId")
private Customer customer;
private String status;
}
and this is my medicine entity,
#Data
#NoArgsConstructor
#AllArgsConstructor
#ToString
#Entity
public class Medicine {
#Id
private String medicineId;
private String medicineName;
private float medicineCost;
private LocalDate mfd;
private LocalDate expiryDate;
**#ManyToMany(cascade = CascadeType.ALL, mappedBy = "medicineList")
private List<Order> orderList;** //order/ medicine many to many mapping
// OneToOne Mapping
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "categoryId", referencedColumnName = "categoryId")
private Category category;
in my order service interface i have a method,
List showAllOrder(string medId);
I have to fetch all orders that has the matching med id.
this many to many mapping have created a additional table ord_med with two columns named ord_id,med_id(type foreign keys).In addition to that due to this bidirectional mapping(i believe it is) while creating object of medicine entity its asking me to add orderlist ,how to approach this method or how exactly should i solve this. thankyou.
in your OrderRepository you can implements this method
findByMedicineId(String id);
if i go for findByMedicineId(String id);
it gives error saying no property medicineId is found in Order entity,cuz the property medicineId is in Medicine entity,while defining custom method in repository follows rules, refer https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
anyway I have found the solution for this,
public List<Order> getOrderListBasedOnMedicineId(String medicineid) {
Optional<Medicine> med=medicineRepo.findById(medicineid);//find if medicine is present in database with the id.
if(med.isEmpty()) {
return null;
}
List<Order> orders = medicineServ.getOrderList(); //getorderlist defined in service implementation of medicine.
List<Order> ordersWithMedId = new ArrayList();//new list to add all orders that has atleast one medicineId that matches.
for(int i=0;i<orders.size();i++) {
List<Medicine> medicines= orders.get(i).getMedicineList();
for(int j=0;j<medicines.size();j++) {
ordersWithMedId.add(orders.get(i));
}
}
return ordersWithMedId;//returning the list of orders.
}
#Override
public List<Order> getOrderList() {//medicine service implementation
return orderRepo.findAll();
}
//OrderController
#GetMapping("/orders/list/{id}")
public ResponseEntity<List<Order>> getOrderListBasedOnMedicineId(#PathVariable("id") String id) {
List<Order> ord= orderService.getOrderListBasedOnMedicineId(id);
if(ord==null) {
throw new OrderNotFoundException("Order not found with medicine id:"+id);
}
return new ResponseEntity<List<Order>>(orderService.getOrderListBasedOnMedicineId(id),HttpStatus.OK);
}

Hibernate Fetch #Formula annotated fields on demand

I have a entity (declared with 2 way)(some not influencing code part are ommited for readability)
Entity version 1.
#Entity
public class Article {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Formula("(SELECT COUNT(w.id) FROM stock s LEFT JOIN warehouse w ON s.id=w.stock_id WHERE s.article_id = id)")
private int variants;
public int getVariants() {
return variants;
}
}
Entity version 2.
#Entity
public class Article {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Transient
private int variants;
#Access(AccessType.PROPERTY)
#Formula("(SELECT COUNT(w.id) FROM stock s LEFT JOIN warehouse w ON s.id=w.stock_id WHERE s.article_id = id)")
public int getVariants() {
return variants;
}
}
respective DTO and ArticleMapper - MapStruct
#JsonIgnoreProperties(ignoreUnknown = true)
public class ArticleDTOCommon {
private Long id;
private String name;
}
#JsonIgnoreProperties(ignoreUnknown = true)
public class ArticleDTO {
private Long id;
private String name;
private int variants;
}
#Mapper(componentModel = "spring", uses = {})
public interface ArticleMapper{
ArticleDTO toDto(Article article);
ArticleDTOCommon toDtoCommon(Article article);
}
I have a #Service layer on which how i know Hibernate creates it's proxy(for defining which field is fetch or not fetch) and transactions are occurs.
#Service
#Transactional
public class ArticleService {
#Transactional(readOnly = true)
public List<ArticleDTO> findAll() {
return articleRepository.findAll()
stream().map(articleMapper::toDto).collect(Collectors.toList());
}
#Transactional(readOnly = true)
public List<ArticleDTO> findAllCommon() {
return articleRepository.findAll()
stream().map(articleMapper::toDtoCommon).collect(Collectors.toList());
}
}
It works fine with fetching Related entity but
Problem is (fetching #Formula annotated field) when I am looking executed query on log it fetchs all time variants #Formula annotated query not depending on respective DTO.
But it must be ignored on toDtoCommon - i.e. It must not fetch variants field -> because when mapping Article to ArticleDtoCommon it not uses getVariant() field of Article. I have tried multiple ways as mentioned above.
I can solve it with writing native query(for findAllCommon() method) and map respectivelly with other way... But I want to know that how we can solve it with ORM way and where is problem.
Manupulating #Access type is not helping too.
Thanks is advance.

Lazy loading with JPA on #ManyToOne

I have a project with Spring Boot, and I cannot make lazy loading work. I have 2 entities: Question and Answer. A question can have many answers.
What I want is, when I try to get an answer, to get only the answer without the question. And also, if I want both, to have this possibility.
What I did, is I added in application.yml: spring.jpa.open-in-view: true.
The Answer entity is like:
#Entity
#Table(name = "mst_ans_answer", schema = "lquest_sc")
public class Answer implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "lquest_sc.mst_ans_answer_ans_lqs_id_seq")
#SequenceGenerator( name = "lquest_sc.mst_ans_answer_ans_lqs_id_seq", sequenceName = "lquest_sc.mst_ans_answer_ans_lqs_id_seq")
#Column(name = "ans_lqs_id")
private int id;
#Column(name = "qst_lqs_id")
private int questionId;
#Column(name = "ans_text")
private String text;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "qst_lqs_id", insertable=false, updatable=false)
#JsonIgnore
private Question question;
//getters and setters
}
The Question entity is:
#Entity
#Table(name = "mst_qst_question", schema = "lquest_sc")
public class Question implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "lquest_sc.mst_qst_question_qst_lqs_id_seq")
#SequenceGenerator(name = "lquest_sc.mst_qst_question_qst_lqs_id_seq", sequenceName = "lquest_sc.mst_qst_question_qst_lqs_id_seq")
#Column(name = "qst_lqs_id")
private int id;
#Column(name = "qst_title")
private String title;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "question")
#OrderBy("order asc")
private Set<Answer> answers = new HashSet<Answer>();
//getters and setters here
The call in the controller is:
#RequestMapping(value = "/questionId/{id}", method = RequestMethod.GET)
public List<Answer> listAll( #PathVariable("id") int id ){
List<Answer> answers = answerRepository.findByEnabledAndQuestionIdOrderByOrderAsc(1,id);
return answers;
}
and the repository is
public interface AnswerRepository extends JpaRepository<Answer, Long> {
List<Answer> findByEnabledAndQuestionIdOrderByOrderAsc(int enabled,int questionId);
}
The problem is that in the controller, when I try to evaluate
answers.get(0).getQuestion(), I receive the entity of Question, with the properties filled with null values and the error Method threw 'org.hibernate.LazyInitializationException' exception. Cannot evaluate Question_$$_jvst5b6_1.toString(). What am I doing wrong?
I don't know why spring.jpa.open-in-view = true is not working in your case. Maybe the OpenEntityManagerInViewInterceptordoes not get triggered or the thread has been left, when you are evaluating the Question. Or you have an older version which just does not support it.
Lazy loading works only inside of a transaction. A solution could be -
as #Pradeep already gave you the hint - to use #Tranactional inside a business logic class.
Even if you put the #Transactional inside your repository it will not work, because you have to put the annotation on top of the method where you are trying to evaluate answers.get(0).getQuestion().
Furthermore I advise you not to call your repository from the controller directly, but to use a service layer, where you put your business logic.
Example implementation
This is only a example implementation to show you how to structure your application and what the important keywords are. Also note, that you can either use #Inject or #Autowired. When you have implemented your logic, just inject the service into your controller and use it there.
AnswerService.java
public interface AnswerService {
List<Answer> findByEnabledAndQuestionIdOrderByOrderAsc(int enabled, Long id);
}
AnswerServiceImpl.java
#Service
public class AnswerServiceImpl implements AnswerService {
private AnswerRepository answerRepository;
#Inject
public AnswerServiceImpl(AnswerRepository answerRepository) {
this.answerRepository = answerRepository;
}
#Transactional
#Override
public List<Answer> findByEnabledAndQuestionIdOrderByOrderAsc(int enabled, Long id) {
List<Answer> answerList = findByEnabledAndQuestionIdOrderByOrderAsc(int enabled,int questionId);
// do your lazy loading here
// because you are still in the same transactional context
// return the list
return answerList;
}
}

How Do I Create Many to Many Hibernate Mapping for Additional Property from the Join Table?

I need a many to many hibernate mapping needed 3 joins. I've tried to find out a solution without intermediate entity like LecturerCourse.
I have a many to many relation in my database between my lecturer and course tables. A course can be given by several lecturer while a lecturer can give several courses.
I have courses stored before hand. However, I need to assign courses to lecturer. When I assign courses I also store the capacity of that course.
My database diagram:
I use hibernate and spring. I need a hibernate mapping when a course is assign any lecturer. I need to add values to capacity field.
My lecturer mapping :
#Entity
#Table(name="LECTURER")
public class Lecturer {
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="LECTURER_ID_SEQ")
#SequenceGenerator(name="LECTURER_ID_SEQ", sequenceName="LECTURER_ID_SEQ")
private Long Id;
#Column(name="NAME")
private String name;
#Column(name="SURNAME")
private String surname;
#Column(name="EMAIL")
private String email;
#Column(name="USERNAME")
private String username;
#Column(name="PASSWORD")
private String Password;
#ManyToMany
#JoinTable(
name="LECTURER_COURSE",
joinColumns=#JoinColumn(name="LECTURER_ID"),
inverseJoinColumns=#JoinColumn(name="COURSE_ID")
)
private List<Course> courses;
//getters - setters
}
My course mapping :
#Entity
#Table(name="COURSE")
public class Course {
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="COURSE_ID_SEQ")
#SequenceGenerator(name="COURSE_ID_SEQ", sequenceName="COURSE_ID_SEQ")
private Long id;
#Column(name="NAME")
private String name;
#Column(name="CODE")
private String code;
}
Any idea how to solve my problem ?
You need to use #EmbeddedId and #Embeddable annotations to solve this issue:
Lecturer Class:
#Entity
#Table(name="LECTURER")
public class Lecturer {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.lecturer", cascade=CascadeType.ALL)
Set<LecturerCourse> lecturerCourses == new HashSet<LecturerCourse>();
//all others properties Setters and getters are less relevant.
}
Course class:
#Entity
#Table(name="COURSE")
public class Course {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.course", cascade=CascadeType.ALL)
Set<LecturerCourse> lecturerCourses == new HashSet<LecturerCourse>();
//all others properties Setters and getters are less relevant.
}
LecturerCourse Class:
#Entity
#Table(name = "lecturer_course")
#AssociationOverrides({
#AssociationOverride(name = "pk.lecturer",
joinColumns = #JoinColumn(name = "LECTURER_ID")),
#AssociationOverride(name = "pk.course",
joinColumns = #JoinColumn(name = "COURSE_ID")) })
public class LecturerCourse {
private LecturerCourseID pk = new LecturerCourseID();
#Column(name = "CAPACITY", nullable = false, length = 10)
private String capacity;
#EmbeddedId
public LecturerCourseID getPk() {
return pk;
}
}
Now the Primary Key:
#Embeddable
public class LecturerCourseID implements java.io.Serializable {
private Lecturer lecturer;
private Course course;
#ManyToOne
public Stock getLecturer() {
return lecturer;
}
public void setLecturer(Lecturer lecturer) {
this.lecturer= lecturer;
}
#ManyToOne
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course= course;
}
}
now Your Main should be something like this:
Lecturer lecturer1 = new Lecturer();
Course math = new Course();
LecturerCourse lecturer1math = new LecturerCourse();
lecturer1math.setCapacity("capacity");
lecturer1math.setLecturer(lecturer1);
lecturer1math.setCourse(math);
lecturer1.getLecturerCourses().add(lecturer1math);
//saving object
session.save(lecturer1);
You need to be sure that class marked as #Embeddable should implement Serializable marker interface.
Hope it helps.

Resources