J'ai un modèle objet à persistance JPA qui contient une relation plusieurs-à-un: un compte comporte de nombreuses transactions. Une transaction a un compte.
Voici un extrait du 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;
Je peux créer un objet Compte, y ajouter des transactions et conserver correctement l'objet Compte. Mais, lorsque je crée une transaction, en utilisant un compte existant déjà existant et en conservant la la transaction , je reçois une exception:
Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.paulsanwald.Account
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.Java:141)
Je peux donc conserver un compte contenant des transactions, mais pas une transaction comportant un compte. Je pensais que c'était parce que le compte n'était peut-être pas attaché, mais ce code me donne toujours la même 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);
Comment enregistrer correctement une transaction associée à un objet de compte déjà persistant?
C'est un problème typique de cohérence bidirectionnelle. Il est bien discuté dans ce lien ainsi que ce lien.
Conformément aux articles des 2 liens précédents, vous devez corriger vos paramètres dans les deux côtés de la relation bidirectionnelle. Un exemple de configurateur pour le côté Un se trouve dans ce lien.
Un exemple de paramètre pour le côté Plusieurs est dans ce lien.
Une fois que vous avez corrigé vos paramètres, vous souhaitez déclarer le type d'accès Entity à "Property". La meilleure pratique pour déclarer le type d'accès "Propriété" consiste à déplacer TOUTES les annotations des propriétés de membre vers les accesseurs correspondants. Il ne faut surtout pas mélanger les types d'accès "Field" et "Property" dans la classe d'entité, sinon le comportement n'est pas défini par les spécifications JSR-317.
La solution est simple, utilisez simplement le CascadeType.MERGE
au lieu de CascadeType.PERSIST
ou CascadeType.ALL
.
J'ai eu le même problème et CascadeType.MERGE
a fonctionné pour moi.
J'espère que vous êtes triés.
L'utilisation de la fusion est risquée et délicate. Il s'agit donc d'une solution de contournement peu efficace dans votre cas. Vous devez au moins vous rappeler que lorsque vous transmettez un objet d'entité à fusionner, il s'arrête est attaché à la transaction et à la place une nouvelle entité, désormais attachée, est renvoyée. Cela signifie que si quelqu'un a toujours l'ancien objet Entity en sa possession, les modifications apportées à celui-ci sont ignorées en silence et rejetées au commit.
Vous ne montrez pas le code complet ici, je ne peux donc pas vérifier votre schéma de transaction. Une façon de parvenir à une telle situation est de ne pas avoir de transaction active lors de l'exécution de la fusion et de la conserver. Dans ce cas, le fournisseur de persistance doit ouvrir une nouvelle transaction pour chaque opération JPA que vous effectuez, puis la valider et la fermer immédiatement avant le retour de l'appel. Si tel est le cas, la fusion est exécutée dans une première transaction, puis une fois que la méthode de fusion est renvoyée, la transaction est complétée et fermée et l'entité renvoyée est maintenant détachée. La persistance sous celle-ci ouvrirait alors une deuxième transaction et tenterait de faire référence à une entité détachée en donnant une exception. Enveloppez toujours votre code dans une transaction, sauf si vous savez très bien ce que vous faites.
En utilisant une transaction gérée par conteneur, cela ressemblerait à ceci. Remarque: cela suppose que la méthode est à l'intérieur d'un bean de session et appelée via une interface locale ou distante.
@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);
}
Probablement dans ce cas, vous avez obtenu votre objet account
à l'aide de la logique de fusion, et persist
est utilisé pour conserver de nouveaux objets et se plaindra si la hiérarchie a déjà un objet persistant. Vous devez utiliser saveOrUpdate
dans de tels cas, au lieu de persist
.
Ne passez pas id (pk) à la méthode persistante ou essayez la méthode save () au lieu de persist ().
Si rien ne vous aide et que vous obtenez toujours cette exception, passez en revue vos méthodes equals()
- et n'incluez pas de collection enfant dans celle-ci. Surtout si vous avez une structure profonde de collections incorporées (par exemple, A contient Bs, B contient Cs, etc.).
Dans l'exemple de 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!
}
}
Dans l'exemple ci-dessus, supprimez les transactions des contrôles equals()
. Cela est dû au fait que hibernate implique que vous n'essayez pas de mettre à jour un ancien objet, mais que vous passez un nouvel objet à conserver chaque fois que vous modifiez un élément de la collection enfant.
Bien entendu, ces solutions ne conviendront pas à toutes les applications et vous devez concevoir avec soin ce que vous souhaitez inclure dans les méthodes equals
et hashCode
.
Dans votre définition d'entité, vous ne spécifiez pas le @JoinColumn pour la Account
jointe à une Transaction
. Vous voudrez quelque chose comme ça:
@Entity
public class Transaction {
@ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
@JoinColumn(name = "accountId", referencedColumnName = "id")
private Account fromAccount;
}
EDIT: Eh bien, je suppose que cela serait utile si vous utilisiez l’annotation @Table
dans votre classe. Il h. :)
Vous devez définir la transaction pour chaque compte.
foreach(Account account : accounts){
account.setTransaction(transactionObj);
}
Ou bien, si cela est suffisant, vous pouvez définir des identifiants nuls sur plusieurs côtés.
// 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.
Tout d’abord, merci pour une très bonne description et un exemple illustrant le problème.
Considérez une solution simple: supprimez la cascade de l'entité enfant Transaction
, cela devrait simplement être:
@ManyToOne // no cascading here!
private Account fromAccount;
(FetchType.EAGER
peut être supprimé ainsi que par défaut pour @ManyToOne
)
C'est tout!
Pourquoi? En disant "tout en cascade" sur l'entité enfant, vous indiquez que chaque opération de base de données est propagée à l'entité parent. Si vous faites ensuite persist(transaction)
, persist(account)
sera également invoqué. Mais seules les entités (nouvelles) transitoires peuvent être passées à persist (), les entités détachées (transaction
dans ce cas, est déjà dans la base de données) ne le peuvent pas. Par conséquent, vous obtenez l'exception "entité séparée passée à persister".
En général, vous ne voulez pas propager d’enfant à parent. Malheureusement, il existe de nombreux exemples de code dans les livres (même les bons) et sur Internet, ce qui est exactement ce que vous faites. Je ne sais pas, pourquoi ... Peut-être parfois simplement copier encore et encore sans trop y penser ... Devinez ce qui se passe si vous _remove(transaction)
ayant toujours "cascade ALL" dans ce @ManyToOne? La account
(au fait, avec toutes les autres transactions) sera également supprimée de la base de données. Mais ce n'était pas votre intention, n'est-ce pas?
Peut-être qu’il s’agit du bogue d’OpenJPA. Lors d’une restauration, il réinitialise le champ @Version, mais le programme pcVersionInit reste à true. J'ai un AbstraceEntity qui a déclaré le champ @Version. Je peux contourner le problème en réinitialisant le champ pcVersionInit. Mais ce n'est pas une bonne idée. Je pense que cela ne fonctionne pas quand cascade persistent entité.
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;
}
Même si vos annotations sont déclarées correctement pour gérer correctement la relation un à plusieurs, vous pouvez toujours rencontrer cette exception précise. Lorsque vous ajoutez un nouvel objet enfant, Transaction
, à un modèle de données attaché, vous devez gérer la valeur de la clé primaire - sauf si vous n'êtes pas censé le faire . Si vous fournissez une valeur de clé primaire pour une entité enfant déclarée comme suit avant d'appeler persist(T)
, vous rencontrerez cette exception.
@Entity
public class Transaction {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
....
Dans ce cas, les annotations déclarent que la base de données gérera la génération des valeurs de clé primaire de l'entité lors de l'insertion. Le fait d'en fournir un vous-même (par exemple, par l'intermédiaire du paramètre de définition de l'identifiant) provoque cette exception.
Alternativement, mais en réalité identique, cette déclaration d’annotation entraîne la même exception:
@Entity
public class Transaction {
@Id
@org.hibernate.annotations.GenericGenerator(name="system-uuid", strategy="uuid")
@GeneratedValue(generator="system-uuid")
private Long id;
....
Donc, ne définissez pas la valeur id
dans votre code d'application alors qu'il est déjà géré.
Si les solutions ci-dessus ne fonctionnent pas, ne commentez qu'une seule fois les méthodes d'accès et de définition de la classe d'entité et ne définissez pas la valeur de id (clé primaire).
cascadeType.MERGE,fetch= FetchType.LAZY
Dans mon cas, je commettais une transaction lorsque la méthode persist - était utilisée. En changeant la méthode persist en save , le problème a été résolu.