Spring Data JPA + Hibernate save children entities without finding parent firstly - java

In my opinion, if you want to save children entities, you must find their parent firstly. But it's not true. Following is an example.
Person table
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
private List<Book> books;
}
Book table
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "person_id", nullable = false)
private Person person;
}
BookRepository
public interface BookRepository extends CrudRepository<Book, Long> {}
persist process
// the person(id=1) is in the database.
Person p = new Person();
p.setId(1);
Book book = new Book();
book.setPerson(p);
book.setName("book");
bookRepository.save(book); // no error throws
I want to know why the last statement is success. The person instance is created manually and is not retrieved from database by Spring Data JPA, I think it means the state of person instance is transient.

Based on your logic Book and person are new then you have to create those objects. Why you think it should come from database. If you want retrieve the user by findbyId then set the newly created book in List and save the user object behind the scenes it will do the updation in the database for that user.

You can use personRepository.getOne(1) which will return a proxy for a person with id 1. Setting this object then as person on the book will do what you are looking for.

Related

Java entity modeling with table that relates to several others

I have a doubt about how the modeling of my entity would be. Come on, I have a table in the database that serves to save documents from my system, this table has the columns id, fk_id (element foreign key), fk_table (entity name) and file_name (stores the name of my file) .
I did a lot of research before posting my question here, but I didn't find anything related to it, what would my entities, user, patient and doctor?
DB:
id
fk_id
fk_table
file_name
1
21
user
test1.jpg
2
32
doctor
test2.pdf
3
61
user
test10.pdf
4
100
patient
test5.jpg
Class:
public class User{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String LastName;
// What would a one-to-many relationship look like?
}
public class patient{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
// What would a one-to-many relationship look like?
}
You can use #Where. But be aware that #Where is a Hibernate annotation. It's not in the JPA standard.
For example in the User entity: (I assume that your table is mapped to an entity called Document)
#Where( clause = "fk_table = 'user'")
#JoinColumn(name = "fk_id")
#OneToMany
private List<Document> documents = new ArrayList<>( );
The following is based only on standard JPA annotations. The idea is to create an inheritance hierarchy for the documents table. The base is:
#Entity
#Table(name = "XX_DOCUMENT")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "fk_table")
public abstract class BaseDocument {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#Column(name = "file_name")
private String fileName;
}
Here we define that all entities extending this will go to the same table, with the fk_table column to discriminate. The entities extending it are defined as follows:
#Entity
#DiscriminatorValue("doctor")
public class DoctorDocument extends BaseDocument {
#ManyToOne
#JoinColumn(name = "fk_id")
private Doctor doctor;
}
#Entity
#DiscriminatorValue("patient")
public class PatientDocument extends BaseDocument {
#ManyToOne
#JoinColumn(name = "fk_id")
private Patient patient;
}
// and so on
The interesting thing is that we are reusing the column fk_id to point to the right table. From a small experiment, Hibernate seems to not have problems with it. I would suggest that you manage the DB creation another way just to be safe.
The Doctor, Patient etc need not have a common base class, e.g.:
#Entity
#Table(name = "XX_DOCTOR")
public class Doctor {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#OneToMany(mappedBy = "doctor")
private Collection<DoctorDocument> documents = new ArrayList<>();
// any doctor-specific fields
}
#Entity
#Table(name = "XX_PATIENT")
public class Patient {
#Id
#GeneratedValue(strategy=SEQUENCE)
private Long id;
#OneToMany(mappedBy = "patient")
private Collection<PatientDocument> documents = new ArrayList<>();
// any patient-specific fields
}
// and so on
You can read a (doctor, patient, ...)'s documents from the relevant collection. You can even query BaseDocument instances based on any criteria.
You can even go ahead and do more fabcy stuff with the Java code. E.g. define an interface HasDocuments:
public interface HasDocuments<D extends BaseDocument> {
Collection<D> getDocuments();
}
Doctor, Patient, ..., implements this, so they can all be treated the same way.

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.

Hibernate clone entity without lazy fields

I have two entities:
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "age")
private int age;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "person_id")
private Person person;
and
#Entity
#Table(name = "persons")
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "number")
private String number;
The person is LAZY. I load one user and detach it.
#Transactional
#Override
public void run(String... args) {
User user = userService.getOne(1L);
userService.detach(user);
System.out.println(user.getName());
System.out.println(user.getAge());
Person person = user.getPerson();
System.out.println(person.getName());
System.out.println(person.getNumber());
}
But when I call user.getPerson() - it does not throw exceptions. I expect exception because I detach entity and try to call LAZY field but it still works.
I want to create a clone of the user, without person and save as a new entity.
User user = userService.getOne(1L);
userService.detach(user);
user.setId(null)//autogenerate id
but when I save user, person clone too. I can set null:
User user = userService.getOne(1L);
userService.detach(user);
user.setId(null);
user.setPerson(null);
But person lazy and it looks like a hack. And what's the point then detach method...
EDIT:
Very interesting thing - If I start example application in debugging with breakpoints - all work fine, but if I deselect all breakpoints I get exception in the console:
Caused by: org.hibernate.LazyInitializationException: could not initialize proxy [com.example.detachexample.User#1] - no Session
If I understand it, you are calling detach on a clone? Well that clone is not of a plain User object, but of the proxy that extends the User object.
You need to get the raw loaded entity first using unproxy.
User olduser = userService.getOne(1L);
User user = org.hibernate.Hibernate.unproxy(olduser);
if (olduser == user) userService.detach(user);
user.setId(null)//autogenerate id
user.getPerson().setId(null); // so you will generate this as well
user.getPerson().setUser(user); // so that it will point to the correct new entity
It seems that at the point of detach the Person has been loaded actually.
This is possible according to FetchType documentation:
The LAZY strategy is a hint to the persistence provider runtime that
data should be fetched lazily when it is first accessed. The
implementation is permitted to eagerly fetch data for which the LAZY
strategy hint has been specified.
So take a look at the Hibernate debug logs and most likely there will be a join to the Person somewhere along with the select of its fields.

object references an unsaved transient instance - save the transient instance before flushing : Spring Data JPA

I have Below 3 models :
Model 1: Reservation
#Entity
public class Reservation {
public static final long NOT_FOUND = -1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
#OneToMany(mappedBy = "reservation", cascade = CascadeType.ALL, orphanRemoval = true)
public List<RoomReservation> roomReservations = new ArrayList<>();
}
Model 2: Room Reservation:
public class RoomReservation extends{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "RESERVATION_ID")
public Reservation reservation;
#OneToMany(mappedBy = "roomReservation", cascade = CascadeType.ALL, orphanRemoval = true)
public List<GuestDetails> guestDetails = new ArrayList<>();
}
Model 3 : Guest Details:
public class GuestDetails {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
public Long guestId;
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ROOM_RESERVATION_ID")
public RoomReservation roomReservation;
public Boolean isPrimary;
#Transient
public Guest guest;
}
The Relationship between those three are as :
Reservation --One to Many on RESERVATION_ID--> Room Reservation --One to Many on ROOM_RESERVATION_ID--> Guest Details
I am getting the reservation object and trying to update guest details i get the following error:
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.model.GuestDetails.roomReservation -> com.model.RoomReservation
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1760)
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1677)
at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:82)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:517)
... 73 common frames omitted
I have changed cascadeType to ALL as suggested in common question still getting the same error.Please donot make it duplicate as i have tried all the solution realated to this kind of question already asked
Please Let me know what mistake i am doing. Thanks
Code to save Reservation Object by changing GuestDetails:
Reservation existingReservation = reservationRepository.findOne(reservationId);
Reservation reservation = reservationParser.createFromJson(reservationNode);
existingReservation.roomReservations.forEach(roomReservation -> {
RoomReservation updatedRoomReservation = reservation.roomReservations.stream().filter(newRoomReservation -> Objects.equals(roomReservation.id, newRoomReservation.savedReservationId)).findFirst().orElse(null);
if(updatedRoomReservation != null){
roomReservation.guestDetails = updatedRoomReservation.guestDetails;
}
});
reservationRepository.save(existingReservation);
... save the transient instance before flushing :
com.model.GuestDetails.roomReservation -> com.model.RoomReservation
This exception states clearly that RoomReservation contained in GuestDetails, does not exist in the database (and most likely it's id is null).
In general, you can solve this exception either by :
Saving RoomReservation entity before saving GuestDetails
Or making cascade = CascadeType.ALL (or at least {CascadeType.MERGE, CascadeType.PERSIST}) for #ManyToOne GuestDetail-->RoomReservation
But first, I have a couple of points to cover:
Do not use public fields in your class, this violates the encapsulation concept.
While you have a bidirectional association, you can set the other side of the association in your Setter methods.
For your case, you should change RoomReservation class :
public class RoomReservation{
//..... other lines of code
#OneToMany(mappedBy = "roomReservation", cascade = CascadeType.ALL, orphanRemoval = true)
private List<GuestDetails> guestDetails = new ArrayList<>();
public void setGuestDetails(List<GuestDetails> guestDetails) {
this.guestDetails.clear();
// Assuming that by passing null or empty arrays, means that you want to delete
// all GuestDetails from this RoomReservation entity
if (guestDetails == null || guestDetails.isEmpty()){
return;
}
guestDetails.forEach(g -> g.setRoomReservation(this));
this.guestDetails.addAll(guestDetails);
}
public List<GuestDetails> getGuestDetails() {
// Expose immutable collection to outside world
return Collections.unmodifiableList(guestDetails);
}
// You may add more methods to add/remove from [guestDetails] collection
}
Saving the Reservation:
Reservation existingReservation = reservationRepository.findOne(reservationId);
Reservation reservation = reservationParser.createFromJson(reservationNode);
existingReservation.roomReservations.forEach(roomReservation -> {
Optional<RoomReservation> updatedRoomReservation = reservation.roomReservations.stream().filter(newRoomReservation -> Objects.equals(roomReservation.id, newRoomReservation.savedReservationId)).findFirst();
if(updatedRoomReservation.isPresent()){
// roomReservation already exists in the database, so we don't need to save it or use `Cascade` property
roomReservation.setGuestDetails( updatedRoomReservation.get().getGuestDetails());
}
});
reservationRepository.save(existingReservation);
Hope it helps!
This can be caused by incorrect transaction semantics.
If the referenced instance was not fetched in the current transaction it counts as transient.
The easiest solution is to add #Transactional to the method:
#Transactional
public void saveReservation(...) {
Reservation existingReservation = reservationRepository.findOne(reservationId);
Reservation reservation = reservationParser.createFromJson(reservationNode);
// ...
reservationRepository.save(existingReservation);
}
GuestDetails - add the needed CasadeType:
#ManyToOne(fetch = FetchType.LAZY, cascade=CascadeType.ALL)
#JoinColumn(name = "ROOM_RESERVATION_ID")
public RoomReservation roomReservation;
RoomReservation - add the nedded CascadeType:
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY, cascade=CascadeType.AL)
#JoinColumn(name = "RESERVATION_ID")
public Reservation reservation;
And then you need to persist the data before/after using the for-each loop. Depends on you safe()-Method.
Reservation reservation = reservationParser.createFromJson(reservationNode);
entityManager.persist(reservation);
And then safe it afterwards. Tell me your result. Maybe directly working without changing/adding the cascadetypes.
You can save the reservation you get from the Json.
JPA will update the rows with the same id's.
The error you get is because the guestDetails has still a reference to the updatedRoomReservation.
If you don't want to save the whole reservation from the json you have to set the right RoomReservation.
e.g.:
if(updatedRoomReservation != null){
roomReservation.guestDetails = updatedRoomReservation.guestDetails;
guestDetails.forEach(guestDetail -> guestDetail.roomReservation = roomReservation);
}
If you are using JPA 2.0 then defaults fetch type for OneToMany is LAZY. If after your lambda, your updatedRoomReservation is null (as you set in orElse) then existingReservation.roomReservation.guestDetails will never be loaded and will be null.
Therefore when you save existingReservation, you get the error.

JPA - Entity is already persistent - using GAE

This question is similar to this one but the person asking never confirmed if it worked.
entityManager.persist(user) -> javax.persistence.EntityExistsException: User#b3089 is already persistent
Scenario
ProductCategory has a OneToMany relationship with Account which inversely has a ManyToOne relationship wuth ProductCategory. When ProductCategory is inserted, Accounts are not available. So ProductCategory is inserted without accounts. Later when accounts are available, I would like to insert accounts in the accounts table and also update ProductCategory with Accounts. The issue is with updating accounts in ProducCategory. When I use mgr.persist for productCategory, I get a error Entity already is Persistent!. When I do not use persist (as per suggestion the link, provider(datanucleus) will take care of writing it to the database at commit time), it does not update. The entities and methods are as follows:
#Entity
public class ProductCategory {
#Id
#Column(name = "CAT_ID", allowsNull="false")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key catId;
#Column(name = "CAT_SHORT_NAME", length=30)
private String catShortName;
//other fields
#OneToMany(mappedBy="productCategory",targetEntity=Account.class,
fetch=FetchType.EAGER, cascade=CascadeType.ALL)
private ArrayList<Account> accounts;
//getters & setters
#Entity
public class Account {
#Id
#Column(name = "ACCT_NBR_KEY", allowsNull="false")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Key acctNbrKey;
#Column(name = "CAT_ID")
private Key acctCatId;
//other fields
#ManyToOne(optional=false, fetch=FetchType.EAGER, cascade=CascadeType.ALL)
#JoinColumn(name="CAT_ID", insertable=false, updatable=false)
private ProductCategory productCategory;
//getters & setters
AccountEndpoint.java
public void insertAccountBulk() {
log.info("AccountEndpoint.insertAccountBulk....");
Account account = new Account();
ProductCategory pc = (new ProductCategoryEndpoint()).getProductCategoryByShortName("Savings");
account.setProductCategory(pc);
account.setAcctCatId(pc.getCatId());
//setting other fields
//updationg accounts in product category
getEntityManager().detach(pc);
if(pc.getAccounts() == null){
ArrayList<Account> accts = new ArrayList<Account>();
accts.add(account);
pc.setAccounts(accts);
}
else{
pc.getAccounts().add(account);
}
getEntityManager().merge(pc);
**//new ProductCategoryEndpoint().updateProductCategory(pc);**
ProductCategoryEndpoint.java
#ApiMethod(name = "updateProductCategory")
public ProductCategory updateProductCategory(ProductCategory productcategory) {
EntityManager mgr = getEntityManager();
try {
if (!containsProductCategory(productcategory)) {
throw new EntityNotFoundException("Object does not exist");
}
mgr.persist(productcategory);
} finally {
mgr.close();
}
return productcategory;
}
**If I uncomment new `ProductCategoryEndpoint().updateProductCategory(pc)` I get the error Entity already persistent.
If I keep it commented, the account is not updated in ProductCategory**
Judging by your other question you're using GAE/Datastore NOT RDBMS, and so are not using Hibernate - please update your question and remove that tag.
About the problem, if the entity is already persistent then you are trying to persist an object with a particular Key when an object with that Key already exists (i.e you passed a TRANSIENT object to persist. You should pass a DETACHED object to that method. The log would tell you what state objects are in
Try changing the #JoinColumn annotation to allow inserts and updates of the ProductCategory. I think it is preventing the Categories from being associated with the Account.
#ManyToOne(optional=false, fetch=FetchType.EAGER, cascade=CascadeType.ALL)
#JoinColumn(name="CAT_ID")
private ProductCategory productCategory;

Categories

Resources