Hibernate triggering constraint violations using orphanRemoval - java

I'm having trouble with a JPA/Hibernate (3.5.3) setup, where I have an entity, an "Account" class, which has a list of child entities, "Contact" instances. I'm trying to be able to add/remove instances of Contact into a List<Contact> property of Account.
Adding a new instance into the set and calling saveOrUpdate(account) persists everything lovely. If I then choose to remove the contact from the list and again call saveOrUpdate, the SQL Hibernate seems to produce involves setting the account_id column to null, which violates a database constraint.
What am I doing wrong?
The code below is clearly a simplified abstract but I think it covers the problem as I'm seeing the same results in different code, which really is about this simple.
SQL:
CREATE TABLE account ( INT account_id );
CREATE TABLE contact ( INT contact_id, INT account_id REFERENCES account (account_id) );
Java:
#Entity
class Account {
#Id
#Column
public Long id;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "account_id")
public List<Contact> contacts;
}
#Entity
class Contact {
#Id
#Column
public Long id;
#ManyToOne(optional = false)
#JoinColumn(name = "account_id", nullable = false)
public Account account;
}
Account account = new Account();
Contact contact = new Contact();
account.contacts.add(contact);
saveOrUpdate(account);
// some time later, like another servlet request....
account.contacts.remove(contact);
saveOrUpdate(account);
Result:
UPDATE contact SET account_id = null WHERE contact_id = ?
Edit #1:
It might be that this is actually a bug
http://opensource.atlassian.com/projects/hibernate/browse/HHH-5091
Edit #2:
I've got a solution that seems to work, but involves using the Hibernate API
class Account {
#SuppressWarnings("deprecation")
#OneToMany(cascade = CascadeType.ALL, mappedBy = "account")
#Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
#JoinColumn(name = "account_id", nullable = false)
private Set<Contact> contacts = new HashSet<Contact>();
}
class Contact {
#ManyToOne(optional = false)
#JoinColumn(name = "account_id", nullable = false)
private Account account;
}
Since Hibernate CascadeType.DELETE_ORPHAN is deprecated, I'm having to assume that it has been superseded by the JPA2 version, but the implementation is lacking something.

Some remarks:
Since you have a bi-directional association, you need to add a mappedBy attribute to declare the owning side of the association.
Also don't forget that you need to manage both sides of the link when working with bi-directional associations and I suggest to use defensive methods for this (shown below).
And you must implement equals and hashCode on Contact.
So, in Account, modify the mapping like this:
#Entity
public class Account {
#Id #GeneratedValue
public Long id;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true)
public List<Contact> contacts = new ArrayList<Contact>();
public void addToContacts(Contact contact) {
this.contacts.add(contact);
contact.setAccount(this);
}
public void removeFromContacts(Contact contact) {
this.contacts.remove(contact);
contact.setAccount(null);
}
// getters, setters
}
In Contact, the important part is that the #ManyToOne field should have the optional flag set to false:
#Entity
public class Contact {
#Id #GeneratedValue
public Long id;
#ManyToOne(optional = false)
public Account account;
// getters, setters, equals, hashCode
}
With these modifications, the following just works:
Account account = new Account();
Contact contact = new Contact();
account.addToContact(contact);
em.persist(account);
em.flush();
assertNotNull(account.getId());
assertNotNull(account.getContacts().get(0).getId());
assertEquals(1, account.getContacts().size());
account.removeFromContact(contact);
em.merge(account);
em.flush();
assertEquals(0, account.getContacts().size());
And the orphaned Contact gets deleted, as expected. Tested with Hibernate 3.5.3-Final.

Related

How to send only a list of IDs in many-to-many spring boot JPA POST request instead of sending the full object's data

I have 2 DTOs "OrderItem" and "Ingredient", both classes has #ManyToMany annotation:
#Entity
#Table
#NoArgsConstructor
#Data
public class OrderItem {
private #Id #GeneratedValue #NotNull long id;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Order order;
#ManyToOne(optional = false)
#JoinColumn(nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private Food food;
private int quantity;
#ManyToMany(cascade=CascadeType.ALL)
#JoinTable(
name = "order_item_ingredient",
joinColumns = #JoinColumn(name = "order_item_id"),
inverseJoinColumns = #JoinColumn(name = "ingredient_name")
)
private Set<Ingredient> ingredients = new HashSet<>();
}
#Entity
#Table
#Data
#NoArgsConstructor
public class Ingredient {
private #Id String ingredientName;
private float basePrice;
private boolean addable;
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<Food> foods= new HashSet<>();
#ManyToMany(mappedBy = "ingredients",cascade=CascadeType.ALL)
private Set<OrderItem> orderItems= new HashSet<>();
public Ingredient(String ingredientName, float basePrice, boolean addable) {
this.ingredientName = ingredientName.toLowerCase();
this.basePrice = basePrice;
this.addable = addable;
}
}
And I'm looking to add a new OrderItem using a POST request using the following #PostMapping controller function:
#PostMapping("{id}/orderItem")
public ResponseEntity<OrderItem> createMenuItem(
#PathVariable(value = "id") Long orderId,
#RequestBody OrderItem orderItem) {
Order order = orderService.getOrder(orderId)
.orElseThrow(() -> new ResourceNotFoundException("order '" + orderId + "' is not found"));
orderItem.setOrder(order);
orderItemRepository.save(orderItem);
return new ResponseEntity<>(orderItem, HttpStatus.CREATED);
}
When I send a post request to localhost:8080/1/orderItem with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1"
}
It works fine and a new order_item database record is created, but when I send the same request with the following body:
{
"order":"1",
"food":"burger",
"quantity":"1",
"ingredients": [{"ingredientName":"leaf"}]
}
It fails and gives the following SQL error:
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'leaf' for key 'ingredient.PRIMARY'
I know that this record already exists, but how do I tell Spring Boot that I want it to look for an existing Ingredient instead of trying to create a new one?
I have an ugly solution in my mind, and that is to send the OrderItem object alongside a list of strings where each element represents a primary key for Ingredient class, then iterate through that list element by element calling the repository to get the Ingredient object then manually add it to OrderItem.ingredients, but I'm sure that is not the best solution out there.
Being defined on the OrderItem class, the relation ingredients is considered as a composition on the cascading strategy point of view. Therefore, the CascadeType.ALL implies the attempt to create the ingredient.
To avoid this, you can change the direction of this relation reverse the mappedBy information.
But then again, if you keep a CascadeType.ALL on the ingredient side, you will be in trouble if you create an ingredient with an existing orderItem. You can win on both sides an use CascadeType.ALL.
check JPA Hibernate many-to-many cascading

How to correctly save associated entities?

Let's say we have the following three domain model entities: Company, Departament, and Employee.
#Getter #Setter #NoArgsConstrutor
public class Employee {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "department_id", nullable = false, insertable = false, updatable = false)
private Department department;
#JoinColumn(name = "department_id", nullable = false)
private int department_id;
}
#Getter #Setter #NoArgsConstrutor
public class Department {
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "company_id", nullable = false, insertable = false, updatable = false)
private Company company;
#JoinColumn(name = "company_id", nullable = false)
private int company_id;
#OneToMany(mappedBy = "department")
private List<Employee> employees;
}
#Getter #Setter #NoArgsConstrutor
private class Company {
private String name;
#OneToMany(mappedBy = "company")
private List<Department> departments;
}
For each entity, we have Repositories which extend JpaRepository, Services, and Controllers. In each Service we #Autowire the respective Repository, and in each entity Controller we call methods from the entity Service.
My issue is the following: I cannot save an entire Company, because the Departments require a Company ID, and Employees a Deparment ID. So, firstly, in my CompanyService I save and then clear the departments list, do a saveAndFlush which assigns an ID to my company. I assign the received ID to every company_id in each entity of the previously saved departments list, then attach the list back to the company and do another saveAndFlush, and I do this one more time for the employee list.
#RestController
public class CompanyController {
#Autowire
private CompanyService companyService;
#PostMapping("/companies")
public Company createCompany(#RequestBody Company newCompany) {
return companyService.createCompany(newCompany);
}
}
#Service
public class CompanyService {
#Autowire
private CompanyRepository companyRepository;
public Company createCompany(Company company) {
List<Department> departments = new ArrayList<>(company.getDepartments());
company.getDepartments().clear();
companyRepository.saveAndFlush(company);
int company_id = company.getId();
departments.forEach (department ->
department.setCompany_id(company_id);
);
//here I save a copy of the previously saved departments, because I still need the employees
company.getDepartments().addAll(departments.stream().map(department -> department.clone(department)).collect(Collectors.toList()));
company.getDepartments().forEach(department -> department.getEmployees().clear());
companyRepository.saveAndFlush(company);
//here I assign each employee it's corresponding department ID
for (int i = 0; i < company.getDepartments().size(); i++) {
Department departmentInSavedCompany = company.getDepartments().get(i);
Department departmentWhichStillHasEmployees = departments.get(i);
departmentWhichStillHasEmployees.setId(departmentInSavedCompany.getId());
departmentWhichStillHasEmployees.getEmployees().forEach(employee -> employee.setDepartment_id(departmentInSavedCompany.getId()));
}
company.getDepartments.clear();
company.getDepartments.addAll(departments);
return companyRepository.saveAndFlush(company);
}
}
#Repository
public interface CompanyRepository extends JpaRepository<Company, Integer> {
}
I currenty do not like this implementation neither do I find it good. Which is the correct approach for this situation?
When working with JPA, do not work with IDs, work with object references.
In your case, this means removing the id attributes that duplicate the references.
In order to obtain the proper entities for IDs use JpaRepository.getOne. It will return either the entity if it is already in the 1st level cache or a proxy just wrapping the id, so it won't hit the database.
This allows you to assemble your object graph and persist it in one pass starting with the entity having no references to other entities.
You might also consider configuring cascading, if you consider entities to be part of the same Aggregate, i.e. they should be loaded and persisted together.

Does #ElementCollection imply orphanRemoval?

According to this post Difference between #OneToMany and #ElementCollection? I should prefer #ElementCollection for embeddable types and #OneToMany for entities. But using #OneToMany I can additionaly set option orphanRemoval=true. How can I do this with #ElementCollection? It it implied?
It is implied. Removing the owning entity would also remove all data on the #ElementCollection. Setting the Collection to null or changing elements in the Collection would cause an update if Session isn't already closed.
The official documentation here says this:
2.8.1. Collections as a value type
Value and embeddable type collections have a similar behavior as
simple value types because they are automatically persisted when
referenced by a persistent object and automatically deleted when
unreferenced. If a collection is passed from one persistent object to
another, its elements might be moved from one table to another.
...
For collections of value types, JPA 2.0 defines the #ElementCollection
annotation. The lifecycle of the value-type collection is entirely
controlled by its owning entity.
I ran these three tests to test it out:
#Test
public void selectStudentAndSetBooksCollectionToNull() {
Student student = studentDao.getById(3L);
List<String> books = student.getBooks();
books.forEach(System.out::println);
student.setBooks(null);
em.flush(); // delete from student_book where student_id = ?
}
#Test
public void selectStudentAndAddBookInCollection() {
Student student = studentDao.getById(3L);
List<String> books = student.getBooks();
books.add("PHP Book");
books.forEach(System.out::println);
em.flush(); // insert into student_book(student_id, book) values(?, ?)
}
#Test
public void selectStudentAndChangeCollection() {
Student student = studentDao.getById(3L);
List<String> newBooks = new ArrayList<>();
newBooks.add("Rocket Engineering");
newBooks.forEach(System.out::println);
student.setBooks(newBooks);
em.flush(); // delete from student_book where student_id = ?
// insert into student_book(student_id, book) values(?, ?)
}
This is the Student class:
#Entity
#Table(name = "student")
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "student_id", nullable = false, insertable = false, updatable = false)
private Long id;
#Column(name = "name", nullable = false)
private String name;
#ElementCollection
#CollectionTable(
name = "student_books",
joinColumns = #JoinColumn(name = "student_id", referencedColumnName = "student_id"))
#Column(name = "book")
private List<String> books = new ArrayList<>();
// Getters & Setters
}

Spring Data: How to write a record in join table for unidirectional one-to-may?

I have a Subscription class and Payment class. When I do the following, it doesn't create a record in join table. Should I use intermediate class or is it possible to create such record without it? subscriptionRepository is a CrudRepository from Spring-Data.
#Transactional
public Subscription activate(#Valid Subscription subscription, #Valid Payment payment) {
Set<Payment> payments = subscription.getPayments();
if (payments == null)
payments = new HashSet<>();
payments.add(payment);
return subscriptionRepository.save(subscription);
}
Classes:
Subscription:
#Entity
public class Subscription {
...
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(
joinColumns = {#JoinColumn(name = "subscription_id", referencedColumnName = "id")},
inverseJoinColumns = {#JoinColumn(name = "payment_id", referencedColumnName = "id", unique = true)}
)
#Getter #Setter
private Set<Payment> payments;
}
Payment:
#Entity
public class Payment {
#Column
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#JsonIgnore
private Integer id;
#Column(nullable = false)
private PaymentType paymentType;
#Past
#Column(nullable = false)
private Date date;
public enum PaymentType {
MONEY,
PROMO_CODE,
TRIAL
}
}
you forgot to inject the payments in the subcription , your repository and pojo seem just fine
if (payments == null) {
payments = new HashSet<>();
subscription.setPayments(payments);
}
First of all, you need to mark your method with #Transactional annotation, cause the Spring Data save method does not execute explicit save action, it just selects a database row identifier and sets it to your entity.
1) Mark your method as #Transactional (best solution)
2) Inject EntityManager and create a transaction manually.
P.S.: JPA Persistence with Hibernate advises to initialize your collections in your model class (No lazy initialization). It reduces a lot of boilerplate checks and sometimes the realization shows Hibernate which Hibernate built-in collection to use (bags etc)

Hibernate - One to Many Mapping of Set - Annotations

Newbie to Hibernate here. I'm building a simple app to play around with Hibernate and I'm getting the hang of most of the annotations but the mappings are really confusing me.
I have a Person class and I have a Note's class. A person can have many notes associated with them but a single note will only ever correspond to a specific person.
I'm trying to set it up so that the note table has a column called person_id such that I won't need an extra person_note table in the database for the associations.
How would I go about setting up the annotations such that an extra table is not created in the database and I can associate multiple notes to a single person via an extra column in the note's table?
I've tried a few options after searching on Google such as using annotations like this but with no luck:
#JoinColumn(name="id_person", nullable=false)
Person Class
#Entity
#Table(name = "person")
public class Person {
#OneToMany()
private Set<Note> notes;
public Set<Note> getNotes() {
return notes;
}
public void setNotes(Set<Note> notes) {
this.notes = notes;
}
...
}
Note Class
#Entity
#Table (name = "note")
public class Note {
#ManyToOne
private Person person;
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
...
}
Any help would be appreciated. Thank you.
Final Working Solution
For the benefit of anyone looking in the future, I now don't have a separate table for mapping note objects to people objects. The final code (with extra lines removed) now looks like this:
Person Class
#Entity
#Table(name = "person")
public class Person {
#OneToMany(fetch = FetchType.EAGER, mappedBy = "person", cascade = CascadeType.ALL)
private Set<Note> notes;
...
}
Note Class
#Entity
#Table (name = "note")
public class Note {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "id_person", nullable = false)
private Person person;
...
}
Misc Points
I had to add cascade = CascadeType.ALL in the Person class to ensure that when I saved a Person object that all the Note objects inside were saved.
I combined the code from chsdk and Saif. Thank you to both of you.
In your mapping annotations you should map the entities with a mappedBy property in the #OneToMany annotation and specify a joinColumn under the #ManyToOne annotation using the #JoinColumn annotation, Change your code like this:
Person class:
#OneToMany(mappedBy = "person") // "person" here refers to the person property in the notes class
private Set<Note> notes;
Notes class:
#ManyToOne
#JoinColumn(name = "id_person", nullable = false)
private Person person;
Take a look at Hibernate Collection mapping for further information.
Edit Person
#OneToMany(fetch = FetchType.EAGER , mappedBy = "person")
private Set<Note> notes;
And Note
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "person_id", nullable = false)
private Person person;
I agree with you. One can implement what you're asking with only the two tables you already have.
Here is what I would do:
Database wise:
Table Person, with:
PK column: person_id
Table Note, with:
PK column: note_id
column: person_id (nullable)
column: assigned_person_id (nullable with unique index)
Purpose:
person_id - reference to the PK of the Person table. That will define the [Person : Note] relationship, where 1 person can have multiple notes.
assigned_person_id - reference to the person assigned to the note. However to guarantee your requirement that only one person can be assigned to a note - add an unique index by that column. That will guarantee that the same person_id was on used for another note record.
Hibernate wise:
Note that the code snippet is not complete! It is just pointing the important moments.
You can look at one of the many complete examples, for instance:
http://www.mkyong.com/hibernate/hibernate-one-to-many-relationship-example-annotation/
public class Person {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "person")
public setNotes(Set<Note> notes) {
this.notes=notes;
}
...
}
#Table(name = "note", catalog = "schema_name",
uniqueConstraints = #UniqueConstraint(columnNames = "assigned_person_id"))
public class Note {
private Person person;
private Person assignedPerson;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "person_id", nullable = false)
public Person getPerson() {
return person;
}
public Person getAssignedPerson() {
return assignedPerson;
}
...
}

Categories

Resources