JPA - Entity is already persistent - using GAE - java

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;

Related

Duplicate entry when using one to one relationship with shared primary key in JPA

I followed the example of Modeling With a Shared Primary Key as below:
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
//...
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
#PrimaryKeyJoinColumn
private Address address;
//... getters and setters
}
#Entity
#Table(name = "address")
public class Address {
#Id
#Column(name = "user_id")
private Long id;
//...
#OneToOne
#MapsId
#JoinColumn(name = "user_id")
private User user;
//... getters and setters
}
However, if there are already a record with id 123456 in address table, then I tried to update the record like below:
Address po = new Address();
po.setId(123456L);
po.setCountry("TW");
AddressRepository.save(po);
Duplicate entry '123456' for key Exception will occur. Why JPA will insert a new record instead of merging it? How to solve this problem?
I know the reason finally. It is because the entity has version field and the version field in the new entity is null.
We need to dig into the source of of save() method in JPA.
#Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
Then, if we don't override the isNew(), it will use the default isNew() of JpaMetamodelEntityInformation.
#Override
public boolean isNew(T entity) {
if (!versionAttribute.isPresent()
|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
return super.isNew(entity);
}
BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
}
Here, we can see that if version is present and the version is different from the existing record in the database, the entity will be a new entity and JPA will execute the insert action. Then, it will occur the error of duplicate entry.

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

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.

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;

Categories

Resources