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.
Related
I have an issue when trying to update the contents of a cart with new values added one by one. I am using Spring boot with Hibernate, JPA Repositories, MySQL Database and a front-end built with vanilla JS.I will describe my issue in the following lines.
I am having an user entity that looks like this:
#Entity
#Table(name = "users")
#Getter
#Setter
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "user_role", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles;
#Column(name = "cartprod_id")
#OneToMany(cascade = {CascadeType.ALL})
private List<CartItem> cartProduct;
This entity has a List<CartItem> field that looks like this:
#Entity
#Getter
#Setter
public class CartItem {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "product_id")
private int productId;
#JsonIgnore
#ManyToOne
private User user;
}
And the relationship between them in the Database looks like in this image:
The idea of the above code is to have a way to keep the state of the products Cart of an user.
The flow is as follows :
I am doing a request from the front-end each time a user adds a product in the cart, when this happens, I am saving the contents of the cart(that consist of only the products ID's) in the database using the CartItem entity.
The problem
With this approach, instead of saving only the last product added to the cart(or replace the cart completely with the new values similar to an update), it is inserting the same object over and over again but with all the new appended values instead of overwriting the old object(table) in the database. An example of this would be in this first image . As you can see I have added a product to the cart with id 327 first.
Next I am adding a product with id 328 but it also adds 327 a second time, this goes on and on, the more products I add. This last code snippet contains my controller code .
#PostMapping("/savecart")
public ResponseEntity<String> saveCartToDb(#RequestBody List<CartItem> cartItemList, Principal principal){
System.out.println(cartItemList);
User logedInUser = userService.findUserByUsername(principal.getName()).get();
List<CartItem> cartItem = logedInUser.getCartProduct();
if(cartItem.isEmpty()){
logedInUser.setCartProduct(cartItemList);
userService.saveNewUser(logedInUser);
}else {
cartItem = cartItemList;
logedInUser.setCartProduct(cartItem);
userService.saveNewUser(logedInUser);
}
// userService.saveNewUser(logedInUser);
return ResponseEntity.ok().body("ASD");
}
How can I overwrite the contents of the List<CartItems> for the user so that I will not append new and old values again and again ? Would a SET help as it won't allow duplicates ?
I have also tried this How do I update an entity using spring-data-jpa? but I a not sure that I need to create a #Query for this issue.
I managed to make it work. The solution is the following.
This is a bidirectional one-to-many / many-to-one issue. In order to be able to remove child elements from the parent, we need to decouple(detach) them from the parent. Since parent and child are bound together by a foreign key the detachment has to be done on both ends. If one has a reference to the other this will not work so BOTH REFERENCES have to be removed. 2 methods are needed and both need to be called when doing decoupling. This is what I used.
private Set<CartProduct> cartProduct;
This one to be added in the parent class (User).
public void removeChild(CartProduct child) {
this.cartProduct.remove(child);
}
This one to be added in the Child class
public void removeParent() {
this.user.removeChild(this);
this.user = null;
}
Methods also have to be called like this
for(CartProduct cartItem : cartItemList){
cartItem.removeParent();
logedInUser.removeChild(cartItem);
}
L.E
It may be that with the above implementation you will get a
java.util.ConcurrentModificationException: null
it happen to me in one of the cases too. In order to fix this I used an Iterator like below.
for (Iterator<CartProduct> iterator = cartItemList.iterator(); iterator.hasNext();) {
CartProduct cartItem = iterator.next();
if (cartItem != null) {
iterator.remove();
}
}
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.
Iam using Spring Hibernate with JPA. cascade=CascadeType.ALL and #Id #GeneratedValue(strategy = GenerationType.IDENTITY). I have two tables one is User the other is photos when I want to add a new photo do I need to go and take the User from the database and then set it as a user in the photo with photo.setUser(user). Is there a way to just do User user = new User(); user.setId(1) and then put it in the photo with photo.setUser() without a full reference I am getting detached entity passed to persist when I execute repo.save(photo) when I am setting only the id.
What I want to do is:
User user = new User();
user.setId(1);
photo.setUser(user);
repo.save(photo)
Where user is already created in the database and has several photos.
instead of:
User user = repo.findUserById(1);
photo.setUser(user);
repo.save(photo);
my entities:
#Entity
#Table(name = "users")
public class Photo implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#OneToMany(cascade = CascadeType.ALL,mappedBy = "user", fetch = FetchType.EAGER)
private List<Photo> photos = new ArrayList<>();
#Entity
#Table(name = "photos")
public class Photo implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#ManyToOne(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
#JoinColumn(name = "user_id")
private User user;
Use EntityManager.getReference() to get an user object. The nice things of it compared with EntityManager.find() is that it will not trigger additional SQL to get the user object.
The user object is just a proxy with only ID is set. Just make sure you do not access its properties other than ID before save , then no additional SQL will be triggered to get the user which is good for setting up the foreign key to the existing object with the known ID.
User user = entityManager.getReference(1, User.class);
photo.setUser(user);
repo.save(photo)
If you are using spring-data repository , the equivalent method is getOne() which will internally call EntityManager.getReference()
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;
I have two persistence entity: User and UserDetail. They have one-to-one relationship. I use hibernate annotations. But I am getting in my database several objects of user information for one same user. Apparently my knowledge of Hibernate annotations are not so good to solve this problem.
User class:
#Entity
#Table(name = "USER")
public class User {
#Id
#GeneratedValue
#Column(name = "ID")
private Long id;
#Column(name = "NAME")
private String name;
#Column(name = "PASSWORD")
private String password;
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private UserDetail userDetail;
// setters and getters
}
UserDetail class:
#Entity
#Table(name = "USER_DETAIL")
public class UserDetail {
#OneToOne
#JoinColumn(name = "USER_ID")
private User user;
// other fields
}
I use this in my code as follows:
UserDetail userDetail = new UserDetail();
userDetail.setInfo(info);
userDetail.setUser(seventhUser);
hibernateTemplate.saveOrUpdate(userDetail);
And everything works properly. Here's what my table USER_DETAIL:
But when I try to change user information, I get an incorrect behavior. I get following table after I again set user information:
UserDetail newUserDetail = new UserDetail();
newUserDetail.setInfo(newInfo);
newUserDetail.setUser(seventhUser);
hibernateTemplate.saveOrUpdate(newUserDetail);
Why the same two objects of information correspond to one user?? I have One-To-One relationship. How can I avoid this? What am I doing wrong?
If you want to modify an existing UserDetail, then you must set its ID, or get it from the session and modify it. Else, Hibernate thinks it's a new one that must be saved, since it doesn't have any ID.
UserDetail existingUserDetail = session.get(UserDetail.class, theUserDetailId);
existingUserDetail.setInfo(newInfo);
To make sure you don't save two UserDetail instances for the same user, you should add a unique constraint on the USER_ID column of the UserDetail database table.