Hibernate clone entity without lazy fields - java

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.

Related

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

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.

bidirectional onetomany on spring boot and hibernate, best way to save

I have 2 entities:
#Data
#Entity
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#Table(name = "source_company")
public class SourceCompany {
#Id
#EqualsAndHashCode.Include
private UUID id;
private String name;
#OneToMany( mappedBy = "company")
private final Set<SourceUser> users = new HashSet<>();
#Column(name = "version")
#Version
private Long version;
}
#Data
#Entity
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#Table(name = "source_user")
public class SourceUser {
#Id
#EqualsAndHashCode.Include
private UUID id;
private String name;
#Column(name = "version")
#Version
private Long version;
//ref
#ManyToOne
#JoinColumn(name = "fk_source_company")
private SourceCompany company;
}
Is it correct to save in this way (only 2 save)?
#Test
public void testSourceUserSave() {
SourceCompany sourceCompany= new SourceCompany();
sourceCompany.setName("xxx");
sourceCompany.setId(UUID.fromString("2bf05cbc-d530-11eb-b8bc-0242ac130003"));
SourceUser sourceUser= new SourceUser();
sourceUser.setName("dev-team");
sourceUser.setId(UUID.fromString("4bede7a0-d530-11eb-b8bc-0242ac130003"));
sourceUser.setCompany(sourceCompany);
sourceCompany.getUsers().add(sourceUser);
sourceCompanyRepository.save(sourceCompany);
sourceUserRepository.save(sourceUser);
assertNotNull(sourceUser);
assertEquals(sourceUser.getCompany().getId(), sourceCompany.getId());
assertEquals(sourceCompany.getUsers().stream().findFirst().get().getId(), sourceUser.getId());
}
or I need to save the user (without company) and the company (without user) and after that to update the user with a save and the company (without save because is not the owner) like this (3 save):
#Test
public void testSourceUserSave() {
SourceCompany sourceCompany= new SourceCompany();
sourceCompany.setName("xxx");
sourceCompany.setId(UUID.fromString("2bf05cbc-d530-11eb-b8bc-0242ac130003"));
SourceUser sourceUser= new SourceUser();
sourceUser.setName("dev-team");
sourceUser.setId(UUID.fromString("4bede7a0-d530-11eb-b8bc-0242ac130003"));
sourceUserRepository.save(sourceUser);
sourceCompanyRepository.save(sourceCompany);
sourceUser.setCompany(sourceCompany);
sourceCompany.getUsers().add(sourceUser);
sourceUserRepository.save(sourceUser);
assertNotNull(sourceUser);
assertEquals(sourceUser.getCompany().getId(), sourceCompany.getId());
assertEquals(sourceCompany.getUsers().stream().findFirst().get().getId(), sourceUser.getId());
}
It seems, looking in the db, that the first way works, so in future can I update only the owner side (I mean update and save) and so can I update the not-owner side only in the object without save it again?
Thanks in advance
You usually tend to save only one of the objects. This can be done adding the
#ManyToOne(cascade = CascadeType.PERSIST)
to the mapping annotation. This makes sure that the nested entities get persisted too
You would need to do just:
SourceCompany sourceCompany= new SourceCompany();
sourceCompany.setName("xxx");
sourceCompany.setId(UUID.fromString("2bf05cbc-d530-11eb-b8bc-0242ac130003"));
SourceUser sourceUser= new SourceUser();
sourceUser.setName("dev-team");
sourceUser.setId(UUID.fromString("4bede7a0-d530-11eb-b8bc-0242ac130003"));
sourceUser.setCompany(sourceCompany);
sourceUserRepository.save(sourceUser);
One more thing to note is that the .save method actually returns an entity itself. That entity is the persisted entity just created. Basically if you manage everything within a single transactional method any modification to the persisted entity within that method (transaction) will be applied without calling any save, merge or update method
I suggest reading about the #Transactional annotation

#Transactional in bidirectional relation with Spring Data returns null

I am using Spring Data and #Transactional annotation(for automatic rollback after tests).
I have simple bidirectional relation between account and user(owning side):
#Entity
#Table(name = "ACCOUNT_T")
public class AccountEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String password;
private String verificationCode;
private Boolean active = false;
#OneToOne(mappedBy = "account", fetch = FetchType.EAGER,
cascade = {CascadeType.MERGE, CascadeType.PERSIST,
CascadeType.DETACH, CascadeType.REFRESH})
private UserEntity user;
}
#Entity
#Table(name = "USER_T")
public class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String surname;
private String phone;
private LocalDate birthDate;
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
#JoinColumn(name = "account_id")
private AccountEntity account;
}
I am using JpaRepositories and fetching is set to eager.
Why sometimes when I get objects from database I can't get their child
objects-null is returned. It depends on from which side I add objects.
I have written simple test using Junit5:
#ExtendWith(SpringExtension.class)
#SpringBootTest
#Transactional
class UserAndAccountRepositoriesTest {
void testA() {
UserEntity userEntity = new UserEntity();
setUserProperties(userEntity);
AccountEntity accountEntity = new AccountEntity();
setAccountProperties(accountEntity); //just setting values for fields
accountEntity.setUser(userEntity);
accountRepository.save(accountEntity);
accountRepository.findAll().get(0).getUser(); //returns user
userRepository.findAll().get(0).getAccount(); //returns null,but should return account related to that user
}
void testB() {
UserEntity userEntity = new UserEntity();
setUserProperties(userEntity);
AccountEntity accountEntity = new AccountEntity();
setAccountProperties(accountEntity);
accountEntity.setUser(userEntity);
accountRepository.save(accountEntity);
accountRepository.findAll().get(0).getUser(); //again returns null,but shouldn't
userRepository.findAll().get(0).getAccount(); //returns account
}
}
Without #Transactional everything works fine - I am not getting null.
What am I doing wrong?
You'd need to set both sides of a relationship for explicitly defining it.
Try adding userEntity.setAccount(accountEntity) during your setup case, this would resolve the issue.
Hibernate won't help you and assume just because you set a -> b, it would set b <- a for you within the other entity.
The reason why it might work without #Transactional is that, without the annotation you are committing your setup data into whatever datasource you are using, and nothing is rollbacked at the end, and since you are selecting data without any id with findAll, you are getting previous user/account entites that have already been committed, some with relationship & some without, thus the random error you are getting.
It is because you are not setting account in userEntity. Please try like following:
userEntity.setAccount(accountEntity);
I will explain why the behavior is different depending on whether your are in a transaction or not :
When you are in a transaction :
a) Any get to fetch an entity A you have created prior to this transaction (so which is already in DB) will return a new object in term of memory adress, and hibernate will resolve its bidirectional relationship, even if you did not set it explicitly.
b) Any get to fetch an entity B you have created earlier in this transaction (so which is not yet in DB) will return the same object in term of memory adress, so it really is the same object, thus if you did not set its bidirectional relationship explicitly, it will not exist until you set it or until the transaction is over (as it will effectively persist B in DB) and you fetch B again.
When you are not in a transaction :
Any get to fetch any entity will behave like described in case a).
Conclusion :
The author was in case b).

Adding an object from Object A into Object B without creating new object ? HIBERNATE

Lets say I have two objects, say one is a User object and the other is a State Object. The state object is basically the 50 states of America so it doesn't ever have to change. The user object however has a Collection of States where the user has been. So like this:
#Entity
#Table(name="tbl_users")
class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id", unique=true, nullable = false)
private int id;
#Column(name="user_name")
private String name;
#OneToMany(targetEntity=State.class, orphanRemoval = false)
#Column(name="states")
private Collection<State> states;
//getters and setters
}
and the States entity looks like this:
#Entity
#Table(name="tbl_states")
class State {
#Id
#Column(name="id", unique=true, nullable=false)
private int id;
#Column(name="state")
private String state;
// getters and setters
}
Code for adding user (using hibernate):
public int addUser(User user) {
em.persist(user);
em.flush();
return user.getId();
}
Code for getting state by id:
public State getStateById(int id) {
return em.createQuery("SELECT s FROM State s WHERE s.id =:id, State.class)
.setParameter("id", id)
.getSingleResult();
}
but when I try to create a User and pick several states, I get a PSQLException:
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "uk_g6pr701i2pcq7400xrlb0hns"
2017-06-21T22:54:35.959991+00:00 app[web.1]: Detail: Key (states_id)=(5) already exists.
I tried looking up the Cascade methods to see if I could use any, but Cascade.MERGE and Cascade.PERSIST seem to do the same thing, and the rest I don't think I need (REMOVE, DETACH, etc). My question is:
How do I add states to the User object without having that error?
This code works:
class Example {
#Test
public void workingTest() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("testPU");
EntityManager em = emf.createEntityManager();
// Creating three states
State alabama = new State(state: 'Alabama');
State louisiana = new State(state: 'Louisiana');
State texas = new State(state: 'Texas');
em.getTransaction().begin();
em.persist(alabama);
em.persist(louisiana);
em.persist(texas);
em.getTransaction().commit();
List<State> states = em.createQuery('FROM State').getResultList();
// Assert only three states on DB
assert states.size() == 3;
User userFromAlabama = new User();
User userFromAlabamaAndTexas = new User();
em.getTransaction().begin();
State alabamaFromDB = em.find(State, alabama.getId());
State texasFromDB = em.find(State, texas.getId());
userFromAlabama.getStates().add(alabamaFromDB);
userFromAlabamaAndTexas.getStates().add(alabamaFromDB);
userFromAlabamaAndTexas.getStates().add(texasFromDB);
em.persist(userFromAlabama);
em.persist(userFromAlabamaAndTexas);
em.getTransaction().commit();
states = em.createQuery('FROM State').getResultList();
// Assert only three states on DB again
assert states.size() == 3;
// Assert one user
User userFromDB = em.find(User, userFromAlabama.getId());
assert userFromDB.getStates().size() == 1;
userFromDB = em.find(User, userFromAlabamaAndTexas.getId());
assert userFromDB.getStates().size() == 2;
}
}
#Entity
#Table(name="tbl_users")
class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id
#Column(name="user_name")
private String name
#ManyToMany
private Collection<State> states = Lists.newArrayList()
// Getters and setters
}
#Entity
#Table(name="tbl_states")
class State {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(name="state")
private String state;
// Getters and setters
}
You should change your mapping to #ManyToMany!
And you must have 3 tables on DB like this:
TBL_USERS, TBL_STATES and TBL_USERS_TBL_STATES
The TBL_USERS_TBL_STATES table is the default table name that Hibernate uses when a property is annotated with #ManyToMany. If you want to change the tablename of TBL_USERS_TBL_STATES, use the #JoinTable annotation too. See the docs here
With this configuration, you should be able to fetch a State from database, add it to a new User and then persist it. I made a unit test and It works!
In your case it might be better to use a manytomany association with manytomany hibernate dont generate unicity constraint.
Hibernate auto generation scheme behavior is a little bit strange with onetoMany but you can use this workaround.
Try this:
#ManyToMany
#JoinTable(name = "user_state")
private List<State> states;

Hibernate One to Many add new children

I have read a lot of articles and answers on similar questions, but i still can not understand.
Here my parent class:
#Entity
#Table(name = "users")
public class User implements Serializable, Annotation {
#Id
#GeneratedValue(generator = "system-uuid", strategy = GenerationType.IDENTITY)
#GenericGenerator(name = "system-uuid", strategy = "uuid2")
#Column(name = "uuid", unique = true)
protected String uuid;
#Column(name = "username")
protected String username;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.user", cascade = CascadeType.ALL)
private Set<UserItem> userItems;
}
Here we can see two sets - Items and Friends of User.
Example of UserItemId class:
#Entity
#Table(name = "user_item")
#AssociationOverrides({
#AssociationOverride(name = "pk.user",
joinColumns = #JoinColumn(name = "user_uuid")),
#AssociationOverride(name = "pk.ItemShop",
joinColumns = #JoinColumn(name = "item_shop_uuid")) })
public class UserItem implements Serializable {
#EmbeddedId
protected UserItemId pk = new UserItemId();
#Transient
public User getUser() {
return getPk().getUser();
}
public void setUser(User user) {
getPk().setUser(User);
}
#Transient
public ItemShop getItemShop() {
return getPk().getItemShop();
}
public void setItemShop(ItemShop ItemShop) {
getPk().setItemShop(ItemShop);
}
}
And now i'm trying to add new Item to user:
/*
'user' in this code i got from SecurityContext in controller
and I give it as parameter from controller to method (service layer) which can add new Item to user.
So user it is entity which already exists in database - it is logged user
*/
UserItem userItem = new UserItem();
userItem.setUser(user);
userItem.setItemShop(ItemShop);
user.getUserItems().add(userItem);
session.saveOrUpdate(user);
But i got an error:
org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session
I understand that the main problem is in CascadeTypes - ALL it is not best variant for this, because hibernate can not do what i want - understand that User is a persistent object and just add to persistent new item and save it to database. So i want to ask you, what is the best practice to win in this situation, when i have Parent class (User) and children (Items) and i want to add new items and delete items from parent and save it to database. Indeed i found working method (using iterator), but i understand that on highload project it is the worst variant make for loop for such operations. I really want to find best practices for such situations.
QUESTION UPDATE:
I've made mistake. When i call code which execute adding child to set (code above) - everything is good - child added in database and in entity, but when I'm trying to delete child:
Set<UserItem> userItems = user.getUserItems();
UserItem userItem = userDAO.findUserItem(user, itemShop);
userItems.remove(userItem);
session.saveOrUpdate(user);
session.flush();
Then i got exception:
org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session
ONE MORE UPDATE:
After a lot of experiments i got working code of adding item:
UserItem userItem = new UserItem();
userItem.setUser(user);
userItem.setItemShop(ItemShop);
user.getUserItems().add(userItem);
session.saveOrUpdate(user);
It is a code of method in a Service layer which add item to user. And it is working. And i have half-working code to delete item from user:
Set<UserItem> userItems = user.getuserItems();
UserItem userItem = UserDAO.findUserItem(user, itemShop);
userItems.remove(userItem);
userDAO.merge(user);
It is not properly works code. We have a situation - user has 3 items. I delete one item - everything is fine. Later I'm trying to delete one more item and hibernate says me:
ObjectNotFoundException: No row with the given identifier exists
That means in cache still 3 items - that one i delete first - still in cache and hibernate try to add it into database - but it is deleted.
And i got one more mind idea, which can help us to answer this question. Object User - where i'm trying to delete child - it is logged user from SecurityContext - i got it from controller - controller has annotation where User got from Security. So, hibernate already has in cache logged user (User object). Any ideas?
OP has to manually manage what is effectively a join table because there are extra fields on the join table that have to be managed as well. This is causing issues when trying to delete an item object off a user.
This is the standard way that I set up a bi-directional many to many when I have to manage the join explicitly or I have extra information attached to the join. If I don't have any extra info, or don't have to manage the join myself, just use the #JoinTable annotation.
New Objects:
#Entity
#Table(name = "users")
public class User implements Serializable, Annotation {
#Id
#GeneratedValue(generator = "system-uuid", strategy = GenerationType.IDENTITY)
#GenericGenerator(name = "system-uuid", strategy = "uuid2")
#Column(name = "uuid", unique = true)
protected String userUUID;
#Column(name = "username")
protected String username;
#OneToMany(mappedBy="user", cascade = CascadeType.ALL, orphanRemoval=true)
private Set<UserItem> items = new HashSet<UserItem>();
}
Item Object:
#Entity
#Table(name="items")
public class Item implements Serializable{
#Id
#GeneratedValue(generator = "system-uuid")
#GenericGenerator(name = "system-uuid", strategy = "uuid2")
#Column(name = "uuid", unique = true)
protected String itemUUID;
#Column(name = "name")
protected String name;
#Column(name = "description")
protected String description;
#OneToMany(mappedBy="item", cascade = CascadeType.ALL, orphanRemoval=true)
private Set<UserItem> users = new HashSet<UserItem>();
UserItem class:
#Entity
#Table(name = "user_item")
public class UserItem implements Serializable {
//Generate this like you are doing with your others.
private String userItemUUID;
#ManyToOne
#JoinColumn(name="userUUID")
private User user;
#ManyToOne
#JoinColumn(name="itemUUID")
private Item item;
private BigDecimal moneyCollect;
private Date dateAdded = new Date();
}
Your service to delete looks like this:
UserItem userItem = userDAO.findUserItem(user, itemShop);
user.getUserItems().remove(userItem);
session.saveOrUpdate(user);
session.flush();
You can do it the other way as well.
UserItem userItem = userDAO.findUserItem(user, itemShop);
item.getUserItems().remove(userItem);
session.saveOrUpdate(item);
session.flush();
The exception you are getting is caused by Hibernate finding two UserItem objects within the method that are both persistent and not knowing which one to delete.
//Create one UserItem instance here
Set<UserItem> userItems = user.getUserItems();
//Create another UserItem instance here
UserItem userItem = userDAO.findUserItem(user, itemShop);
userItems.remove(userItem);
//Hibernate doesn't know which UserItem instance to remove, and throws an exception.
session.saveOrUpdate(user);
session.flush();
If any of that didn't make sense, let me know.
ADDENDUM: OP was getting a persistent instance of a User inside their controller and not doing anything with it. This was causing the NonUniqueObjectException that Hibernate was throwing.

Categories

Resources