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.
Related
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.
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;
I am experimenting with jpa and hibernate relations. I'm using a table named users and a table named emails. A user can have many emails.
When I run my Spring boot application along with the following code I get one email record in my h2 database. The email_address of this record is testAddress and the user_username column of this record is null. The user_username column is listed as a foreign key on the emails table. My question is, why is emailRepository.save(email1) successful when there is no corresponding user in the database?
#Entity
#Table(name = "emails")
public class Email {
#Id
private String emailAddress;
#ManyToOne
private User user;
...
}
#Entity
#Table(name = "users")
public class User {
#Id
private String username;
#OneToMany(mappedBy="user", cascade=CascadeType.ALL, orphanRemoval=true)
private Set<Email> emails;
...
}
public interface UserRepository extends JpaRepository<User, String> {
}
public interface EmailRepository extends JpaRepository<Email, String> {
}
#Component
public class UserRepositoryCommandLineRunner implements CommandLineRunner {
#Autowired
private EmailRepository emailRepository;
public void run(String... args) throws Exception {
Email email1 = new Email();
email1.setEmailAddress("testAddress");
emailRepository.save(email1);
}
}
Take a look at the documentation of the JoinColumn annotation:
https://docs.jboss.org/hibernate/jpa/2.1/api/javax/persistence/JoinColumn.html#nullable()
It is mentioned:
If the JoinColumn annotation itself is defaulted, a single join column
is assumed and the default values apply.
Since you did not specify a JoinColumn in your ManyToOne mapping, then Hibernate would assume the default JoinColumn. If you take a look at the JoinColumn.nullable attribute, it is defaulted to true. Thus, when Hibernate generates your schema, the foreign key column is by default NULLABLE.
You may need to explicitly add a #JoinColumn annotation on top of your #ManyToOne mapping and set its nullable attribute to false.
#ManyToOne
#JoinColumn(nullable=false)
private User user;
This way, it'll throw out an error when you try to insert email without a user.
I have been struggling with query dsl joins. My problem is with unidirectional Many to One mapping. I am unable to fetch data from both tables as query dsl is unable to find many to one relationship entity as null. As I am using latest query dsl library and spring data.
#Entity
Public class Parent {
#Id
#GeneratedValue
private Integer id;
#ManyToOne
private Child c;
// Getters and setters.
}
#Entity
public class Child {
#Id
#GenereatedValue
private Integer id;
private String name;
// Getters and setters
}
// Query dsl method.
public List<Parent> getParentsByChildName(String name){
QParent qParent = QParent.parent ;
QChild qChild = QChild.child;
return queryFactory.select(Qparent).from(qChild).innerJoin(qParent). where (qChild.name.eq(name));
}
My domain objects are roughly like:
#Entity
public class Customer{
#Id
private String id;
private String name;
#OneToMany(cascade=CascadeType.ALL)
private List<Account> accountList;
}
#Entity
public class Account{
#Id
private String id;
private String name;
#OneToMany
private List<Service> serviceList;
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(nullable=false)
private Customer customer;
}
#Entity
public class Service{
#Id
private String id;
private String name;
#ManyToOne
#JoinColumn(nullable=false)
private Account account;
}
I have a transactional Spring service. I want to return an Account instance to fronthand but I don't want to send Customer and Service List informations because of bandwith issues.
When I do:
account.setServiceList(null);
It gives no error but after a while it deletes all my service list.
When I do:
account.setCustomer(null);
It says customer_id of account cannot be null.
I just want to return a transient instance without a validation. How can I handle this.
The solution to your problem is to make the entity detached, by calling entityManager.detach(accountInstance) (or entityManager.clear() if you use JPA 1.0 to detach all entities) and only THAN change anything to it. If you use Hibernate's sessions, use the Session.evict() method.
The other solution is to use a DTO, something that has only the fields that you need.
PS: I think you have an bug, in which the bidirectional relationships are missing on one side the mapped property. E.g in Customer you should have something like
#OneToMany(cascade=CascadeType.ALL, mappedBy="customer")
private List<Account> accountList;
The same with the other relationship.