Clonage d'une entité JPA
J'ai une entité JPA déjà persisté dans la base de données.
Je voudrais en avoir une copie (avec un id différent), avec quelques champs modifiés.
Quelle est la façon la plus simple de le faire? Comme:
- définir son champ
@Id
surnull
et le persister fonctionnera? - devrai-je créer une méthode clone pour l'entité (copier tous les champs sauf le
@Id
)? - est-il une autre approche (comme l'utilisation d'un clonage cadre)?
6 réponses
Utilisez EntityManager.detach
. Cela rend le bean plus lié à EntityManager. Ensuite, définissez L'Id sur le nouvel Id (ou null si automatique), modifiez les champs dont vous avez besoin et persistez.
Lorsque vous utilisez EclipseLink, vous pouvez utiliser la fonction CopyGroup très pratique:
Http://wiki.eclipse.org/EclipseLink/Examples/JPA/AttributeGroup#CopyGroup
Un gros avantage est que sans trop bidouiller, il clone correctement les relations privées.
C'est mon code, cloner une Playlist avec sa relation privée @OneToMany est une question de quelques lignes:
public Playlist cloneEntity( EntityManager em ) {
CopyGroup group = new CopyGroup();
group.setShouldResetPrimaryKey( true );
Playlist copy = (Playlist)em.unwrap( JpaEntityManager.class ).copy( this, group );
return copy;
}
Assurez-vous que vous utilisez persist() pour enregistrer ce nouvel objet, merge() ne travail.
Vous pouvez utiliser des frameworks de mappage comme Orika. http://orika-mapper.github.io/orika-docs/ Orika est un framework de mappage Java Bean qui copie récursivement les données d'un objet à un autre. Il est facile à configurer et offre diverses flexibilités.
Voici comment je l'ai utilisé dans mon projet:
ajout d'une dépendance:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.4.6</version>
</dependency>
, Puis l'utiliser dans le code comme suit:
MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
MapperFacade mapper=mapperFactory.getMapperFacade();
User mappedUser = mapper.map(oldUser, User.class);
Cela pourrait aider si vous avez beaucoup de cas d'utilisation où ce type de clonage est nécessaire.
Je fais face au même problème aujourd'hui: j'ai une entité dans la base de données et je veux:
- l'obtenir à partir de la base de données
- change une de ses valeurs d'attributs
- créer un clone de lui
- Ne modifiez que quelques attributs du clone
- conserver le clone dans la base de données
Je réussis à faire les étapes suivantes:
@PersistenceContext(unitName = "...")
private EntityManager entityManager;
public void findUpdateCloneAndModify(int myEntityId) {
// retrieve entity from database
MyEntity myEntity = entityManager.find(MyEntity.class, myEntityId);
// modify the entity
myEntity.setAnAttribute(newValue);
// update modification in database
myEntity = entityManager.merge(myEntity);
// detach entity to use it as a new entity (clone)
entityManager.detach(myEntity);
myEntity.setId(0);
// modify detached entity
myEntity.setAnotherAttribute(otherValue);
// persist modified clone in database
myEntity = entityManager.merge(myEntity);
}
Remarque : la dernière étape (persistance du clone) ne fonctionne pas si j'utilise 'persist' au lieu de 'merge', même si je note dans mode de débogage qui clone id a été changé après 'persist' commande !
Le problème auquel je suis toujours confronté est que ma première entité n'a pas été modifiée avant de la détacher.
Comme mentionné dans les commentaires de la réponse acceptée, detach ignorera les modifications non affichées de l'entité gérée.
Si vous utilisez spring, vous avez une autre option qui consiste à utiliser org.springframework.beans.BeanUtils
Ici, vous avez BeanUtils.copyProperties(Object source, Object target)
. Cela vous permettra de faire une copie complète sans altérer entityManager.
Comme je l'ai expliqué dans Cet article , il vaut mieux utiliser un constructeur de copie et contrôler exactement quels attributs doivent être clonés.
Donc, si vous avez une entité Post
comme celle-ci:
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
@GeneratedValue
private Long id;
private String title;
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();
@OneToOne(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
private PostDetails details;
@ManyToMany
@JoinTable(
name = "post_tag",
joinColumns = @JoinColumn(
name = "post_id"
),
inverseJoinColumns = @JoinColumn(
name = "tag_id"
)
)
private Set<Tag> tags = new HashSet<>();
//Getters and setters omitted for brevity
public void addComment(
PostComment comment) {
comments.add(comment);
comment.setPost(this);
}
public void addDetails(
PostDetails details) {
this.details = details;
details.setPost(this);
}
public void removeDetails() {
this.details.setPost(null);
this.details = null;
}
}
Il n'est pas logique de cloner le comments
lors de la duplication d'un Post
et de l'utiliser comme modèle pour un nouveau:
Post post = entityManager.createQuery(
"select p " +
"from Post p " +
"join fetch p.details " +
"join fetch p.tags " +
"where p.title = :title", Post.class)
.setParameter(
"title",
"High-Performance Java Persistence, 1st edition"
)
.getSingleResult();
Post postClone = new Post(post);
postClone.setTitle(
postClone.getTitle().replace("1st", "2nd")
);
entityManager.persist(postClone);
Ce que vous devez ajouter à l'entité Post
est un constructeur de copie:
/**
* Needed by Hibernate when hydrating the entity
* from the JDBC ResultSet
*/
private Post() {}
public Post(Post post) {
this.title = post.title;
addDetails(
new PostDetails(post.details)
);
tags.addAll(post.getTags());
}
C'est la meilleure façon de s'adresser à l'entité clone/problème de dédoublement. Toutes les autres méthodes, qui tentent de rendre ce processus complètement automatique, manquent le point que tous les attributs ne valent pas la peine d'être dupliqués.