15

Imagine the following models:

Employee:

@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "employee_project", joinColumns = @JoinColumn(name = "Emp_Id"), inverseJoinColumns = @JoinColumn(name = "Proj_id"))
private Set<Project> projects = new HashSet<Project>();

Project:

@ManyToMany(mappedBy = "projects")
private Set<Employee> employees = new HashSet<Employee>();

Now if I create a new employee that refers to an existing project and try to persist that employee, I get an error:

detached entity passed to persist: Project

I create the employee as follows:

public void createNewEmployee(EmployeeDTO empDTO) {

  Employee emp = new Employee();
  // add stuff from DTO, including projects

  repository.saveAndFlush(emp);  // FAILS
}

and I update existing ones like this:

public void updateEmployee(EmployeeDTO empDTO) {

   Employee emp = repository.findOne(empDTO.getId());
   // set stuff from DTO, including projects

   repository.saveAndFlush(emp);  // WORKS!
}
Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
wannabeartist
  • 2,753
  • 6
  • 36
  • 49

1 Answers1

20

I guess you're interacting with the repository without expanding the transaction boundaries appropriately. By default, the transaction (and thus session) boundary is at the repository method level. This causes the Project instance to be detached from the EntityManager, so that it cannot be included in a persist operation.

The solution here is to extend the transaction boundary to the client:

@Component
class YourRepositoryClient {

  private final ProjectRepository projects;
  private final EmployeeRepository employees;

  // … constructor for autowiring

  @Transactional
  public void doSomething() {
    Project project = projects.findOne(1L);
    Employee employee = employees.save(new Employee(project));
  }
}

This approach causes the Project instance stay a managed entity and thus the persist operation to be executed for the fresh Employee instance being handled correctly.

The difference with the two repository interactions is that in the second case you'll have a detached instance (has already been persisted, has an id set), where as in the first example you have a completely unmanaged instances that does not have an id set. The id property is what causes the repository to differentiate between calling persist(…) and merge(…). So the first approach will cause a persist(…) to be triggered, the second will cause a merge(…).

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • Thanks, that works, but I'd still like to know why my update method works without fetching the projects from their repository (see my updated question for details) – wannabeartist Apr 11 '13 at 08:48
  • 1
    It's not a good idea to extend questions here as they invalidate the answer to some degree. Better ask new questions. I'll update my answer accordingly. – Oliver Drotbohm Apr 11 '13 at 10:13
  • Is that spring's `@Transactional` or JPA's, or does it matter? – CorayThan Oct 31 '14 at 07:01
  • Spring's. There's no `@Transactional` in JPA. Generally speaking, use whatever transaction control mechanism you like (e.g. Spring annotations, EJB transaction annotations, JTA 1.2's `@Transactional`). – Oliver Drotbohm Oct 31 '14 at 07:30
  • How to do it without preflight `findOne`? I mean to force Spring Data to detect that child object already exists in the database (by given `id`) and should be just `merge`-it (automatically, by Spring Data, not by me!!!) ??? Is Spring Data sucks in such convenience? – ieXcept Jan 29 '19 at 20:45