Hibernate/JPA querying embeddable property of a joined subclass - java

I have the following entities: ShoppingCart, abstract class User and EndUser that extends User. AddressDetails is an embedable that is embeded into EndUser entity.
My query looks like this: SELECT sc FROM ShoppingCart sc JOIN sc.endUser as endUser WHERE endUser.name EQ someName and endUser.addressDetails.zip EQ 1234
When I remove the second part of the WHERE clause, and leave just the endUser.name part, everything works fine (name is a property of endUser entity class which is a subclass of User entity class).
However, when I try the whole query I get:
org.hibernate.QueryException: could not resolve property: zip of:
ShoppingCart:
#Entity
public class ShoppingCart {
...
#ManyToOne(fetch = FetchType.LAZY)
#JoinTable
private EndUser endUser;
}
User:
#Entity
public abstract class User {
...
}
EndUser:
#Entity
public class EndUser extends User {
...
#Column
private String name;
#Embeded
private AddressDetails addressDetails;
...
}
Address Details:
#Embeddable
public class AddressDetails {
...
private int zip;
...
}

I actually found the problem.
When I change FetchType to EAGER on #ManyToOne reladtionship between ShoppingCart and endUser the query works.
So it should be:
#ManyToOne(fetch = FetchType.EAGER)
#JoinTable
private EndUser endUser;

Related

Fetch join to attribute from another object using Spring Specification

I have the following entities:
#Entity
#Table(name = "user_data")
public class UserData {
...
#ManyToOne
private User user;
...
}
#Entity
#Table(name = "user_cars")
public class UserCar {
...
private Integer userId;
...
}
#Entity
#Table(name = "users")
public class User {
...
#OneToMany(mappedBy = "userId", cascade = CascadeType.ALL)
private List<UserCar> userCars;
...
}
As you can see, userCars are loaded lazily (and I am not going to change it). And now I use Specifications in order to fetch UserData:
public Page<UserData> getUserData(final SpecificationParameters parameters) {
return userDataRepository.findAll(createSpecification(parameters), parameters.pageable);
}
private Specification<UserData> createSpecification(final SpecificationParameters parameters) {
final var clazz = UserData.class;
return Specification.where(buildUser(parameters.getUserId()));
}
private Specification<UserData> buildUser(final Integer userId) {
return (root, criteriaQuery, criteriaBuilder) -> {
if (Objects.nonNull(userId)) {
final Join<UserData, User> joinParent = root.join("user");
return criteriaBuilder.equal(joinParent.get("id"), userId);
} else {
return criteriaBuilder.isTrue(criteriaBuilder.literal(true));
}
};
}
But I have no idea how to add there a fetch join clause in order to fetch user cars. I tried to add it in different place and I got either LazyInitializationException (so it didn't work) or some other exceptions...
Slightly different approach from the prior answer, but I think the idea jcc mentioned is on point, i.e. "Hibernate is complaining because it it unable to find the owner, user in this case, of the userCars relationship."
To that end, I'm wondering if the Object-Relational engine is getting confused because you have linked directly to a userId (a primitive) instead of a User (the entity). I'm not sure if it can assume that "userId" the primitive necessarily implies a connection to the User entity.
Can you try to re-arrange the mapping so that it's not using an integer UserId in the join table and instead using the object itself, and then see if it allows the entity manager to understand your query better?
So the mapping might look something like this:
#Entity
#Table(name = "user_cars")
public class UserCar {
...
#ManyToOne
#JoinColumn(name="user_id", nullable=false) // Assuming it's called user_id in this table
private User user;
...
}
#Entity
#Table(name = "users")
public class User {
...
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<UserCar> userCars;
...
}
It would also be more in line with
https://www.baeldung.com/hibernate-one-to-many
In addition of the suggestion #crizzis provided in the question comments, please, try to join fetch the user relationship as well; in the error you reported:
org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list
Hibernate is complaining because it it unable to find the owner, user in this case, of the userCars relationship.
It is strange in a certain way because the #ManyToOne relationship will fetch eagerly the user entity and it will be projected as well while obtaining userData but probably Hibernate is performing the query analysis prior to the actual fetch phase. It would be great if somebody could provide some additional insight about this point.
Having said that, please, consider to set the fetch strategy explicitly to FetchType.LAZY in your #ManyToOne relationship:
#Entity
#Table(name = "user_data")
public class UserData {
...
#ManyToOne(fetch= FetchType.LAZY)
private User user;
...
}
Your Specification code can look like the following:
public Page<UserData> getUserData(final SpecificationParameters parameters) {
return userDataRepository.findAll(createSpecification(parameters), parameters.pageable);
}
private Specification<UserData> createSpecification(final SpecificationParameters parameters) {
final var clazz = UserData.class;
return Specification.where(buildUser(parameters.getUserId()));
}
private Specification<UserData> buildUser(final Integer userId) {
return (root, criteriaQuery, criteriaBuilder) -> {
// Fetch user and associated userCars
final Join<UserData, User> joinParent = (Join<UserData, User>)root.fetch("user");
joinParent.fetch("userCars");
// Apply filter, when provided
if (Objects.nonNull(userId)) {
return criteriaBuilder.equal(joinParent.get("id"), userId);
} else {
return criteriaBuilder.isTrue(criteriaBuilder.literal(true));
}
};
}
I did not pay attention to the entity relations themself previously, but Atmas give you a good advice indeed, it will be the more performant way to handle the data in that relationship.
At least, it would be appropriate to define the relationship between User and UserCars using a #JoinColumn annotation instead of mapping through a non entity field in order to prevent errors or an incorrect behavior of your entities. Consider for instance:
#Entity
#Table(name = "users")
public class User {
...
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "user_id")
private List<UserCar> userCars;
...
}

Spring Data JPA - ManyToMany - JPQL - #Query formation in Repository

I have a spring boot project with PostgreSQL RDBMS.
I have #ManyToMany relation between two entities - Customer & Product. They are joined by the customer_product. But while forming JPQL at repository layer, I am facing difficulties. Here is entity:
#Entity
#NamedQuery(name="Customer.findAll", query="SELECT c FROM Customer c")
public class Customer implements Serializable {
... all properties ... getter setter constructor...
//bi-directional many-to-many association to Product
#JsonIgnore
#ManyToMany
#JoinTable(
name="customer_product"
, joinColumns={
#JoinColumn(name="customer_id")
}
, inverseJoinColumns={
#JoinColumn(name="product_id")
}
)
private List<Product> products;
#Entity
#NamedQuery(name="Product.findAll", query="SELECT p FROM Product p")
public class Product implements Serializable {
... all properties ... getter setter ... constructors ...
//bi-directional many-to-many association to Customer
#JsonIgnore
#ManyToMany(mappedBy="products")
private List<Customer> customers;
Now at the repository later:
#Repository
public interface CustomerRepository extends CrudRepository<Customer, Integer> {
//Below mentioned query works perfectly as Customer & Address has OneToMany Relation
#Query("select new com.arindam.springjpa.springdatajpaexamples.vo.CustomerDetailsVO(c.id, c.customerName, a.fullAddress) from Customer c, Address a where c.address.addressId=a.addressId")
List<CustomerDetailsVO> findAllCustomerDetails();
// But I need help on below query
// I want to find out full details of ManyToMany relation between customer & product
#Query("select new com.arindam.springjpa.springdatajpaexamples.vo.CustomerProductAddressDetailsVO(c.id, c.customerName, a.fullAddress, p.productName) from Customer c, Address a, Product p where c.address.addressId=a.addressId and c.products.product.productId=p.productId")
List<CustomerProductAddressDetailsVO> findAllCustomerAddressProductDetails();
To have results in VO here is simple VO class
#Entity
public class CustomerProductAddressDetailsVO {
private Integer id;
private String customerName;
private String fullAddress;
private String productName;
//Like a simple POJO with getter setter constructors
Can you please suggest.
Thanks for your valuable feedback.
I have resolved the issue. Since the entity classes have been generated using Eclipse plugins, so the class for customer_product was not been generated. So I manually generated it and used the query. So the final code is:
#Entity
#Table(name="customer_product")
#NamedQuery(name="CustomerProduct.findAll", query="SELECT c FROM CustomerProduct c")
public class CustomerProduct implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private Integer id;
#Column(name="customer_id")
private Integer customerId;
#Column(name="product_id")
private Integer productId;
and Repository layer:
#Query("select new com.arindam.springjpa.springdatajpaexamples.vo.CustomerProductAddressDetailsVO(cp.id, c.customerName, a.fullAddress, p.productName) from Customer c, Address a, Product p, CustomerProduct cp "
+ "where c.address.addressId=a.addressId and cp.productId=p.productId and cp.customerId=c.id")
List<CustomerProductAddressDetailsVO> findAllCustomerAddressProductDetails();
It works perfectly.

Three entities mapping is not working properly because of lazy/eager loading in JPA

I am mapping three entities. Doctor, Client (which extends a Person) and MedicalConsultation.
See my code above. Consider all models class with a default constructor, a constructor with all fields and the getters and setters:
#Entity
#Table (name= "person")
public abstract class Person {
#Id #GeneratedValue
protected Long id;
protected String name;
protected String email;
protected String password;
#OneToOne
protected Address address;
Now the class Doctor.
#Entity(name = "doctor")
public class Doctor extends Person{
#OneToMany(mappedBy="doctor" , fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonManagedReference(value = "job-historical")
private List<MedicalConsultation> medicalConsultations;
#Enumerated
private Type type;
#ElementCollection
private List<String> specialties;
public Doctor() {
super();
}
public Doctor(String name, String email, String password, Address address,
List<String> specialties, Type type,
List<MedicalConsultation> medicalConsultations) {
super(name,email,password,address);
this.setMedicalConsultations(medicalConsultations);
this.setSpecialties(specialties);
this.setType(type);
}
My constructors calls the super() and sets their values acording to the super class and its own properties. The same happens with the Client class.
#Entity(name = "client")
public class Client extends Person{
#JsonManagedReference(value = "client-consultations-historical")
#OneToMany(mappedBy="doctor" , fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<MedicalConsultation> medicalConsultations;
Here, the Medical Consultation model, which get the other two models
#Entity
#Table(name = "medical_consultation")
public class MedicalConsultation {
#Id
#GeneratedValue
private Long id;
#JsonBackReference(value = "job-historical")
#ManyToOne
#JoinColumn(name="doctor_fk")
private Doctor doctor;
#ManyToOne
#JoinColumn(name="client_fk")
#JsonBackReference( value = "client-consultations-historical")
private Client client;
#JsonFormat(pattern = "dd/MM/yyyy hh:mm")
private Date date;
private BigDecimal price;
Finally, we got the problem:
On my controller class, I cannot get the full data of medicalConsultations. That is, I got the data, the ID and the price, but I do not get the Client and the Doctor for some reason.
But, if I call the method getDoctor() or getClient and return one of them, I do see all the information.
See the method on the RestControl Class:
#RestController
public class Control {
#Autowired
private PersonRepository personRepo;
#Autowired
private ClientRepository clientRepo;
#Autowired
private AddressRepository addressRepo;
#Autowired
private DoctorRepository doctorRepo;
#Autowired
private MedicalConsultationRepository consultationRepo;
#GetMapping("consultations")
public List<MedicalConsultation> getConsultations() {
List<MedicalConsultation> consultations = this.consultationRepo.findAll();
return consultations;
}
Maybe there is something wrong on the Mapping. But I set the hibernate to show the sql, and it apparently makes all the query getting everything that I want. See:
Hibernate:
select
medicalcon0_.id as id1_2_,
medicalcon0_.client_fk as client_f4_2_,
medicalcon0_.date as date2_2_,
medicalcon0_.doctor_fk as doctor_f5_2_,
medicalcon0_.price as price3_2_
from
medical_consultation medicalcon0_
Hibernate:
select
client0_.id as id2_3_0_,
client0_.address_id as address_7_3_0_,
client0_.email as email3_3_0_,
client0_.name as name4_3_0_,
client0_.password as password5_3_0_,
address1_.id as id1_0_1_,
address1_.city as city2_0_1_,
address1_.number as number3_0_1_,
address1_.phone as phone4_0_1_,
address1_.street as street5_0_1_,
medicalcon2_.doctor_fk as doctor_f5_2_2_,
medicalcon2_.id as id1_2_2_,
medicalcon2_.id as id1_2_3_,
medicalcon2_.client_fk as client_f4_2_3_,
medicalcon2_.date as date2_2_3_,
medicalcon2_.doctor_fk as doctor_f5_2_3_,
medicalcon2_.price as price3_2_3_,
client3_.id as id2_3_4_,
client3_.address_id as address_7_3_4_,
client3_.email as email3_3_4_,
client3_.name as name4_3_4_,
client3_.password as password5_3_4_
from
person client0_
left outer join
address address1_
on client0_.address_id=address1_.id
left outer join
medical_consultation medicalcon2_
on client0_.id=medicalcon2_.doctor_fk
left outer join
person client3_
on medicalcon2_.client_fk=client3_.id
where
client0_.id=?
and client0_.dtype='client'
Hibernate:
select
doctor0_.id as id2_3_0_,
doctor0_.address_id as address_7_3_0_,
doctor0_.email as email3_3_0_,
doctor0_.name as name4_3_0_,
doctor0_.password as password5_3_0_,
doctor0_.type as type6_3_0_,
address1_.id as id1_0_1_,
address1_.city as city2_0_1_,
address1_.number as number3_0_1_,
address1_.phone as phone4_0_1_,
address1_.street as street5_0_1_,
medicalcon2_.doctor_fk as doctor_f5_2_2_,
medicalcon2_.id as id1_2_2_,
medicalcon2_.id as id1_2_3_,
medicalcon2_.client_fk as client_f4_2_3_,
medicalcon2_.date as date2_2_3_,
medicalcon2_.doctor_fk as doctor_f5_2_3_,
medicalcon2_.price as price3_2_3_,
client3_.id as id2_3_4_,
client3_.address_id as address_7_3_4_,
client3_.email as email3_3_4_,
client3_.name as name4_3_4_,
client3_.password as password5_3_4_
from
person doctor0_
left outer join
address address1_
on doctor0_.address_id=address1_.id
left outer join
medical_consultation medicalcon2_
on doctor0_.id=medicalcon2_.doctor_fk
left outer join
person client3_
on medicalcon2_.client_fk=client3_.id
where
doctor0_.id=?
and doctor0_.dtype='doctor'
Can Someone tell me what is happing?
try you mapping annotation like bellow on MedicalConsultation.
#ManyToOne(fetch = FetchType.EAGER)
private Doctor doctor;

#OneToOne from two classes to same class

For better understanding, I want to achieve this:
Note: Buyer may not have ExternalAccount but Seller must have it. What I have/tried:
Buyer Class:
#Entity
public class Buyer extends User {
#OneToOne(optional=true, cascade= {CascadeType.MERGE})
#JoinColumn(nullable=true)
private ExternalAccount externalAccount;
//getters and setters
}
Seller Class:
#Entity
public class Seller extends User {
#OneToOne(optional=false, cascade= {CascadeType.MERGE})
#MapsId
#JoinColumn(nullable=false)
private ExternalAccount externalAccount;
//getters and setters and other properties
}
ExternalAccount class:
#Entity
public class ExternalAccount {
#Id
#PrimaryKeyJoinColumn
private Long id;
//getters and setters
}
I am using Spring Data JPA with Spring Boot and I want that:
If there's no Buyer related but ExternalAccount exists (associated with Seller), associate it.
If there's no Seller related but ExternalAccount exists (associated with Buyer), associate it.
If no ExternalAccount exists, when saving Buyer/Seller, creates the ExternalAccount.
I could achieve similar behavior with CascadeType.MERGE (after reading a lot of posts of Stackoverflow), but using this it doesn't respect #OneToOne mapping. It allows to create a lot of Buyers related to the same ExternalAccount.
I've created a github project with the database tests to reproduce the issue.
https://github.com/ralphavalon/jpa-mapping
There, I have my example rest controllers (MappingController):
//Creating buyer example
#RequestMapping(value = "/newBuyer", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Object newBuyer() {
Buyer buyer = new Buyer();
buyer.setBirthdate(LocalDateTime.now());
buyer.setEmail("buyer#email.com");
buyer.setName("Buyer Name");
ExternalAccount external = new ExternalAccount();
external.setId(123L);
buyer.setExternalAccount(external);
buyerDao.save(buyer);
return buyer;
}
//Creating seller example
#RequestMapping(value = "/newSeller", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Object newSeller() {
Seller seller = new Seller();
seller.setBirthdate(LocalDateTime.now());
seller.setEmail("seller#email.com");
seller.setName("Seller Name");
ExternalAccount external = new ExternalAccount();
external.setId(123L);
seller.setExternalAccount(external);
sellerDao.save(seller);
return seller;
}
When I call /newBuyer at the first time, it saves. Now, if I call /newSeller after calling /newBuyer it returns this:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["PRIMARY KEY ON PUBLIC.EXTERNAL_ACCOUNT(ID)"; SQL statement:
insert into external_account (id) values (?) [23505-196]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause
org.h2.jdbc.JdbcSQLException: Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.EXTERNAL_ACCOUNT(ID)"; SQL statement:
insert into external_account (id) values (?) [23505-196]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:345) ~[h2-1.4.196.jar:1.4.196]
I solved the issue with these changes:
Changing the mapping of the classes:
Buyer class:
#Entity
public class Buyer extends User {
#OneToOne(optional=true, cascade= {CascadeType.MERGE})
#JoinColumn(nullable=true, unique=true)
private ExternalAccount externalAccount;
//getters and setters
}
Seller class:
#Entity
public class Seller extends User {
#OneToOne(optional=false, cascade= {CascadeType.MERGE})
#JoinColumn(nullable=false, unique=true)
private ExternalAccount externalAccount;
//getters and setters
}
ExternalAccount class:
#Entity
public class ExternalAccount {
#Id
private Long id;
//getters and setters
}
And the most important part: Override Spring Data JPA save method to use entityManager.merge
#Service
public class BuyerService {
#PersistenceContext
private EntityManager entityManager;
#Transactional
public Buyer save(Buyer buyer) {
return entityManager.merge(buyer);
}
}
And the same thing with SellerService.

#Transient in spring data doesn't work

I have Settlement entity
#Entity
#Table(name = "settlement")
public class Settlement {
#ManyToOne
#JoinColumn(name = "subscription_x_product_id")
private ProductSubscription productSubscription;
which related to ProductSubscription entity
#Entity
#Table(name = "subscriptionproduct")
public class ProductSubscription {
#ManyToOne
#JoinColumn(name = "product_id")
private Product product;
which related to Product entity
#Entity
public class Product {
#Transient
private String enabled;
in Product entity i have field enabled which annotated with #org.springframework.data.annotation.Transient.
also I have Repository
public interface SettlementRepository extends JpaRepository<Settlement, Integer>
when I call SettlementRepository.findAll(); it give exception Caused by: com.microsoft.sqlserver.jdbc.SQLServerException: Invalid column name 'enabled'.
How can I ignore the enabled field from being loaded from the DB ?
I found the solution, the problem was in Annotation #org.springframework.data.annotation.Transient once I changed to #javax.persistence.Transient it worked fine.

Categories