Does #ElementCollection imply orphanRemoval? - java

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
}

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

Hibernate: add existing child entity to new entity with OneToMany bidirectional relationship and persist it ('detached entity passed to persist')

In order to understand why I have persisted child entities, here is the mapping.
I have Author (id, name, books) and Book (id, title, authors) entities. Their relationship is ManyToMany since any Author may have more than one Book, and any Book may have more than one Author. Also I have BookClient (id, name, rentDate, books) - relationship with Book entity is OneToMany since any Client may rent zero to many books.
Author.java
#Table
public class Author {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private String name;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "books_authors",
joinColumns = { #JoinColumn(name = "author_id") },
inverseJoinColumns = { #JoinColumn(name = "book_id") }
)
private Set<Book> books = new HashSet<>();
Book.java
#Entity
#Table
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(nullable = false, unique = true)
private Long id;
#Column(nullable = false)
private String title;
#ManyToMany(mappedBy = "books", fetch = FetchType.EAGER)
private Set<Author> authors = new HashSet<>();
#ManyToOne
#JoinColumn(name = "client_id")
private BookClient bookClient;
BookClient.java
#Entity
#Table
public class BookClient {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "client_id")
private Long id;
#Column(nullable = false)
private String name;
#OneToMany(mappedBy = "bookClient", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private Set<Book> books = new HashSet<>();
private LocalDate rentDate;
Some business logic behind the scenes: there're a lot of books written by different authors which are persisted in DB of some, let's say, library. And this library gives books to clients. Any new client may register in the library when he/she takes a book.
Book clients are persisted using Entity Manager:
#Transactional
#Repository("bookClientDao")
public class BookClientDaoImpl implements BookClientDao {
#PersistenceContext
private EntityManager entityManager;
#Override
public void save(BookClient bookClient) {
entityManager.persist(bookClient);
}
#Override
public void saveOrUpdate(BookClient bookClient) {
if(bookClient.getId() == null) {
save(bookClient);
} else {
entityManager.merge(bookClient);
}
}
}
Here is an example of how it may look like in code:
public static void main(String[] args) {
ApplicationContext appContext = new ClassPathXmlApplicationContext("META-INF/context.xml");
AuthorDao authorDao = (AuthorDao) appContext.getBean("authorDao");
BookClientDao bookClientDao = (BookClientDao) appContext.getBean("bookClientDao");
//Create a book and its author
Book book = new Book();
book.setTitle("John Doe Book the 1st");
Author author = new Author();
author.setName("John Doe");
author.getBooks().add(book);
authorDao.save(author);
//Registering new Book Client
BookClient bookClient = new BookClient();
bookClient.setName("First Client");
bookClient.getBooks().add(book);
bookClient.setRentDate(LocalDate.now());
book.setBookClient(bookClient);
//book is expected to be updated by cascade
bookClientDao.saveOrUpdate(bookClient); //'detached entity passed to persist' occurs here
}
After running this code I get detached entity passed to persist exception since Book instance has already been persisted earlier.
Exception in thread "main" javax.persistence.PersistenceException:
org.hibernate.PersistentObjectException: detached entity passed to persist: entity.manager.example.entity.Book
...
Caused by: org.hibernate.PersistentObjectException:
detached entity passed to persist: entity.manager.example.entity.Book
If I persist BookClient berforehand, then connection between BookClient and Book is set correctly since both entities are existed in DB. But it seems to me as some workaround.
Is it possible to create new object, connect already persisted entity to it and persist this object with cascade update of all its children?
In your example, save operation for the author and the bookClient executes in different persistence contexts. So as for the main method, first you should merge books (into saveOrUpdate method) that were detached during saving the author.
Is it possible to create new object, connect already persisted entity to it and persist this object with cascade update of all its children?
It may depend on your application requirements and functionality. In this main() example, it looks like you want to save all these entities transactionally.

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)

How to delete Hibernate #Embeddables containing nullable fields?

When deleting an #Embeddable object, I run into some problems.
I have the following domain classes: SwitchVoipTrunkGroup and PrioritizedCodec. The latter contains several fields that are nullable.
class SwitchVoipTrunkGroup {
//...
#CollectionOfElements(fetch = FetchType.LAZY)
#JoinTable(
name = "SWITCH_VOIP_TKG_CODEC",
joinColumns = #JoinColumn(name = "FK_SWITCH_VOIP_TKG_ID")
)
#ForeignKey(name = "FK_CODEC_SWITCH_VOIP_TKG")
private Set<PrioritizedCodec> prioritizedCodecs = new HashSet<PrioritizedCodec>();
//...
}
#Embeddable
public class PrioritizedCodec {
#Column(name = "PRIORITY")
private String priority;
#Column(name = "FAX_MODE")
private String faxMode;
//... some more columns ...
}
When I edit SwitchVoipTrunkGroup's prioritizedCodecs field (e.g. by deleting an entry) and save the entity, I see the following in my Hibernate logging:
13:54:31,919 INFO [STDOUT] Hibernate: delete from T_SWITCH_VOIP_TKG_CODEC where
fk_switch_voip_tkg_id=? and fax_mode=? and priority=?
From this question I understand why Hibernate uses all the fields in the where clause. However, this gives problems: in case some of these fields are empty, the query will look like so:
delete from T_SWITCH_VOIP_TKG_CODEC where fk_switch_voip_tkg_id=1 and fax_mode = ''
and priority =''
This will however not delete any records, as what is really necessary is for Hibernate to check for NULL iso for an empty string. For example:
delete from T_SWITCH_VOIP_TKG_CODEC where fk_switch_voip_tkg_id=1 and fax_mode
IS NULL and priority IS NULL
(cf. here for more info on why checking for an empty string does not suffice)
Any ideas on how to tackle this? Many thx!
I suggest to normalize your database, so both your classes become entities, and then to setup One-to-Many relation between SwitchVoipTrunkGroup and PrioritizedCodec, then you may setup cascading rules so Hibernate automatically updates collection of elements of PrioritizedCodec type, when you persist instance of SwitchVoipTrungGroup.
#Entity
class SwitchVoipTrunkGroup {
//...
#OneToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST}, orphanRemoval = true)
#JoinColumn(name = "switchVoipTrunkGroup_id")
#ForeignKey(name = "FK_PrioritizedCodec_SwitchVoipTrunkGroup")
private Set<PrioritizedCodec> prioritizedCodecs = new HashSet<PrioritizedCodec>();
//...
}
#Entity
public class PrioritizedCodec {
#Column(name = "PRIORITY")
private String priority;
#Column(name = "FAX_MODE")
private String faxMode;
//... some more columns ...
}
#Serice("someService")
public class SomeService {
#Autowired
private SwitchVoipTrunkGroupDao trunkDao;
public SwitchVoipTrunkGroup doOperation("criteria") {
SwitchVoipTrunkGroup tg = trunkDao.find("criteroa");
tg.getPrioritizedCodecs().[remove(2)]; //remove should be implemened, that is just lame statement
tg.getPrioritizedCodecs().get(5).setFaxMod("ENABLED");
return trunkDao.save(tg); //hibernate will remove missing elements from PrioritizedCodec table, and will update necessary entities.
}
}
Alternatively, you may specify default values for priority and faxMode fields via attributes of #Column annotation and enforce nullable constraints
#Column(columnDefinition = "VARCHAR(20) default 'NONE'", nullable = false)
private String faxMode;

Hibernate triggering constraint violations using orphanRemoval

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.

Categories

Resources