Question about the generated Hibernate Queries in 3 situation - spring

I want to learn relations among tables and i met a question about data persistance.
I created 2 entities, User and Dog. Suppose it's a single-direction many-to-one relation, one user can have many dogs, one dog can only have one user.
In DogEntity Class:
#ManyToOne
#JoinColumn(name = "user_id")
private UserEntity user;
And no private field in UserEntity class.
When I test like below:
#Test
public void saveManyFirst(){
UserEntity user1 = new UserEntity("user2#qq.com", "123qwe",
"zhangsan2",null,
null,null,
null);
DogEntity dog1 = new DogEntity("Sakura","Akita",null,user1);
DogEntity dog2 = new DogEntity("Sakura2","Akita2",null,user1);
dogDao.save(dog1);
dogDao.save(dog2);
userDao.save(user1);
}
It's reasonable to get an error as "object references an unsaved transient instance - save the transient instance before flushing".
Then I added #Transactional as below:
#Test
#Transactional
public void saveManyFirst(){
UserEntity user1 = new UserEntity("user2#qq.com", "123qwe",
"zhangsan2",null,
null,null,
null);
DogEntity dog1 = new DogEntity("Sakura","Akita",null,user1);
DogEntity dog2 = new DogEntity("Sakura2","Akita2",null,user1);
dogDao.save(dog1);
dogDao.save(dog2);
userDao.save(user1);
}
I got three Inserts, and table of dog,table of user.
The FKs in table of Dog are null.
Then I added #Rollback(false) (the defalut value is true).
#Test
#Transactional
#Rollback(false)
public void saveManyFirst(){
UserEntity user1 = new UserEntity("user2#qq.com", "123qwe",
"zhangsan2",null,
null,null,
null);
DogEntity dog1 = new DogEntity("Sakura","Akita",null,user1);
DogEntity dog2 = new DogEntity("Sakura2","Akita2",null,user1);
dogDao.save(dog1);
dogDao.save(dog2);
userDao.save(user1);
}
I got 3 Inserts and 2 more updates, and the FKs in table of Dog are updated as wished.
table of dog
Of course I know I should better save the one who has the FK first, but I'm learning Spring Data Jpa, I really want to know why this happend. Could you please help me? Thanks in advance.

When you have one transaction spanning over all operations, because at the end of the trasaction, it sees that Dog references a User which is already what JPA calls managed.
When you have no transaction, Spring-Data JPA create a transaction per call i.e. each call to save. So you have 3 transactions. Now the first transaction when it is about to commit tries to flush and sees that the dog does not have a managed User reference but a transient one, which is why you get the error.
Note that a flush can be triggered when loading/querying as well, so it's important that you either use proper cascades on your associations or make sure the flushing happens in the appropriate order.

Related

Transaction getting rolled back on persisting the entity from Many to one side

I have this association in the DB -
I want the data to be persisted in the tables like this -
The corresponding JPA entities have been modeled this way (omitted getters/setters for simplicity) -
STUDENT Entity -
#Entity
#Table(name = "student")
public class Student {
#Id
#SequenceGenerator(name = "student_pk_generator", sequenceName =
"student_pk_sequence", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator =
"student_pk_generator")
#Column(name = "student_id", nullable = false)
private Long studentId;
#Column(name = "name", nullable = false)
private String studentName;
#OneToMany(mappedBy = "student", cascade = CascadeType.ALL)
private Set<StudentSubscription> studentSubscription;
}
STUDENT_SUBSCRIPTION Entity -
#Entity
#Table(name = "student_subscription")
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class StudentSubscription {
#Id
private Long studentId;
#ManyToOne(optional = false)
#JoinColumn(name = "student_id", referencedColumnName = "student_id")
#MapsId
private Student student;
#Column(name = "valid_from")
private Date validFrom;
#Column(name = "valid_to")
private Date validTo;
}
LIBRARY_SUBSCRIPTION Entity -
#Entity
#Table(name = "library_subscription",
uniqueConstraints = {#UniqueConstraint(columnNames = {"library_code"})})
#PrimaryKeyJoinColumn(name = "student_id")
public class LibrarySubscription extends StudentSubscription {
#Column(name = "library_code", nullable = false)
private String libraryCode;
#PrePersist
private void generateLibraryCode() {
this.libraryCode = // some logic to generate unique libraryCode
}
}
COURSE_SUBSCRIPTION Entity -
#Entity
#Table(name = "course_subscription",
uniqueConstraints = {#UniqueConstraint(columnNames = {"course_code"})})
#PrimaryKeyJoinColumn(name = "student_id")
public class CourseSubscription extends StudentSubscription {
#Column(name = "course_code", nullable = false)
private String courseCode;
#PrePersist
private void generateCourseCode() {
this.courseCode = // some logic to generate unique courseCode
}
}
Now, there is a Student entity already persisted with the id let's say - 100.
Now I want to persist this student's library subscription. For this I have created a simple test using Spring DATA JPA repositories -
#Test
public void testLibrarySubscriptionPersist() {
Student student = studentRepository.findById(100L).get();
StudentSubscription librarySubscription = new LibrarySubscription();
librarySubscription.setValidFrom(//some date);
librarySubscription.setValidTo(//some date);
librarySubscription.setStudent(student);
studentSubscriptionRepository.save(librarySubscription);
}
On running this test I am getting the exception -
org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.springboot.data.jpa.entity.Student; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.springboot.data.jpa.entity.Student
To fix this I attach a #Transactional to the test. This fixed the above exception for detached entity, but the entity StudentSubscription and LibrarySubscription are not getting persisted to the DB. In fact the transaction is getting rolled back.
Getting this exception in the logs -
INFO 3515 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext#35390ee3 testClass = SpringDataJpaApplicationTests, testInstance = com.springboot.data.jpa.SpringDataJpaApplicationTests#48a12036, testMethod = testLibrarySubscriptionPersist#SpringDataJpaApplicationTests, testException = [null], mergedContextConfiguration = [MergedContextConfiguration#5e01a982 testClass = SpringDataJpaApplicationTests, locations = '{}', classes = '{class com.springboot.data.jpa.SpringDataJpaApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer#18ece7f4, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer#264f218, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer#0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer#2462cb01, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer#928763c, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer#0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer#7c3fdb62, org.springframework.boot.test.context.SpringBootTestArgs#1, org.springframework.boot.test.context.SpringBootTestWebEnvironment#1ad282e0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]
Now I have couple of questions -
Why am I getting detached entity exception. When we fetch an entity from the DB, Spring Data JPA must be using entityManager to fetch the entity. The fetched entity gets automatically attached to the persistence context right ?
On attaching #Transactional on the test, why the transaction is getting rolledback, and no entity is getting persisted. I was expecting the two entities - StudentSubscription and LibrarySubscription should've been persisted using the joined table inheritance approach.
I tried many things but no luck. Seeking help from, JPA and Spring DATA experts :-)
Thanks in advance.
Let me add a few details that outline a couple of design problems with your code that significantly complicate the picture. In general, when working with Spring Data, you cannot simply look at your tables, create cookie-cutter entities and repositories for those and expect things to simply work. You need to at least spend a bit of time to understand the Domain-Driven Design building blocks entity, aggregate and repository.
Repositories manage aggregates
In your case, Student treats StudentSubscriptions like an entity (full object reference, cascading persistence operations) but at the same time a repository to persist the …Subscriptions exists. This fundamentally breaks the responsibility of keeping consistency of the Student aggregate, as you can simply remove a …Subscription from the store via the repository without the aggregate having a chance to intervene. Assuming the …Subscriptions are aggregates themselves, and you'd like to keep the dependency in that direction, those must only be referred to via identifiers, not via full object representations.
The arrangement also adds cognitive load, as there are now two ways to add a subscription:
Create a …Subscription instance, assign the Student, persist the subscription via the repository.
Load a Student, create a …Subscription, add that to the student, persist the Student via it's repository.
While that's already a smell, the bidirectional relationship between the …Subscription and Student imposes the need to manually manage those in code. Also, the relationships establish a dependency cycle between the concepts, which makes the entire arrangement hard to change. You already see that you have accumulated a lot of (mapping) complexity for a rather simple example.
What would better alternatives look like?
Option 1 (less likely): Students and …Subscriptions are "one"
If you'd like to keep the concepts close together and there's no need to query the subscriptions on their own, you could just avoid those being aggregates and remove the repository for them. That would allow you to remove the back-reference from …Subscription to Student and leave you with only one way of adding subscriptions: load the Student, add a …Subscription instance, save the Student, done. This also gives the Student aggregate its core responsibility back: enforcing invariants on its state (the set of …Subscription having to follow some rules, e.g. at least one selected etc.)
Option 2 (more likely): Students and …Subscriptions are separate aggregates (potentially from separate logical modules)
In this case, I'd remove the …Subscriptions from the Student entirely. If you need to find a Students …Subscriptions, you can add a query to the …SubscriptionRepository (e.g. List<…Subscription> findByStudentId(…)). As a side effect of this you remove the cycle and Student does not (have to) know anything about …Subscriptions anymore, which simplifies the mapping. No wrestling with eager/lazy loading etc. In case any cross-aggregate rules apply, those would be applied in an application service fronting the SubscriptionRepository.
Heuristics summarized
Clear distinction between what's an aggregate and what not (the former get a corresponding repository, the later don't)
Only refer to aggregates via their identifiers.
Avoid bidirectional relationships. Usually, one side of the relationship can be replaced with a query method on a repository.
Try to model dependencies from higher-level concepts to lower level ones (Students with Subscriptionss probably make sense, a …Subscription without a Student most likely doesn't. Thus, the latter is the better relationship to model and solely work with.)
The transaction is getting rolled back because the test is doing DB updates in the test method.
#Transactional does auto rollback if the transaction includes any update DB. Also here is the compulsion to use transaction because EntityManager gets closed as soon as the Student entity gets retrieved, so to keep that open the test has to be within the transactional context.
Probably if I had used a testDB for my testcases then probably spring wouldn't haveve been rolling back this update.
Will setup an H2 testDb and perform the same operation there and will post the outcome.
Thanks for the quick help guys. :-)
Why am I getting detached entity exception. When we fetch an entity from the DB, Spring Data JPA must be using entityManager to fetch the entity. The fetched entity gets automatically attached to the persistent context right ?
Right, but only for as long as the entityManager stays open. Without the transactional, as soon as you return from studentRepository.findById(100L).get();, the entityManager gets closed and the object becomes detached.
When you call the save, a new entityManager gets created that doesn't contain a reference to the previous object. And so you have the error.
The #Trannsaction makes the entity manager stay open for the duration of the method.
At least, that's what I think it's happening.
On attaching #Transactional on the test, why the transaction is getting rolledback,
With bi-directional associations, you need to make sure that the association is updated on both sides. The code should look like:
#Test
#Transactional
public void testLibrarySubscriptionPersist() {
Student student = studentRepository.findById(100L).get();
StudentSubscription librarySubscription = new LibrarySubscription();
librarySubscription.setValidFrom(//some date);
librarySubscription.setValidTo(//some date);
// Update both sides:
librarySubscription.setStudent(student);
student.getStudentSubscription().add(librarySubscription);
// Because of the cascade, saving student should also save librarySubscription.
// Maybe it's not necessary because student is managed
// and the db will be updated anyway at the end
// of the transaction.
studentSubscriptionRepository.save(student);
}
In this case, you could also use EntityManager#getReference:
#Test
#Transactional
public void testLibrarySubscriptionPersist() {
EntityManager em = ...
StudentSubscription librarySubscription = new LibrarySubscription();
librarySubscription.setValidFrom(//some date);
librarySubscription.setValidTo(//some date);
// Doesn't actually load the student
Student student = em.getReference(Student.class, 100L);
librarySubscription.setStudent(student);
studentSubscriptionRepository.save(librarySubscription);
}
I think any of these solutions should fix the issue. Hard to say without the whole stacktrace.

How to delete a relationship of a neo4j node in spring data

I have User nodes and having contact relationships with each other. user1 has two contacts user2 and user3. Now I want to delete user2 from contacts. Following is a code snippet about contact relationships.
#Node
public class User{
...Id and different properties
#Relationship(type = "CONTACT")
public Set<User> contacts = new HashSet<>();
}
Now when I delete a relationship and save back the node it shows the following message:
WARN 38461 --- [nio-8080-exec-2] o.s.d.n.c.m.DefaultNeo4jIsNewStrategy : Instances of class com.talkkia.api.entity.User with an assigned id will always be treated as new without version property!
Code for deleting relationship is here:
#Transactional
#Override
public String deleteContact(String mobile1, String mobile2) {
Optional<User> user1 = userRepository.findOneByMobile(mobile1);
Optional<User> user2 = userRepository.findOneByMobile(mobile2);
if(user1.get().getContacts().contains(user2.get())){
user1.get().getContacts().remove(user2.get());
System.out.println(user1.get().getContacts());
userRepository.save(user1.get());
return user2.get().getName() + " has been deleted from your contact.";
}
return user2.get().getName() + " can't be deleted from contact.";
}
The warning message is exactly what leads into the situation you are faced with.
Because you are using an assigned, manual provided id, Spring Data Neo4j cannot differentiate if the object is new or not.
The solution is to provide a #Version field in the entity (like #Version Long version).
From this SDN can derive the information if the element comes from the database or was newly created.
As a consequence of the wrongly assumed new state of the entity, no relationships will be deleted because it is not necessary from the persistence logic.
#Query("MATCH(user1:User {mobile: $mobile1})-[c:CONTACT]-(user2:User {mobile:$mobile2}) DETACH DELETE c")
public String deleteContact(String mobile1, String mobile2);
Have found the above solution very easy and good in performance I think. #meistermeier point is also valid and considerable.

Implementing orphanRemoval in #OneToMany

So I'm trying to remove the the child in a onetomany relationship but I'm not sure if this is the right way to do. I was reading up how to do it online but many talked about entitymanager, cacasded, using queries etc. I'm unsure on which way to do it, usually I use crudrepository and simply do .save and .deleteById etc.
Here's what I have so far
#Entity
public class User
#OneToMany(orphanRemoval = true, cascade = CascadeType.PERSIST)
private List<Payment>payment = new ArrayList<Payment>();
getters/setters
#Service
public class UserService {
public void addPayment(User user, Payment payment) {
user.getPayment().add(payment);
}
public void removePayment(User user, Payment payment) {
user.getPayment().remove(payment);
}
Do I have to mess with the cascade type or entitymanager here?
Assuming the user method parameter is an managed Entity already associated with the session, then yes, that's the correct way to do it.
When the session gets flushed, Hibernate will delete the Payment instance which was removed from the users payments.
You can try CascadeType.ALL. It work in my case.

Spring data JPA - Hibernate - TransientObjectException when updating an existing entity with transient nested children

Following my first question here, the question has changed so I'm creating a new one : org.hibernate.TransientObjectException persisting nested children with CascadeType.ALL
I found that my problem was not saving a new entity but updating an existing one.
Let's start from the beginning.
I have a class called Human which has a list of dogs :
#Entity
public class Human {
#Id
#GeneratedValue
private Long id;
#OneToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL}, orphanRemoval = true)
private Set<Dog> dogs = new HashSet<>(List.of(new Dog()));
...
}
The dog class Dog has a list of puppies :
#Entity
public class Dog {
#Id
#GeneratedValue
private Long id;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL}, orphanRemoval = true)
private Set<Puppy> puppies = new HashSet<>(List.of(new Puppy()));
}
#Entity
public class Puppy {
#Id
#GeneratedValue
private Long id;
}
I'm trying to get an existing human that has a dog and the dog has a puppy, if I try to give him a new Dog with another set of puppies :
Human human = humanRepository.findById(id); // This human already had a dog and the dog has puppies
Set<Dog> dogs = new HashSet<>();
Dog dog = new Dog();
dog.setPuppies(new HashSet<>(List.of(new Puppy())));
dogs.add(dog);
human.setDogs(dogs);
humanRepository.save(human);
I get the following error :
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.test.Puppy
In my understanding cascade = {CascadeType.ALL} should persist the children automatically when saving them with a CrudRepository.
EDIT:
The problem comes from the creation of a new dog with puppies when I'm updating an existing entity.
Here are the working examples I tried :
Human human = new Human();
Dog dog = new Dog();
Puppy puppy = new Puppy();
dog.getPuppies().clear();
dog.getPuppies().add(puppy);
human.getDogs().clear();
human.getDogs().add(dog);
humanRepository.save(human);
Human human = humanRepository.findById(id);
human.getDogs().clear();
human.getDogs().add(new Dog());
humanRepository.save(human);
But the following one doesn't work whether the human I retrieve already has dogs or not :
Human human = humanRepository.findById(id);
Dog dog = new Dog();
Puppy puppy = new Puppy();
dog.getPuppies().clear();
dog.getPuppies().add(puppy);
human.getDogs().clear();
human.getDogs().add(dog);
humanRepository.save(human);
Apparently, persisting a transient Human will cascade persist to the children and the children of the children.
Updating an existing Human will cascade persist to the children but not the children of the children and thus cause the TransientObjectException.
Is this expected behaviour? Am I supposed to use a separate repository to persist the dogs and puppies?
Calling save on a JPA repository will either call persist if the entity is transient or merge if the entity is detached.
If it calls persist, the persist will cascade and save all the children entities but not update existing ones.
If it calls merge, the merge operation will cascade and merge all the children that have an Id, but it will not persist the ones that don't have an Id.
The Hibernate specific saveOrUpdate method seems to do this job. If anyone know of any other method available through JPA, please let me know.
EDIT
I've actually managed to use spring repositories to save my entity. However I need to persist manually every new child and grandchild. For this I have written a method that uses the reflection API!
void saveNewEntites(Object entity, EntityManager em) throws IllegalAccessException {
Class<?> clazz = entity.getClass();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(OneToMany.class)) {
for(Object child : (Collection<?>)field.get(entity)){
for(Field childField : child.getClass().getDeclaredFields()){
childField.setAccessible(true);
if(childField.isAnnotationPresent(Id.class) && childField.get(child) == null){
em.persist(child);
break;
}
}
saveNewEntites(child, em);
}
}
}
}
So this is how my update method looks:
#RequestMapping(method = PATCH, path = "/{id}")
#ApiResponse(responseCode = "204", description = "Entity updated")
#ApiResponse(responseCode = "400", description = "Data is invalid for update")
#Transactional
public ResponseEntity<?> update(#PathVariable Long id, #RequestBody #Valid ResourceDTO resource) {
ResourceEntity entity = repository.findById(id).orElseThrow(ResourceNotFoundException::new);
copyProperties(resource, entity);
try {
saveNewEntites(entity, em);
repository.save(entity);
} catch(Exception e){
e.printStackTrace();
throw new InvalidCommandException("Data is invalid for update.");
}
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
The copyProperties method also uses reflection to copy all the properties. It uses the clear and addAll method on OneToMany relations.

Spring Boot: H2 Not Actually Retrieving From Database

I'm trying to write a simple Repository test in Spring Boot. The Test code looks like this:
public class UserRepositoryTest {
private final TestEntityManager entityManager;
private final UserRepository userRepository;
#Autowired
public UserRepositoryTest(TestEntityManager entityManager, UserRepository userRepository) {
this.entityManager = entityManager;
this.userRepository = userRepository;
}
#Test
public void test() {
String firstName = "Frank";
String lastName = "Sample";
String email = "frank#example.com";
String username = "frank#example.com";
String password = "floople";
String passwordConfirm = "floople";
RegisterUserRequest registerUserRequest = new RegisterUserRequest(firstName, lastName, email, username, password, passwordConfirm);
User user = new User(registerUserRequest);
user.setSpinsRemaining(0);
userRepository.save(user);
userRepository.setSpinsRemainingToTen();
User found = userRepository.findByUsername(username);
assertThat(found.getSpinsRemaining()).isEqualTo(10);
}
What's I expect to happen is that the new User object is persisted to the database, the row in the database is modified to set spinsRemaining to 10, and then the now-modified row is retrieved from H2 and shoved into a new variable named "found". The "found" variable will point to an instance of a User object with ten spins remaining.
What actually happens is that the "found" variable points to the exact same instance of User that the "user" variable is. In fact, if I modify some property of the "user" variable AFTER persisting it to H2, the resultant "found" object also has the modified property. According to IntelliJ, both "user" and "found" are pointing to the same thing. How is that possible?
Hibernate caches entities inside a transaction in memory ("first level cache"). - Every time it retrieves an entity from database (or when it's asked to do so by the entity id) it will first look for it in cache so you don't have multiple instances of one entity with the same ID.
But in tests it's sometimes useful to have a "fresh" entity as it can uncover bugs in your persistance configuration/code. What you need to do:
Call EntityManager#flush - this will force synchronization of your changes to the database (save method does not guarantee that when called inside a transaction).
Call EntityManager#clear - Hibernate will forget about previous entity instances and will start fetching from DB again.
Alternatively: You can also instruct your Spring repository method to clear entities automatically after a modifying query. - But this will wipe out all entity instances and not only the one you are modifying so it might not be desirable in your application code.

Resources