2010-08-25 10 views
15

Ich habe mehrere gemappte Objekte in meiner JPA/Hibernate-Anwendung. Im Netzwerk erhalte ich Pakete, die Updates für diese Objekte darstellen, oder sie können tatsächlich neue Objekte darstellen.JPA - create-wenn-nicht-existiert-Einheit?

Ich möchte eine Methode, wie

<T> T getOrCreate(Class<T> klass, Object primaryKey) 

schreiben, die ein Objekt der bereitgestellten Klasse zurückgibt, wenn man in der Datenbank mit pk primaryKey existiert, und erzeugt sonst ein neues Objekt dieser Klasse, bleibt es und gibt es zurück.

Die nächste Sache, die ich mit dem Objekt tun werde, wird sein, alle seine Felder innerhalb einer Transaktion zu aktualisieren.

Gibt es einen idiomatischen Weg, dies in JPA zu tun, oder gibt es einen besseren Weg, um mein Problem zu lösen?

Antwort

0
  1. Eine EntityManager-Instanz (nennen wir es „em“), es sei denn, Sie bereits ein aktiver
  2. eine neue Transaktion erstellen haben (nennen wir es „tx“)
  3. Anruf em.find (Object pk)
  4. Anruf tx.begin()
    • Wenn find() eine nicht-null-Entity-Referenz zurückgegeben, dann müssen Sie ein Update tun. Wenden Sie Ihre Änderungen auf die zurückgegebene Entität an und rufen Sie dann em.merge (Object entity) auf.
    • Wenn find() eine Nullreferenz zurückgegeben hat, dann existiert diese PK nicht in der Datenbank. Erstellen Sie eine neue Entität und rufen Sie dann em.persist (Object newEntity) auf.
  5. Anruf em.flush()
  6. Anruf tx.commit()
  7. Ihre Entitätsverweis Return, pro Ihre Methodensignatur.
+9

Beachten Sie, dass dies wahrscheinlich nicht in mehreren Threads, wenn laufen arbeiten, da es eine Race-Bedingung in den Schritten 3) -7. – sleske

+2

und wie man dieses Problem in mehreren Threads überwinden? –

+0

@sleske Die Schritte 3-7 sind in die Transaktion eingebettet. Dieser Ansatz hilft, "Race Condition" zu verhindern -> http://stackoverflow.com/questions/6477574/do-database-transactions-prevent-race-conditions –

15

würde Ich mag eine Methode, wie <T> T getOrCreate(Class<T> klass, Object primaryKey)

Dies wird nicht einfach zu schreiben.

wäre ein naiver Ansatz so etwas wie diese (vorausgesetzt, das Verfahren läuft innerhalb einer Transaktion) zu tun:

public <T> T findOrCreate(Class<T> entityClass, Object primaryKey) { 
    T entity = em.find(entityClass, primaryKey); 
    if (entity != null) { 
     return entity; 
    } else { 
     try { 
      entity = entityClass.newInstance(); 
      /* use more reflection to set the pk (probably need a base entity) */ 
      return entity; 
     } catch (Exception e) { 
      throw new RuntimeException(e); 
     } 
    } 
} 

Aber in einer Umgebung mit gemeinsamer Zugriff, dieser Code aufgrund einer Race-Bedingung scheitern könnte:

 
T1: BEGIN TX; 
T2: BEGIN TX; 

T1: SELECT w/ id = 123; //returns null 
T2: SELECT w/ id = 123; //returns null 

T1: INSERT w/ id = 123; 
T1: COMMIT; //row inserted 

T2: INSERT w/ name = 123; 
T2: COMMIT; //constraint violation 

Und wenn Sie mehrere JVMs ausführen, wird die Synchronisierung nicht helfen. Und ohne eine Tischsperre zu bekommen (was ziemlich schrecklich ist), sehe ich nicht wirklich, wie Sie das lösen könnten.

In diesem Fall frage ich mich, ob es nicht besser wäre, zuerst systematisch einzufügen und eine mögliche Ausnahme zu behandeln, um eine nachfolgende Auswahl (in einer neuen Transaktion) durchzuführen.

Sie sollten wahrscheinlich einige Details zu den genannten Einschränkungen hinzufügen (Multithreading? Verteilte Umgebung?).

+1

Eine Antwort, die ich mir für den Multi-JVM-Fall vorgeschlagen hatte, ist die Verwendung kooperativer Advisory-Sperren für die DB (zB http://www.postgresql.org/docs/9.1/static/explicit) -locking.html). Sie sperren den gewünschten Primärschlüssel, führen dann die Überprüfung/Aktualisierung durch und geben dann die Sperre frei. –

+0

Das ist interessant, @ Mattr, obwohl ich nicht weiß, ob andere Datenbanken einen ähnlichen Mechanismus bieten. Es sieht nicht wie Oracle aus. – DavidS

4

Mit reinem JPA kann man dies optimistisch in einer Multithreaded-Lösung mit verschachtelten Entity Managern lösen (wirklich brauchen wir nur verschachtelte Transaktionen, aber ich glaube nicht, dass dies mit reinem JPA möglich ist). Im Wesentlichen muss man eine Mikrotransaktion erstellen, die die Find-or-Create-Operation kapselt. Diese Leistung wird nicht fantastisch sein und ist nicht für große Batch-Creates geeignet, sollte aber für die meisten Fälle ausreichen.

Voraussetzungen:

  • Das Unternehmen muss eine eindeutige Beschränkung verletzt haben, wenn zwei Instanzen
  • Sie haben eine Art von Finder erstellt wird scheitern an das Unternehmen zu finden (mit EntityManager von Primärschlüssel finden. Finden Sie oder durch einige Abfrage) wir werden dies als finder
  • Sie haben eine Art von Factory-Methode, um eine neue Entität zu erstellen, sollte die, die Sie suchen, nicht existieren, werden wir dies als factory bezeichnen.
  • Ich gehe davon aus, dass die angegebene findOrCreate-Methode für ein Repository-Objekt existieren würde und im Kontext eines bestehenden Entity-Managers und einer bestehenden Transaktion aufgerufen wird.
  • Wenn die Transaktionsisolationsstufe serialisierbar oder ein Snapshot ist, funktioniert dies nicht. Wenn die Transaktion wiederholbar gelesen wird, dann dürfen Sie nicht versucht haben, die Entität in der aktuellen Transaktion zu lesen.
  • Ich würde empfehlen, die unten stehende Logik in mehrere Wartungsmethoden zu zerlegen.

Code:

public <T> T findOrCreate(Supplier<T> finder, Supplier<T> factory) { 
    EntityManager innerEntityManager = entityManagerFactory.createEntityManager(); 
    innerEntityManager.getTransaction().begin(); 
    try { 
     //Try the naive find-or-create in our inner entity manager 
     if(finder.get() == null) { 
      T newInstance = factory.get(); 
      innerEntityManager.persist(newInstance); 
     } 
     innerEntityManager.getTransaction().commit(); 
    } catch (PersistenceException ex) { 
     //This may be a unique constraint violation or it could be some 
     //other issue. We will attempt to determine which it is by trying 
     //to find the entity. Either way, our attempt failed and we 
     //roll back the tx. 
     innerEntityManager.getTransaction().rollback(); 
     T entity = finder.get(); 
     if(entity == null) { 
      //Must have been some other issue 
      throw ex; 
     } else { 
      //Either it was a unique constraint violation or we don't 
      //care because someone else has succeeded 
      return entity; 
     } 
    } catch (Throwable t) { 
     innerEntityManager.getTransaction().rollback(); 
     throw t; 
    } finally { 
     innerEntityManager.close(); 
    } 
    //If we didn't hit an exception then we successfully created it 
    //in the inner transaction. We now need to find the entity in 
    //our outer transaction. 
    return finder.get(); 
} 
+0

Um 'Finder'-Aufrufe um 50% zu reduzieren:' public static T findOrCreate (EntityManagerFactory emf, Lieferant finder, Lieferant factory) {EntityManager em = emf.createEntityManager(); T versuch1 = finder.get(); if (versuch1! = null) return versuch1; T erstellt = factory.get(); probiere {em.getTransaction(). begin(); em.persist (erstellt); em.getTransaction(). commit(); Rückgabe finder.get(); } catch (Ausnahme ex) {em.getTransaction(). rollback(); T pro test2 = finder.get(); if (versuch2! = null) return versuch2; Ex abwerfen; } catch (Throwable t) {em.getTransaction(). rollback(); werfen t; } endlich {em.schließen(); }} '' – krevelen

+0

@krevelen Das ist eine gute Idee (es vermeidet mehr als nur ein Lesen es vermeidet das Öffnen einer Transaktion, die erhebliche Leistungsvorteile bringen kann) und funktioniert, wenn Ihre Isolationsstufe Commit gelesen oder nicht Committed gelesen wird, aber mit wiederholbarem Lesen fehlschlägt (Standard in vielen Fällen). Obwohl ich glaube, dass selbst meine Vorgehensweise versagen würde, wenn die Isolationsstufe serialisierbar ist. – Pace