web-dev-qa-db-fra.com

JPA avec JTA: conserver l'entité et fusionner les entités enfants en cascade

J'ai une relation un-à-plusieurs bidirectionnelle avec les classes d'entités suivantes:

ou 1 client <-> 0 ou plusieurs commandes de produits

Lors de la persistance de l'entité client, je souhaite que les entités de commande de produit associées soient également conservées (car leur clé étrangère vers le client "parent" peut avoir été mise à jour).

Bien sûr, toutes les options CASCADE requises sont définies côté client. Mais cela ne fonctionne pas si un client nouvellement créé persiste pour la première fois lors du référencement d'une commande de produit existante comme dans ce scénario:

  1. la commande de produit "1" est créée et conservée. Fonctionne bien.
  2. le client "2" est créé et la commande de produits "1" est ajoutée à sa liste de commandes de produits. Ensuite, il persiste. Ne marche pas.

J'ai essayé plusieurs apporaches, mais aucune n'a montré le résultat attendu. Voir ces résultats ci-dessous. J'ai lu toutes les questions connexes ici, mais elles ne m'ont pas aidé. J'utilise EclipseLink 2.3.0, des annotations JPA 2.0 pures et JTA comme type de transaction sur une base de données en mémoire Apache Derby (JavaDB) sur GlassFish 3.1.2. Les relations d'entité sont gérées par une interface graphique JSF. La gestion des relations au niveau de l'objet fonctionne (à part persister), je l'ai testée avec des tests JUnit.

Approche 1) "Default" (basé sur les modèles de classe NetBeans)

Client:

@Entity
public class Client implements Serializable, ParentEntity {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToMany(mappedBy = "client", cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH},
            fetch= FetchType.LAZY)
    private List<ProductOrder> orders = new ArrayList<>();

    // other fields, getters and setters
}

Commande de produit:

@Entity
public class ProductOrder implements Serializable, ChildEntity {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne // owning side
    private Client client;

    // other fields, getters and setters
}

Façade de persistance générique:

// Called when pressing "save" on the "create new..." JSF page
public void create(T entity) {
    getEntityManager().persist(entity);
}

// Called when pressing "save" on the "edit..." JSF page
public void edit(T entity) {
    getEntityManager().merge(entity);
}

Résultat:

create () lève immédiatement cette exception:

Avertissement: Une exception système s'est produite lors d'un appel sur la méthode EJB ClientFacade public void javaee6test.beans.AbstractFacade.create (Java.lang.Object) javax.ejb.EJBException: Transaction abandonnée ...

Causée par: javax.transaction.RollbackException: transaction marquée pour l'annulation. ...

Causée par: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.3.0.v20110604-r9504): org.Eclipse.persistence.exceptions.DatabaseException Exception interne: Java.sql.SQLIntegrityConstraintViolationException: la déclaration a été abandonnée car elle entraînerait ont provoqué une valeur de clé en double dans une contrainte de clé unique ou primaire ou un index unique identifié par 'SQL120513133540930' défini sur 'PRODUCTORDER'. Code d'erreur: -1 Appel: INSÉRER DANS LES VALEURS DU PRODUIT (ID, CLIENT_ID) (?,?) Bind => [2 paramètres liés] Requête: InsertObjectQuery (javaee6test.model.ProductOrder [id = 1]) ...

Causée par: Java.sql.SQLIntegrityConstraintViolationException: l'instruction a été abandonnée car elle aurait provoqué une valeur de clé en double dans une contrainte de clé unique ou primaire ou un index unique identifié par 'SQL120513133540930' défini sur 'PRO-DUCTORDER'. ...

Causée par: org.Apache.derby.client.am.SqlException: l'instruction a été abandonnée car elle aurait provoqué une valeur de clé en double dans une contrainte de clé unique ou primaire ou un index unique identifié par 'SQL120513133540930' défini sur ' PRODUCTOR-DER '.

Je ne comprends pas cette exception. edit () fonctionne très bien. MAIS je voudrais ajouter des commandes de produits à un client au moment de sa création, c'est donc insuffisant.

Approche 2) fusionner () uniquement

Modifications apportées à la façade de persistance générique:

// Called when pressing "save" on the "create new..." JSF page
public void create(T entity) {
    getEntityManager().merge(entity);
}

// Called when pressing "save" on the "edit..." JSF page
public void edit(T entity) {
    getEntityManager().merge(entity);
}

Résultat:

Sur create (), la sortie de journalisation EclipseLink indique:

Très bien: INSÉRER DANS LE CLIENT (ID, NOM, ADRESSE_ID) VALEURS (?,?,?) Bind => [3 paramètres liés]

mais PAS de "MISE À JOUR" sur le tableau de commande des produits. Ainsi, la relation n'est pas établie. Encore une fois, edit (), d'autre part, fonctionne très bien.

Apporach 3) Id GenerationType.IDENTITY sur les deux types d'entités

Modifications de la classe de commande du client et du produit:

...
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...

Résultat:

Sur create (), la sortie de journalisation EclipseLink indique:

Très bien: INSÉRER DANS LES VALEURS CLIENT (NOM, ADRESSE_ID) (?,?) Bind => [2 paramètres liés]

Fine: valeurs IDENTITY_VAL_LOCAL ()

Amende: INSÉRER DANS LES VALEURS DU PRODUIT (ORDERDATE, CLIENT_ID) (?,?) Bind => [2 paramètres liés]

Fine: valeurs IDENTITY_VAL_LOCAL ()

ainsi, au lieu d'établir une relation avec la commande de produit ajoutée à la liste du client, une nouvelle entité de commande de produit est créée et persistée (!) et une relation avec cette entité est établie. Même chose ici, edit () fonctionne très bien.

Apporach 4) Approche (2) et (3) combinées

Résultat: identique à l'approche (2).

Ma question est: Existe-t-il un moyen de réaliser le scénario décrit ci-dessus? Comment peut-il être archivé? J'aimerais rester avec JPA (pas de solution spécifique au fournisseur).

19
SputNick

Salut j'ai eu le même problème aujourd'hui, je demande à la liste de diffusion openJPA avec cet email:

Salut. J'ai un problème avec l'insertion et la mise à jour d'une référence dans la même entité.

Im essayant d'insérer un nouvel objet (examen) qui a une référence à un autre objet (personne) et en même temps je veux mettre à jour un attribut (birthDate) de l'objet personne. La mise à jour ne se produit jamais même si j'ai défini CascadeType sur ALL. La seule façon dont cela fonctionne est de faire une opération de persistance et ensuite une opération de fusion. Est-ce normal? Dois-je changer quelque chose ??

Je n'aime pas l'idée d'une "mise à jour manuelle" utilisant la fusion dans l'objet Personne car je ne sais pas combien d'objets (objet enfant d'examen) l'utilisateur veut mettre à jour.

Entités:

public class Exam{  
   @ManyToOne(cascade= CascadeType.ALL)
   @JoinColumn(name = "person_id")
   public Person person;
......
}

public class Person{
    private Date birthDate;
   @OneToMany(mappedBy = "person")
    private List<Exam> exams
.......
}

public class SomeClass{
   public void someMethod(){
      exam = new Exam()
      person.setBirthDate(new Date());
      exam.setPerson(person); 
      someEJB.saveExam(exam);
   }
}

public class someEJB(){

   public void saveExam(Exam exam){
        ejbContext.getUserTransaction().begin();
        em.persist(exam);
        //THIS WORKS
        em.merge(exam.getPerson());
        ejbContext.getUserTransaction().commit();       
   }

}

Dois-je utiliser la méthode MERGE pour chaque objet enfant?

Et la réponse était la suivante:

Il semble que votre problème soit que l'examen est nouveau, mais la personne existe et l'entité existante est ignorée lors de la cascade de l'opération persistante. Je crois que cela fonctionne comme prévu.

Tant que vos relations sont définies sur CascadeType.ALL, vous pouvez toujours modifier votre em.persist (examen); à em.merge (examen) ;. Cela permettrait de conserver le nouvel examen et de répercuter l'appel de fusion vers la personne.

Merci, Rick


J'espère que cela pourra vous aider.

7
maxtorzito

@SputNick, je l'ai résolu par les étapes décrites ci-dessous. Je vais utiliser votre extrait de code pour montrer ce que j'ai fait,

Vous avez Entité client - @ OneToMany et Entité ProductOrder - @ ManyToOne. J'utiliserai

// Called when pressing "save" on the "edit..." 
public void edit(T entity) {
 getEntityManager().merge(entity);}

pour la persistance car il peut me donner la flexibilité de sauvegarder ainsi que de mettre à jour.

Pour créer Client et commandes de produits correspondantes utilisez

client.set..(some property to be saved);

productOrder.set..(some property to be saved);

// Définir productOrder pour le client

List<ProductOrder> order = new ArrayList<>();

client.setOrders(order); // Notez que cela attend une liste à passer

// définir le client pour productOrder

productOrder.setClient(productOrder);

.edit(client) or .edit(productOrder)

Notez que dans les deux cas, vous pourrez mettre à jour les deux tables d'entités avec vos modifications.

Bien que tard, j'espère que cela aidera quelqu'un

1
Fredrick Aponyo

Assurez-vous que vous définissez les deux côtés de la relation, vous ne pouvez pas simplement ajouter la commande au client, vous devez également définir le client de la commande. Vous devez également fusionner les deux objets que vous avez modifiés. Si vous venez de fusionner le client, le client de la commande ne sera pas fusionné (bien que votre cascade vous le fasse fusionner).

persist ne fonctionne pas, car persist exige que l'objet persistant soit correct pour le contexte de persistance, c'est-à-dire qu'il ne référence pas les objets détachés, il doit référencer les objets gérés.

Votre problème vient de votre détachement des objets. Si vous n'avez pas détaché les objets, vous n'auriez pas les mêmes problèmes. Normalement, dans JPA, vous allez créer un EntityManager, trouver/interroger vos objets, les modifier/les conserver, appeler commit. Aucune fusion n'est requise.

0
James

Utilisez l'annotation @Joincolumn dans votre entité ProductOrder, voir ci-dessous.

   @Entity
   public class ProductOrder implements Serializable, ChildEntity {
   private static final long serialVersionUID = 1L;

   @Id
   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;

   @ManyToOne // owning side
   @JoinColumn(name = "PRODUCTORDER_ID", referencedColumnName = "ID")
   //write your database column names into name and referencedColumnName attributes.
   private Client client;

   // other fields, getters and setters
   }
0