2015-06-12 7 views
5

Ich versuche, eine alte Anwendung umzufunktionieren, um EJB3 mit JPA zu verwenden.Wie schichte ich EJB3 und Servlets korrekt?

Wir haben zwei Client-Schichten (eine Servlet-Basis, einem nicht), das in eine Delegiertenschicht nennen beide, die eine EJB-Schicht aufruft, die wiederum eine DAO aufruft. Das EJB ist EJB2 (Bean-Managed Persistence), und das DAO verwendet handgenerierte SQL-Abfragen, die Transaktionen festschreiben und Verbindungen manuell schließen.

Ich möchte die EJB2 mit EJB3 ersetzen und alle DAO ändern JPA zu verwenden.

begann ich durch den Austausch des EJB2 Code mit einem EJB3 mit Container-gesteuerten Transaktionen ab. Da Hibernate Criteria so einfach ist, und kann der EntityManager injiziert wird, kann ich so etwas tun:

@Stateless 
public class NewSelfcareBean implements SelfcareTcApi { 

@PersistenceContext(unitName="core") 
EntityManager em; 

public BasicAccount getAccount(String id) { 
    Criteria crit = getCriteria(BasicAccount.class); 
    crit.add(Restrictions.eq("id", id)); 
    BasicAccount acc = (BasicAccount) crit.uniqueResult(); 
    } 
} 

Keine Notwendigkeit für eine separate DAO Schicht. Das Konto Objekt sieht ein bisschen wie folgt aus:

@Entity 
@Table(name="er_accounts") 
public class BasicAccount { 
    @OneToMany(mappedBy="account", fetch=FetchType.LAZY) 
    protected List<Subscription> subscriptions; 
} 

Aber in der Servlet-Schicht, wo ich die EJB rufen Sie das Konto-Objekt zu erhalten, habe ich eine Antwort bauen wollen, die vielleicht (oder auch nicht) umfassen Kind Abonnements von die BasicAccount:

die Servlet-Schicht sieht wie folgt aus:

ResponseBuilder rb; 

public void doPost(HttpServletRequest request, HttpServletResponse response) 
     throws ServletException, IOException { 
    ... 
    Account acc = getDelegateLayer().getAccount(); 
    rb.buildSubscriptionResponse(acc.getSubscriptions()); 

    ... 
} 

Offensichtlich ist dies nicht funktioniert, da die Transaktion und EntityManager haben durch die Zeit, die wir auf die Servlet-Schicht wieder geschlossen worden ist - ich LazyInitializationException bekommen .

So kann ich ein paar Optionen:

  1. ServletFilter Transaktionen manuell zu verwalten. Das bedeutet, dass ich die Vorteile von EJB-Container-verwalteten Transaktionen verliere, nicht wahr? Außerdem müsste ich einen anderen Filtermechanismus auf dem anderen Client implementieren (nicht am Ende der Welt).
  2. Verwenden Sie stattdessen eine Stateful Session Bean, dann kann ich erweiterten Persistenzkontext verwenden. Aber ich brauche keine Stateful Bean, da zwischen den Transaktionen keine Daten gespeichert werden. Das würde den Server unnötig belasten und ich würde Stateful für etwas verwenden, für das es nicht wirklich entwickelt wurde.
  3. Anruf Hibernate.init(acc.getSubscriptions()) - dies funktioniert, muss aber in der EJB getan werden. Angenommen, ich verwende die Bean für eine andere Client-Methode, die die Subskriptionen nicht benötigt? Ein unnötiger DB-Aufruf.
  4. Verwenden Sie EAGER FetchType auf meinem Kontoobjekt. Schlecht für die Leistung und erstellt unnötige DB-Last.

Keine dieser Optionen scheint etwas Gutes.

Fehle ich etwas? Wie soll das gemacht werden? Ich kann nicht die erste Person sein, um dieses Problem zu haben ...

+1

Abfrage einer Liste von Abonnements, die einem Konto zugeordnet sind, in einem separaten Aufruf an das jeweilige EJB (separate Transaktion). Sie können eine Subskriptionsliste, die auf einem Konto basiert, während einer Transaktion abrufen und von der EJB-Methode in einem separaten Aufruf zurückgeben. – Tiny

+0

Warum nicht zwei Methoden erstellen, eine für nur den BasicAccount, eine andere für BasicAccount mit seinem Subscribe prefetched? Lassen Sie Ihre Anwendung wählen Sie dann die, die es benötigt – Chris

+0

@Chris, könnte dies funktionieren - ich könnte sogar nur eine Methode verwenden, aber eine boolesche Flag angeben, ob ich Abonnements will. Aber es hat einen schlechten Code-Geruch. Gibt es eine JPA, die Hibernate.initialize() entspricht? – mdarwin

Antwort

6

Zwei Abrufen Politik bedeutet zwei Anwendungsfällen, so sind Sie besser schreiben zwei Methoden in diesem Fall:

public BasicAccount getAccount(String id) { 
    Criteria crit = getCriteria(BasicAccount.class); 
    crit.add(Restrictions.eq("id", id)); 
    BasicAccount acc = (BasicAccount) crit.uniqueResult(); 
    } 
} 

public BasicAccount getAccountWithSubscriptions(String id) { 
    Criteria crit = getCriteria(BasicAccount.class); 
    crit.add(Restrictions.eq("id", id)); 
    crit.setFetchMode("subscriptions", FetchMode.JOIN); 
    BasicAccount acc = (BasicAccount) crit.uniqueResult(); 
    } 
} 

Eager fetching is most often a code smell und es ist die Service Layer (EJB) Verantwortung für das Abrufen der Daten. Wenn Sie feststellen, dass Sie die Webebene hacken, um Transaktionsverantwortlichkeit hinzuzufügen, ist dies ein Zeichen dafür, dass die Grenzen der Anwendungsschicht aufgebrochen werden.

Ein besserer Ansatz ist die Verwendung von DTOs. JPA-Entitäten sind Persistenz-bezogen und sie verlieren die Datenbank und die ORM-spezifischen Abruf-Abrufmechanismen. Ein DTO passt viel besser, da die Menge der Daten, die abgerufen und an die Web-Schicht gesendet werden, minimiert werden kann. Dies ist ideal für das Rendern einer Ansicht. Obwohl Sie Entitäten sowohl in der Serviceebene als auch in der Webebene praktisch verwenden können, gibt es Anwendungsfälle, in denen eine Datenprojektion eine bessere Alternative darstellt.

+2

Große Erklärung. Liebe, dass Sie zusätzliche Informationen in der Antwort auf "warum" enthalten. – Rentius2407

+0

Die Anwendung ist 12 Jahre alt und insgesamt ein Durcheinander, so dass ich zögern, noch eine weitere Ebene des Codes für DTO hinzuzufügen. Mir gefällt jedoch die Idee, setFetchMode zu verwenden, um das Laden von EAGER in einen LAZY-Join zu erzwingen. Wir haben das getestet und es funktioniert. Vielen Dank! – mdarwin

+0

Das Problem ist, dass es nur funktioniert, wenn Sie nur eine Sammlung haben. Wenn Sie mehr als eine Sammlung haben, erhalten Sie org.hibernate.loader.MultipleBagFetchException: kann nicht mehrere Taschen gleichzeitig abrufen – mdarwin

1

Dies ist ein sehr typisches Problem mit EAGER/LAZY holen.

Lösung 1 (nicht die beste): Erstellen Sie zwei Methoden, eine getBasicAccountWithSubscription() und eine getBasicAccount(), wie Vlad sagte. Das ist Code Geruch IMHO. Stellen Sie sich vor, Sie haben 10 andere solche Beziehungen. Das Erstellen einer solchen Methode für jede mögliche Kombination erzeugt 2^10 (exponentielle) neue Methoden (wie getBasicAccountWithSubsciptionWithoutLastLoginsWithTelephones...()), was nicht gut ist. Natürlich könnten Sie eine Methode mit 10 booleschen Parametern generieren, die angeben, welche Beziehung abgerufen werden soll, aber die Methode mischt in diesem Fall DB-Zeug (welche Beziehung zu laden) mit Geschäftslogikparametern (ID des BasicAccounts in Ihrem Fall).

Lösung 2 (die beste IMHO): Erstellen Sie eine andere Methode in Ihrem EJB: List<Subscription> getSubscriptionsForAccount(Long accountId); (mit der Eigenschaft wird faul abgerufen). Rufen Sie die neue Methode auf, für die Sie die Abonnements eines Kontos benötigen. Um sicherzustellen, dass niemand Ihre BasicAccount.getSubscriptions() Methode aufruft, können Sie es privat machen. Wenn Sie 10 Beziehungen hätten, würden Sie für jede Beziehung eine neue Methode anlegen (also die Anzahl der neuen Methoden). Wenn Sie beispielsweise eine andere Beziehung private List<Login> logins; in Ihrer Entität BasicAccount hätten, würden Sie dem EJB-Service public List<Login> getLoginsForAccount(Long accountId) eine weitere Methode hinzufügen.

+0

Hallo, ich bin nicht klar, wie Lösung 2 funktionieren soll. Eine Problemumgehung für das Problem mit Lösung 1 besteht auch darin, eine Map oder Liste von Booleschen Strings zu übergeben, die die erforderlichen Joins - z. B. "subscriptions" - darstellen: true, "transactions": false. Also erzeugen wir nur eine zusätzliche Methode für eine beliebige Anzahl von Beziehungen. – mdarwin

+0

Ihr Workaround für das Problem ist immer noch nicht OK wegen des gleichen Problems: Sie mischen Parameter, die Geschäftslogik darstellen (Parameter 'Long accountId') mit Parametern, die sagen, wie man Dinge aus DB holt, was ein schlechtes Design ist. Ich habe meine Antwort mit weiteren Details bearbeitet. Hilft es jetzt? Machen Sie eine Rest-Schnittstelle oder verwenden Sie die Subskriptionen in einer JSP? –

+0

Ja das ist jetzt klarer.Für mich enthält die Problemumgehung einfach die Felder, die Sie ausfüllen möchten, ähnlich wie bei einem Abrufplan. Option 2 würde zu mehr DB-Anrufen führen, wenn ich zuerst das Konto, dann die Abonnements, dann die Anmeldungen usw. erhalten müsste. Schneller, alle mit Joins in einem Anruf zu erledigen. – mdarwin