Criteria Api generetes too much joins - spring

Entities:
#Entity
#Table(name = "shop")
public class Shop implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String address;
#OneToMany(mappedBy = "shop")
private List<Product> product = new ArrayList<>();
public Shop() {
}
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 String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
#Entity
#Table(name = "product")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "disc_col")
public class Product implements Serializable{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal price;
#ManyToOne
private Shop shop;
public Product() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Shop getShop() {
return shop;
}
public void setShop(Shop shop) {
this.shop = shop;
}
}
#Entity
#DiscriminatorValue("loose")
public class LooseProduct extends Product {
private BigDecimal weight;
public LooseProduct() {
}
public BigDecimal getWeight() {
return weight;
}
public void setWeight(BigDecimal weight) {
this.weight = weight;
}
}
#Entity
#DiscriminatorValue("liquid")
public class LiquidProduct extends Product {
private BigDecimal volume;
public LiquidProduct() {
}
public BigDecimal getVolume() {
return volume;
}
public void setVolume(BigDecimal volume) {
this.volume = volume;
}
}
Service:
public class ShopRepositoryImpl implements ShopRepositoryCustom{
#PersistenceContext
private EntityManager em;
#Override
public List<Shop> findShops(BigDecimal volume, BigDecimal weight, BigDecimal price) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Shop> cq = cb.createQuery(Shop.class);
Root<Shop> root = cq.from(Shop.class);
Join<Shop, Product> product = root.join("product", JoinType.LEFT);
Predicate p1 = cb.equal(cb.treat(product, LiquidProduct.class).get("volume"), volume);
Predicate p2 = cb.equal(cb.treat(product, LooseProduct.class).get("weight"), weight);
Predicate p3 = cb.equal(product.get("price"), price);
cq.where(cb.and(p3, cb.or(p1, p2)));
Query q = em.createQuery(cq);
return q.getResultList();
}
}
I have a problem that my query findShops generates too much joins:
select shop0_.id as id1_1, shop0_.address as address2_1, shop0_.name as name3_1 from shop shop0
left outer join product product1 on shop0_.id=product1_.shop_id
left outer join product product2 on shop0_.id=product2_.shop_id
left outer join product product3_ on shop0_.id=product3_.shop_id where product3_.price=1 and (product2_.volume=1 or product3_.weight=0)
It is InheritanceType.SINGLE_TABLE strategy so it shouldn't create three joins because there is just one table Product in database. Is there any way to optimize this?
Code from org.hibernate.query.criteria.internal.CriteriaBuilderImpl class:
#SuppressWarnings("unchecked")
private <X, T, V extends T, K extends JoinImplementor> K treat(
Join<X, T> join,
Class<V> type,
BiFunction<Join<X, T>, Class<V>, K> f) {
final Set<Join<X, ?>> joins = join.getParent().getJoins();
final K treatAs = f.apply( join, type );
joins.add( treatAs );
return treatAs;
}
The treat method creates new join from existing one. It is happening every time independent of inheritence type.
Next hibernate generates query and do not check duplicates in joins.
Do you have any idea how to prevent from generate additional joins when we use treat method?
I found the error report:
https://hibernate.atlassian.net/projects/HHH/issues/HHH-12094?filter=allissues&orderby=created%20DESC&keyword=treat

If you use JPA 2.1 onwards you can change the query like this:
select shop0_.id as id1_1, shop0_.address as address2_1, shop0_.name as name3_1
from shop shop0
left join product product1 on shop0_.id=product1_.shop_id
and (
(product1.disc_col = 'loose' and product1.weight = weight_var)
or (product1.disc_col = 'liquid' and product1.volume = volume_var)
);
For the implementation you would need to add the mapping of the disc_col column to the Product entity.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Shop> cq = cb.createQuery(Shop.class);
Root<Shop> root = cq.from(Shop.class);
Join<Shop, Product> product = root.join("product", JoinType.LEFT);
//add other conditions
product.on(
cb.and(
cb.or(
cb.and(cb.equal(product.get("discCol"),"liquid"),cb.equal(product.get("volume"),volumeVar)),
cb.and(cb.equal(product.get("discCol"),"loose"),cb.equal(product.get("weight"),weightVar))
)
)
);
Query q = em.createQuery(cq);
return q.getResultList();

Related

How to map multiple classes to one table in JPA?

I know this is a frequently asked question, but I couldn't find any answers because I have 3 classes and I generally have problems to build the given structure:
type OrderItem = {
count: number,
price: number,
order: number,
subItems: {
count: number,
name: string,
price: number,
extraItems: {
count: number,
name: string,
price: number,
}
}
};
This is my try at doing it in Java with JPA:
Order.java
package de.gabriel.mcdonaldsproject.models;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
#Entity
#Table(name = "orders", schema = "public")
public class Order implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_generator")
#SequenceGenerator(name = "order_generator", sequenceName = "order_seq")
private long id;
private List<Item> products; // <--------- 'Basic' attribute type should not be a container
public Order() {
}
public Order(List<Item> products) {
this.products = products;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public List<Item> getProducts() {
return products;
}
public void setProducts(List<Item> product) {
this.products = product;
}
}
Item.java
package de.gabriel.mcdonaldsproject.models;
import javax.persistence.*;
public class Item{
private double count;
private double price;
private double order;
private SubItems subItems;
public Item(){}
public Item(double count, double price, double order, SubItems subItems) {
this.count = count;
this.price = price;
this.order = order;
this.subItems = subItems;
}
public double getCount() {
return count;
}
public void setCount(double count) {
this.count = count;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public double getOrder() {
return order;
}
public void setOrder(double order) {
this.order = order;
}
public SubItems getSubItems() {
return subItems;
}
public void setSubItems(SubItems subItems) {
this.subItems = subItems;
}
}
SubItems.java
package de.gabriel.mcdonaldsproject.models;
import javax.persistence.*;
import java.util.List;
public class SubItems {
private double count;
private String name;
private double price;
private List<String> extraItems;
public SubItems(){}
public SubItems(double count, String name, double price, List<String> extraItems) {
this.count = count;
this.name = name;
this.price = price;
this.extraItems = extraItems;
}
public double getCount() {
return count;
}
public void setCount(double count) {
this.count = count;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public List<String> getExtraItems() {
return extraItems;
}
public void setExtraItems(List<String> extraItems) {
this.extraItems = extraItems;
}
}
Does someone have an idea on how to rebuild this structure in Java with JPA so it also gets saved in the database?
If this object orderitem is not going to expand, I would suggest JSON string saving in the database.
OR you can do following mappings:
#OneToMany(mappedBy="order")
public Order(List<Item> products) {
this.products = products;
}
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "subitem_id", referencedColumnName = "id")
private SubItems subItems;
Update the following information like this :
#Embeddable
public class Item {
// .....
#Embedded
private SubItems subItems;
//.......
}
#Embeddable
public class SubItems {
// .....
#ElementCollection
private List<String> extraItems;
//.......
}
#Entity
#Table(name = "orders", schema = "public")
public class Order implements Serializable {
//.....
#ElementCollection
private List<Item> products;
//.......
}

I cannot remove the association in Many To Many bidirectional hibernate

I can't delete the association in the courses_student table of course and student when trying to delete a course, even if I want to cascade delete it does not work for me since there is a foreign key in courses_student, I don't know what the problem is.
I have also tried to remove the association in the courses_student table doing a update.but nothing happened.
DAO
#Override
public boolean deleteCourse(int id) {
Session currentSession = entityManager.unwrap(Session.class);
Courses course = currentSession.load(Courses.class, id);
for(Student student : course.getEstudiantes()) {
course.removeStudent(student);
}
currentSession.delete(course);
if(course.getId() == null)
return true;
else
return false;
}
Courses entity
#Entity
#Table(name = "courses")
public class Courses {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
private Integer id;
#Column
private String nombre;
#Column
private String descripcion;
#ManyToMany(mappedBy = "courses")
private Set<Student> Estudiantes = new HashSet<Student>();
public Courses() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getDescripcion() {
return descripcion;
}
public void setDescripcion(String descripcion) {
this.descripcion = descripcion;
}
public Set<Student> getEstudiantes() {
return Estudiantes;
}
public void setEstudiantes(Set<Student> estudiantes) {
Estudiantes = estudiantes;
}
public void removeStudent(Student student) {
this.Estudiantes.remove(student);
student.getCourses().remove(this);
}
}
Student entity
#Entity
#Table(name = "students")
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
private Integer id;
#Column
private String nombre;
#Column
private String apellido;
#Column
private String dni;
#ManyToMany(fetch=FetchType.LAZY,
cascade= {CascadeType.PERSIST, CascadeType.MERGE,
CascadeType.DETACH, CascadeType.REFRESH})
#JoinTable(
name="courses_students",
joinColumns=#JoinColumn(name="id_student"),
inverseJoinColumns=#JoinColumn(name="id_course")
)
private Set<Courses> courses = new HashSet<Courses>();
public Student() {
}
public Student(String nombre, String apellido, String dni) {
this.nombre = nombre;
this.apellido = apellido;
this.dni = dni;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public String getApellido() {
return apellido;
}
public void setApellido(String apellido) {
this.apellido = apellido;
}
public String getDni() {
return dni;
}
public void setDni(String dni) {
this.dni = dni;
}
public Set<Courses> getCourses() {
return courses;
}
public void setCourses(Set<Courses> courses) {
this.courses = courses;
}
}
EDIT:
apparently it works for me, trying to update since owner side.
#Override
public boolean deleteCourse(int id) {
Session currentSession = entityManager.unwrap(Session.class);
Courses course = currentSession.load(Courses.class, id);
for(Student student : course.getEstudiantes()) {
student.removeCourse(course);
}
currentSession.update(course);
if(course.getId() == null)
return true;
else
return false;
}
It seems to me that you are missing a cascade configuration of your #ManyToMany annotation on Courses which is actually the one you are updating / deleting. Try the following:
#ManyToMany(mappedBy = "courses", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<Student> Estudiantes = new HashSet<Student>();
Also, given that you have a bi-directional relationship, you should also remove the Course from each Student courses property.

How to load a full graph of 2 entities that are in relationship #OneToMany each other with a Join Table

I'm using Spring Boot and Spring Data and I have a problem when trying to load entities using JPA and EntityGraph.
I have a Patient and Insurance entities. Each Patient can have many Insurances and each Insurance can be assigned to many patients. I decided to use a Join Table PatientInsurance because I need to store extra fields like 'active', and also the relation code (a Patient can be a Member, Spouse, or Child for that specific insurance).
Using Spring Data repositories I annotated the method to find a patient, with an EntityGraph, to have ready the list of PatientInsurances (and Insurances) for that patient in one query.
This is the code (I removed the non-necessary parts in the scope)
Patient class
#Entity
#Table(name = "patient")
public class Patient {
#NotNull
#NotEmpty
#Column(length = 60, nullable = false)
private String patientFirstName;
#NotNull
#NotEmpty
#Column(length = 60, nullable = false)
private String patientLastName;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "patient", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
List<PatientInsurance> patientsInsurances = new ArrayList<>();
public void addPatientInsurance(PatientInsurance patientIns) {
if (!patientsInsurances.contains(patientIns)) {
patientsInsurances.add(patientIns);
}
}
//other properties...
Insurance class
#Entity
#Table(name = "insurance")
public class Insurance {
#Column(name = "policy_id", length = 20)
private String policyId;
#OneToMany(mappedBy = "insurance", fetch = FetchType.LAZY,cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
private List<PatientInsurance> patientsInsurances = new ArrayList<PatientInsurance>();
public void addPatientInsurance(PatientInsurance patientIns) {
if (!patientsInsurances.contains(patientIns)) {
patientsInsurances.add(patientIns);
}
}
//other properties
Entity for the join table between patient and insurance (needed a join table for extra field in this entity like active and relCode
#Entity
#IdClass(PatientInsurance.PatientInsurancePK.class)
#Table(name = "patient_insurance")
public class PatientInsurance implements Serializable {
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "patient_id")
private Patient patient;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "insurance_id")
private Insurance insurance;
#Column(name = "active")
private boolean active;
#Column(length = 1)
private String relCode;
public PatientInsurance() {
insurance = new Insurance();
patient = new Patient();
}
public PatientInsurance(Patient p, Insurance i, boolean active, String relCode) {
this.patient = p;
this.insurance = i;
this.active = active;
this.relCode = relCode;
p.addPatientInsurance(this);
i.addPatientInsurance(this);
}
public Patient getPatient() {
return patient;
}
public Insurance getInsurance() {
return insurance;
}
public void setInsurance(Insurance insurance) {
this.insurance = insurance;
insurance.addPatientInsurance(this);
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public void setPatient(Patient patient) {
this.patient = patient;
patient.addPatientInsurance(this);
}
public String getRelCode() {
return relCode;
}
public void setRelCode(String relCode) {
this.relCode = relCode;
}
static public class PatientInsurancePK implements Serializable {
protected Patient patient;
protected Insurance insurance;
public PatientInsurancePK() {
}
public PatientInsurancePK(Patient patient, Insurance insurance) {
this.patient = patient;
this.insurance = insurance;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PatientInsurancePK)) return false;
PatientInsurancePK that = (PatientInsurancePK) o;
if (!patient.equals(that.patient)) return false;
return insurance.equals(that.insurance);
}
#Override
public int hashCode() {
int result = (patient != null) ? patient.hashCode() : 0;
result = 31 * result + ((insurance != null) ? insurance.hashCode() : 0);
return result;
}
}
}
Implementation of the PatientService
#Transactional
#Service("patientService")
public class PatientServiceImpl implements PatientService {
#Autowired
PatientRepository patientRepository;
#Override
public Optional<Patient> findByIdFull(Long id) {
Optional<Patient> patient = patientRepository.findById(id);
return patient;
}
//other methods...
Patient Repository
public interface PatientRepository extends JpaRepository<Patient, Long> {
#EntityGraph(
attributePaths = {
"patientsInsurances",
"patientsInsurances.patient",
"patientsInsurances.insurance"},
type = EntityGraph.EntityGraphType.LOAD)
Optional<Patient> findById(Long id);
A snippet of code that calls the method in PatientService
Optional<Patient> patientOptional = patientService.findByIdFull(p.getId());
if (patientOptional.isPresent()) {
Patient patient1 = patientOptional.get();
List<PatientInsurance> patientInsurances = patient1.getPatientInsurances();
PatientInsurances patientInsurance = patientInsurances.get(0);
Patient patient2 = patientInsurance.getPatient(); //and this is same istance of patient1, it's ok
Insurance insurance = patientInsurance.getInsurance();
//here is the problem!!!
insurance.getPatientInsurances();
//Unable to evaluate the expression Method threw 'org.hibernate.LazyInitializationException' exception.
So the problem seems that when I go inside the patient side, I can loop into his Insurances without problems, but when I try to do the same starting from the Insurance instance, I cannot loop into its patients cause they are lazily loaded.
So how to make jpa download the full graph in the correct way?

Spring JPARepository querying many to many intersection table

I have 3 entity classes as follows (Example taken from https://hellokoding.com/jpa-many-to-many-extra-columns-relationship-mapping-example-with-spring-boot-maven-and-mysql/)
Book class
#Entity
public class Book{
private int id;
private String name;
private Set<BookPublisher> bookPublishers;
public Book() {
}
public Book(String name) {
this.name = name;
bookPublishers = new HashSet<>();
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
public Set<BookPublisher> getBookPublishers() {
return bookPublishers;
}
public void setBookPublishers(Set<BookPublisher> bookPublishers) {
this.bookPublishers = bookPublishers;
}
}
Publisher class
#Entity
public class Publisher {
private int id;
private String name;
private Set<BookPublisher> bookPublishers;
public Publisher(){
}
public Publisher(String name){
this.name = name;
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "publisher")
public Set<BookPublisher> getBookPublishers() {
return bookPublishers;
}
public void setBookPublishers(Set<BookPublisher> bookPublishers) {
this.bookPublishers = bookPublishers;
}
}
Intersection Table
#Entity
#Table(name = "book_publisher")
public class BookPublisher implements Serializable{
private Book book;
private Publisher publisher;
private Date publishedDate;
#Id
#ManyToOne
#JoinColumn(name = "book_id")
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
#Id
#ManyToOne
#JoinColumn(name = "publisher_id")
public Publisher getPublisher() {
return publisher;
}
public void setPublisher(Publisher publisher) {
this.publisher = publisher;
}
#Column(name = "published_date")
public Date getPublishedDate() {
return publishedDate;
}
public void setPublishedDate(Date publishedDate) {
this.publishedDate = publishedDate;
}
}
I want to query 2 things,
Get list of books belonging to a particular publisher e.g. get all books associated with publisher 100
Get list of books not associated with a particular publisher e.g. get all books not associated with publisher 100
I want to achieve this using a simple JPARepository query if possible like findByXYZIn(...) etc.
Please let me know if querying a many to many relation is possible using JPA repository queries and if yes, whether I can do it directly or would it require any changes in the entity classes
In BookRepository
Get publisher's books
findBooksByBookPublishersPublisherId(Long publisherId)
Get books not published by publisher
findBooksByBookPublishersPublisherIdNot(Long publisherId)
IMHO Publication is much more apropriate name then BookPublisher in your case as Publisher by itself could be BookPublisher (a published that publishing books)
I'm not sure if you can make it just by method name. But you definitely can use JPA query. Something like this: "SELECT b FROM Book b JOIN b.bookPublishers bp JOIN bp.publisher p WHERE p.id = ?1". and with not equal for the second case
Well you can use named Queries to fulfill your requirements:
#Query("select b from Book b where b.publisher.idd = ?1")
Book findByPublisherId(int id);
#Query("select b from Book b where b.publisher.idd <> ?1")
Book findByDifferentPublisherId(int id);
Take a look at Using #Query Spring docs for further details.

(Hibernate) Can't add entity with many-to-many relationship

Basically, my goal is to have product and factory entity where relationship is many-to-many.
Factory must have some product, while product is not necessary associated with a factory. The problem is that when I try to add Product entity I get this error.
Caused by: org.hsqldb.HsqlException: integrity constraint violation: NOT NULL check constraint; SYS_CT_10103 table: PRODUCT column: FACT_ID
This is my Application class. I want to be able to save product to repository without giving it factory list but I got that error, so I tried adding what's in the comment but the same error remains.
#SpringBootApplication
public class Application implements CommandLineRunner {
#Autowired
private ProductRepo prodRepo;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
#Transactional
public void run(String... strings) throws Exception {
Product one = new Product();
/*
List<Product> productList= new ArrayList<Product>();
productList.add(one);
SomeFactory factory_one = new SomeaFactory("one", productList);
List<Factory> factoryList = new ArrayList<Factory>();
factoryList.add(factory_one);
one.setFactoryList(factoryList);
*/
prodRepo.save(one);
}
Product.java
#Entity
#Table(name="PRODUCT")
public class Product {
private int id;
private List<Factory> factoryList = null;
public Product() {}
#Id
#Column(name = "PROD_ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
public int getId() { return id; }
public void setId(int id) { this.id = id; }
#ManyToMany(fetch = FetchType.LAZY, mappedBy = "productList", targetEntity = Factory.class)
public List<Factory> getFactoryList() {
return factoryList;
}
public void setFactoryList(List<Factory> f) {
this.factoryList = f;
}
}
SomeFactory.java
#Entity
#Table(name ="SOME_FACT")
public class SomeFactory extends Factory {
private String name;
private List<Product> productList;
public SomeFactory() {}
public SomeFactory(String name, List<Product> products) {
this.name = name;
this.productList = products;
}
#Column(name = "FACT_NAME", nullable = false)
public String getName() { return name; }
public void setName(String name) { this.name = name; }
#ManyToMany
#JoinTable(name = "PRODUCT",
joinColumns = #JoinColumn(name = "FACT_ID", nullable = true),
inverseJoinColumns = #JoinColumn(name = "PROD_ID", nullable = false))
public List<Product> getProductList() {
return productList;
}
public void setProductList(List<Product> products) {
this.productList = products;
}
}
Factory.java
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Factory {
int id;
public Factory() {}
#Id
#Column(name="FACT_ID")
#GeneratedValue(strategy = GenerationType.TABLE)
int getId() { return id; }
void setId(int id) { this.id = id; }
}

Resources