Reputation: 11309
I have a JPA-persisted object model that contains a many-to-one relationship: an Account
has many Transactions
. A Transaction
has one Account
.
Here's a snippet of the code:
@Entity
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
private Account fromAccount;
....
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(cascade = {CascadeType.ALL},fetch= FetchType.EAGER, mappedBy = "fromAccount")
private Set<Transaction> transactions;
I am able to create an Account
object, add transactions to it, and persist the Account
object correctly. But, when I create a transaction, using an existing already persisted Account, and persisting the the Transaction, I get an exception:
Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.paulsanwald.Account at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:141)
So, I am able to persist an Account
that contains transactions, but not a Transaction that has an Account
. I thought this was because the Account
might not be attached, but this code still gives me the same exception:
if (account.getId()!=null) {
account = entityManager.merge(account);
}
Transaction transaction = new Transaction(account,"other stuff");
// the below fails with a "detached entity" message. why?
entityManager.persist(transaction);
How can I correctly save a Transaction
, associated with an already persisted Account
object?
Upvotes: 358
Views: 743421
Reputation: 7165
for anyone who happens upon this and the above answers aren't working:
the reason here is the entity be inserted is an existed data, which has given primary key column value, and fix it by change to merge.
Upvotes: 0
Reputation: 1297
for anyone who happens upon this and the above answers aren't working
In my case the issue was a race condition. The app I'm working on is comprised of a fleet of microservices that uses a queue to communicate. In some cases many messages get sent to the same service - and in turn the same code that touches the db - at the same time. In that block of code there are checks to see if a record exists in the db, and, based on the result of the check, either a new one is made or the existing one is used.
The race condition was due to callers (a) and (b) checking the db to see if a record exists and taking action based on that result. Both would attempt to create the same record because (b) got the same result as (a) because (b) asked before (a) was able to serialize the record.
I ended up adding lombok @Syncrhonized to the top of the method to ensure only one caller at a time could perform the db operation and things work.
I did explore the @Transactional
annotation solutions above and they did not work for me. That may well be the better solution. Rn I'm in a place where I need working and better can wait.
g/l
Upvotes: 0
Reputation: 844
Even if your annotations are declared correctly to properly manage the one-to-many relationship you may still encounter this precise exception. When adding a new child object, Transaction
, to an attached data model you'll need to manage the primary key value - unless you're not supposed to. If you supply a primary key value for a child entity declared as follows before calling persist(T)
, you'll encounter this exception.
@Entity
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
....
In this case, the annotations are declaring that the database will manage the generation of the entity's primary key values upon insertion. Providing one yourself (such as through the Id's setter) causes this exception.
Alternatively, but effectively the same, this annotation declaration results in the same exception:
@Entity
public class Transaction {
@Id
@org.hibernate.annotations.GenericGenerator(name="system-uuid", strategy="uuid")
@GeneratedValue(generator="system-uuid")
private Long id;
....
So, don't set the id
value in your application code when it's already being managed.
Upvotes: 6
Reputation: 907
The problem here is lack of control.
When we use the CrudRepository/JPARepository save method we loose the transactional control.
To overcome this issue we have Transaction Management
I prefer the @Transactional
mechanism
imports
import javax.transaction.Transactional;
Entire Source Code:
package com.oracle.dto;
import lombok.*;
import javax.persistence.*;
import java.util.Date;
import java.util.List;
@Entity
@Data
@ToString(exclude = {"employee"})
@EqualsAndHashCode(exclude = {"employee"})
public class Project {
@Id
@GeneratedValue(strategy = GenerationType.AUTO,generator = "ps")
@SequenceGenerator(name = "ps",sequenceName = "project_seq",initialValue = 1000,allocationSize = 1)
@Setter(AccessLevel.NONE)
@Column(name = "project_id",updatable = false,nullable = false)
private Integer pId;
@Column(name="project_name",nullable = false,updatable = true)
private String projectName;
@Column(name="team_size",nullable = true,updatable = true)
private Integer teamSize;
@Column(name="start_date")
private Date startDate;
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name="projectemp_join_table",
joinColumns = {@JoinColumn(name = "project_id")},
inverseJoinColumns = {@JoinColumn(name="emp_id")}
)
private List<Employee> employees;
}
package com.oracle.dto;
import lombok.*;
import javax.persistence.*;
import java.util.List;
@Entity
@Data
@EqualsAndHashCode(exclude = {"projects"})
@ToString(exclude = {"projects"})
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO,generator = "es")
@SequenceGenerator(name = "es",sequenceName = "emp_seq",allocationSize = 1,initialValue = 2000)
@Setter(AccessLevel.NONE)
@Column(name = "emp_id",nullable = false,updatable = false)
private Integer eId;
@Column(name="fist_name")
private String firstName;
@Column(name="last_name")
private String lastName;
@ManyToMany(mappedBy = "employees")
private List<Project> projects;
}
package com.oracle.repo;
import com.oracle.dto.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmployeeRepo extends JpaRepository<Employee,Integer> {
}
package com.oracle.repo;
import com.oracle.dto.Project;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProjectRepo extends JpaRepository<Project,Integer> {
}
package com.oracle.services;
import com.oracle.dto.Employee;
import com.oracle.dto.Project;
import com.oracle.repo.ProjectRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.transaction.Transactional;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
@Component
public class DBServices {
@Autowired
private ProjectRepo repo;
@Transactional
public void performActivity(){
Project p1 = new Project();
p1.setProjectName("Bank 2");
p1.setTeamSize(20);
p1.setStartDate(new Date(2020, 12, 22));
Project p2 = new Project();
p2.setProjectName("Bank 1");
p2.setTeamSize(21);
p2.setStartDate(new Date(2020, 12, 22));
Project p3 = new Project();
p3.setProjectName("Customs");
p3.setTeamSize(11);
p3.setStartDate(new Date(2010, 11, 20));
Employee e1 = new Employee();
e1.setFirstName("Pratik");
e1.setLastName("Gaurav");
Employee e2 = new Employee();
e2.setFirstName("Ankita");
e2.setLastName("Noopur");
Employee e3 = new Employee();
e3.setFirstName("Rudra");
e3.setLastName("Narayan");
List<Employee> empList1 = new LinkedList<Employee>();
empList1.add(e2);
empList1.add(e3);
List<Employee> empList2 = new LinkedList<Employee>();
empList2.add(e1);
empList2.add(e2);
List<Project> pl1=new LinkedList<Project>();
pl1.add(p1);
pl1.add(p2);
List<Project> pl2=new LinkedList<Project>();
pl2.add(p2);pl2.add(p3);
p1.setEmployees(empList1);
p2.setEmployees(empList2);
e1.setProjects(pl1);
e2.setProjects(pl2);
repo.save(p1);
repo.save(p2);
repo.save(p3);
}
}
Upvotes: 0
Reputation: 47
@OneToMany(mappedBy = "xxxx", cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE})
worked for me.
Upvotes: 1
Reputation: 946
So I stumbled across this Question and Answers because I got the same Error but a very basic object with just Strings and Integers.
But in my case I was trying to set a Value to a Field which was annotated with @Id
.
So if you are using @Id
it seems that you can't create a new Object on a Class and set an Id by yourself and persist it to Database. You should then leave the Id blank. I wasn't aware and maybe this helps anyone else.
Upvotes: 0
Reputation: 799
This error comes from the JPA Lifecycle. To solve, no need to use specific decorator. Just join the entity using merge like that :
entityManager.merge(transaction);
And don't forget to correctly set up your getter and setter so your both side are sync.
Upvotes: 1
Reputation: 4028
Another reason I have encountered this issue is having Entities that aren't versioned by Hibernate in a transaction.
Add a @Version
annotation to all mapped entities
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@Version
private Integer version;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "orders")
private CustomerOrders orders;
}
@Entity
public class CustomerOrders {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@Version
private Integer version;
private BigDecimal value;
}
Upvotes: 0
Reputation: 2924
Here is my fix.
Below is my Entity. Mark that the id is annotated with @GeneratedValue(strategy = GenerationType.AUTO), which means that the id would be generated by the Hibernate. Don't set it when entity object is created. As that will be auto generated by the Hibernate. Mind you if the entity id field is not marked with @GeneratedValue then not assigning the id a value manually is also a crime, which will be greeted with IdentifierGenerationException: ids for this class must be manually assigned before calling save()
@Entity
@Data
@NamedQuery(name = "SimpleObject.findAll", query="Select s FROM SimpleObject s")
public class SimpleObject {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column
private String key;
@Column
private String value;
}
And here is my main class.
public class SimpleObjectMain {
public static void main(String[] args) {
System.out.println("Hello Hello From SimpleObjectMain");
SimpleObject simpleObject = new SimpleObject();
simpleObject.setId(420L); // Not right, when id is a generated value then no need to set this.
simpleObject.setKey("Friend");
simpleObject.setValue("Bani");
EntityManager entityManager = EntityManagerUtil.getEntityManager();
entityManager.getTransaction().begin();
entityManager.persist(simpleObject);
entityManager.getTransaction().commit();
List<SimpleObject> simpleObjectList = entityManager.createNamedQuery("SimpleObject.findAll").getResultList();
for(SimpleObject simple : simpleObjectList){
System.out.println(simple);
}
entityManager.close();
}
}
When I tried saving that, it was throwing that
PersistentObjectException: detached entity passed to persist.
All I needed to fix was remove that id setting line for the simpleObject in the main method.
Upvotes: 3
Reputation: 5257
An old question, but came across the same issue recently . Sharing my experience here.
Entity
@Data
@Entity
@Table(name = "COURSE")
public class Course {
@Id
@GeneratedValue
private Long id;
}
Saving the entity (JUnit)
Course course = new Course(10L, "testcourse", "DummyCourse");
testEntityManager.persist(course);
Fix
Course course = new Course(null, "testcourse", "DummyCourse");
testEntityManager.persist(course);
Conclusion : If the entity class has @GeneratedValue for primary key (id), then ensure that you are not passing a value for the primary key (id)
Upvotes: 9
Reputation: 153690
So, you need to remove the @CascadeType.ALL
from the @ManyToOne
association. Child entities should not cascade to parent associations. Only parent entities should cascade to child entities.
@ManyToOne(fetch= FetchType.LAZY)
Notice that I set the fetch
attribute to FetchType.LAZY
because eager fetching is very bad for performance.
Whenever you have a bidirectional association, you need to synchronize both sides using addChild
and removeChild
methods in the parent entity:
public void addTransaction(Transaction transaction) {
transcations.add(transaction);
transaction.setAccount(this);
}
public void removeTransaction(Transaction transaction) {
transcations.remove(transaction);
transaction.setAccount(null);
}
Upvotes: 36
Reputation: 2692
Resolved by saving dependent object before the next.
This was happened to me because I was not setting Id (which was not auto generated). and trying to save with relation @ManytoOne
Upvotes: 1
Reputation: 3689
My Spring Data JPA-based answer: I simply added a @Transactional
annotation to my outer method.
The child entity was immediately becoming detached because there was no active Hibernate Session context. Providing a Spring (Data JPA) transaction ensures a Hibernate Session is present.
https://vladmihalcea.com/a-beginners-guide-to-jpa-hibernate-entity-state-transitions/
Upvotes: 9
Reputation: 737
If above solutions not work just one time comment the getter and setter methods of entity class and do not set the value of id.(Primary key) Then this will work.
Upvotes: 0
Reputation: 2761
Remove cascading from the child entity Transaction
, it should be just:
@Entity class Transaction {
@ManyToOne // no cascading here!
private Account account;
}
(FetchType.EAGER
can be removed as well as it's the default for @ManyToOne
)
That's all!
Why? By saying "cascade ALL" on the child entity Transaction
you require that every DB operation gets propagated to the parent entity Account
. If you then do persist(transaction)
, persist(account)
will be invoked as well.
But only transient (new) entities may be passed to persist
(Transaction
in this case). The detached (or other non-transient state) ones may not (Account
in this case, as it's already in DB).
Therefore you get the exception "detached entity passed to persist". The Account
entity is meant! Not the Transaction
you call persist
on.
You generally don't want to propagate from child to parent. Unfortunately there are many code examples in books (even in good ones) and through the net, which do exactly that. I don't know, why... Perhaps sometimes simply copied over and over without much thinking...
Guess what happens if you call remove(transaction)
still having "cascade ALL" in that @ManyToOne? The account
(btw, with all other transactions!) will be deleted from the DB as well. But that wasn't your intention, was it?
Upvotes: 68
Reputation: 341
In my case I was committing transaction when persist method was used. On changing persist to save method , it got resolved.
Upvotes: 0
Reputation: 4359
Using merge is risky and tricky, so it's a dirty workaround in your case. You need to remember at least that when you pass an entity object to merge, it stops being attached to the transaction and instead a new, now-attached entity is returned. This means that if anyone has the old entity object still in their possession, changes to it are silently ignored and thrown away on commit.
You are not showing the complete code here, so I cannot double-check your transaction pattern. One way to get to a situation like this is if you don't have a transaction active when executing the merge and persist. In that case persistence provider is expected to open a new transaction for every JPA operation you perform and immediately commit and close it before the call returns. If this is the case, the merge would be run in a first transaction and then after the merge method returns, the transaction is completed and closed and the returned entity is now detached. The persist below it would then open a second transaction, and trying to refer to an entity that is detached, giving an exception. Always wrap your code inside a transaction unless you know very well what you are doing.
Using container-managed transaction it would look something like this. Do note: this assumes the method is inside a session bean and called via Local or Remote interface.
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void storeAccount(Account account) {
...
if (account.getId()!=null) {
account = entityManager.merge(account);
}
Transaction transaction = new Transaction(account,"other stuff");
entityManager.persist(account);
}
Upvotes: 18
Reputation: 3606
This is a typical bidirectional consistency problem. It is well discussed in this link as well as this link.
As per the articles in the previous 2 links you need to fix your setters in both sides of the bidirectional relationship. An example setter for the One side is in this link.
An example setter for the Many side is in this link.
After you correct your setters you want to declare the Entity access type to be "Property". Best practice to declare "Property" access type is to move ALL the annotations from the member properties to the corresponding getters. A big word of caution is not to mix "Field" and "Property" access types within the entity class otherwise the behavior is undefined by the JSR-317 specifications.
Upvotes: 156
Reputation: 4482
The solution is simple, just use the CascadeType.MERGE
instead of CascadeType.PERSIST
or CascadeType.ALL
.
I have had the same problem and CascadeType.MERGE
has worked for me.
I hope you are sorted.
Upvotes: 419
Reputation: 923
Don't pass id(pk) to persist method or try save() method instead of persist().
Upvotes: 39
Reputation: 951
You need to set Transaction for every Account.
foreach(Account account : accounts){
account.setTransaction(transactionObj);
}
Or it colud be enough (if appropriate) to set ids to null on many side.
// list of existing accounts
List<Account> accounts = new ArrayList<>(transactionObj.getAccounts());
foreach(Account account : accounts){
account.setId(null);
}
transactionObj.setAccounts(accounts);
// just persist transactionObj using EntityManager merge() method.
Upvotes: 1
Reputation: 4956
If nothing helps and you are still getting this exception, review your equals()
methods - and don't include child collection in it. Especially if you have deep structure of embedded collections (e.g. A contains Bs, B contains Cs, etc.).
In example of Account -> Transactions
:
public class Account {
private Long id;
private String accountName;
private Set<Transaction> transactions;
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Account))
return false;
Account other = (Account) obj;
return Objects.equals(this.id, other.id)
&& Objects.equals(this.accountName, other.accountName)
&& Objects.equals(this.transactions, other.transactions); // <--- REMOVE THIS!
}
}
In above example remove transactions from equals()
checks. This is because hibernate will imply that you are not trying to update old object, but you pass a new object to persist, whenever you change element on the child collection.
Of course this solutions will not fit all applications and you should carefully design what you want to include in the equals
and hashCode
methods.
Upvotes: 6
Reputation: 194
Maybe It is OpenJPA's bug, When rollback it reset the @Version field, but the pcVersionInit keep true. I have a AbstraceEntity which declared the @Version field. I can workaround it by reset the pcVersionInit field. But It is not a good idea. I think it not work when have cascade persist entity.
private static Field PC_VERSION_INIT = null;
static {
try {
PC_VERSION_INIT = AbstractEntity.class.getDeclaredField("pcVersionInit");
PC_VERSION_INIT.setAccessible(true);
} catch (NoSuchFieldException | SecurityException e) {
}
}
public T call(final EntityManager em) {
if (PC_VERSION_INIT != null && isDetached(entity)) {
try {
PC_VERSION_INIT.set(entity, false);
} catch (IllegalArgumentException | IllegalAccessException e) {
}
}
em.persist(entity);
return entity;
}
/**
* @param entity
* @param detached
* @return
*/
private boolean isDetached(final Object entity) {
if (entity instanceof PersistenceCapable) {
PersistenceCapable pc = (PersistenceCapable) entity;
if (pc.pcIsDetached() == Boolean.TRUE) {
return true;
}
}
return false;
}
Upvotes: 1
Reputation: 2066
In your entity definition, you're not specifying the @JoinColumn for the Account
joined to a Transaction
. You'll want something like this:
@Entity
public class Transaction {
@ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
@JoinColumn(name = "accountId", referencedColumnName = "id")
private Account fromAccount;
}
EDIT: Well, I guess that would be useful if you were using the @Table
annotation on your class. Heh. :)
Upvotes: 5
Reputation: 13262
Probably in this case you obtained your account
object using the merge logic, and persist
is used to persist new objects and it will complain if the hierarchy is having an already persisted object. You should use saveOrUpdate
in such cases, instead of persist
.
Upvotes: 14