How can I use (Spring Boot / Hibernate / JPA) to create a Copy of Parent Entity and also to copy all the Children Entities.
After multiple versions I came up with following procedure.
I need this part to be @Transactional in case Parent is saved but children are not so that in that case transaction rolls back.
One problem is that this way I can't use LAZY loading.
Another problem is that I need to have the code that gets the Parent and the Children inside Controller since it can't be under @Transactional because then I can't use ID = null.
I also can't move this code into Service Method that is not @Transactional and then call another Service Method that is @Transactional to do the saving because when @Transactional Method is called within the Service Method then @Transactional Annotation is ignored.
So I got it to work and it looks kind of straight forward but not ideal and a bit complicated at the same time having to be aware of all these considerations.
CopyController.java
@GetMapping(path = "/{schemaId}/copyEntityList", produces = "application/json")
public void copyEntityList(@RequestBody List<Long> ids) throws RuntimeException {
//GET ENTITIES
List<Entity2> entities = entityRepository.findAllById(ids);
//INSERT ENTITIES
copyService.insertEntities(entities);
}
CopyService2.java
@Transactional
public void insertEntities(List<Entity2> entities) throws RuntimeException {
//REMOVE IDS, CHANGE NAME & CODE
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
for(Entity2 entity : entities) {
entity.setName(entity.getName() + " - COPY (" + LocalDateTime.now().format(formatter) + ")");
entity.setCode(entity.getCode() + " (" + LocalDateTime.now().format(formatter) + ")");
entity.setId (null);
}
//INSERT PARENTS (to get IDs)
entityRepository.saveAll(entities);
if (false) { throw new RuntimeException("Error"); }
//UPDATE CHILDREN (with PARENT_ID)
List<Entity2> clonedChildEntities = new ArrayList<>();
for(Entity2 parentEntity : entities) {
for(Entity2 childEntity : parentEntity.getChildEntities()) {
childEntity.setId (null);
childEntity.setParentId(parentEntity.getId());
childEntity.setName (childEntity.getName() + " - COPY (" + LocalDateTime.now().format(formatter) + ")");
childEntity.setCode (childEntity.getCode() + " (" + LocalDateTime.now().format(formatter) + ")");
clonedChildEntities.add(childEntity);
}
}
//INSERT CHILDREN
entityRepository.saveAll(clonedChildEntities);
}
You'll have to implement a clone operation on it. What's tripping you up is the JPA/Hibernate entity cache that tells hibernate which object instances are persisted. Your saved object is in the entity cache and even though you've erased the identifier it recognizes that instance as being persisted. Hence it's going to cause JPA to freak out.
I'm suggesting using the Cloneable interface over doing it all inside copyEntityList
only because you have to create a new instance of these objects. With clone
it'll copy the fields for you. If you choose to mimic this in your copyEntityList
method by manually instantiating the Entity2
object then you'll have to manually copy its fields over as well. Either approach will work.
Doing something like this should work:
Entity2 parent = ....
Entity2 copy = parent.clone();
jpaInterface.save(copy);
...
@Entity
public class Entity2 implements Cloneable {
@Id
Integer id;
@Column(name="PARENT_ID") Long parentId;
List<MyObject> children;
public MyObject clone() {
MyObject copy = super.clone();
copy.id = null;
copy.parentId = null;
copy.setName(....); // your special business logic for naming, etc.
copy.setCode(....);
copy.children = this.children.stream().map(MyObject::clone).collect(Collectors.toList());
return copy;
}
}
This is a rough outline of the approach. Entity2 implements java.lang.Cloneable
interface. In the clone
method you will perform a deep copy of the children objects by calling clone of each child. In each Cloneable methods you can request Java clone this object, but the copy will retain the id and parentId fields too. Delete those by setting those fields to null, and you now have a separate copied instance for each of the Entities known to JPA, and you can save that as a true copy of the other object.
Word to the wise, since your structure is recursive and those children are lazy by cloning this parent it will load all children objects immediately, all the way down the tree (otherwise how else would it clone the entire struct?). This will have performance issues with large sets of entities.