Montag, 8. Februar 2010

JPA: NamedQueries contra IDE

Es ist schön, dass JavaEE heute nicht mehr bedeuten muss, jede Menge XML und Java-Code synchron zu halten. Dank Annotations konnte dieses Wartbarkeitsdesaster deutlich reduziert werden. Trotzdem habe ich oftmals noch nicht das Gefühl, dass JavaEE weit genug geht. Immer noch gibt es Stellen, die bei Änderungen Probleme machen. Warum gibt man dem Compiler keine Chance?

Ein schönes (bzw. ärgerliches) Beispiel für eine solche Situation stellen die NamedQueries von JPA dar. Man darf heute Annotations schreiben statt XML, aber eine Refactoring-Unterstützung durch den Compiler ist immer noch nicht gegeben. Nehmen wir mal folgende Entity:


   @NamedQueries({
      @NamedQuery(name="selectAll",
                  query="select e from Employee e"),
      @NamedQuery(name="selectNames",
                  query="select e.name from Employee e")
   })
   @Entity
   public class Employee { ... }



Das Problem liegt offensichtlich darin, dass wir keinerlei Compiler-Unterstützung hinsichtlich der Query-Namen erwarten können:


   em.createNamedQuery("selectQll");


... ergibt keine Compiletime-Fehlermeldung. Noch dazu: Die Bezeichnung "selectAll" liegt in einem globalen Namespace. Wir müssen also überhaupt erst die Entity finden, bei der die NamedQuery definiert ist. Man nennt die Query also vielleicht besser "employee.selectAll". Dann sollte man allerdings nicht vergessen, das per Hand zu ändern, falls die Klasse Employee irgendwann umbenannt wird. Oh je.

Aus meiner Sicht hätte es hier eine Reihe von Möglichkeiten gegeben, den Compiler einzuspannen. Zum Beispiel so:


  public enum EmployeeQueries {
/** Selects all employees */
@EnumQuery(query="select e from Employee e")
    SELECT_ALL,
/** Selects the names of all employees */
@EnumQuery(query="select e.name from Employee e")
    SELECT_NAMES
};

Wobei JPA etwa folgende Annotation bereitstellen könnte:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public abstract @interface EnumQuery {
public abstract String query();
  ...
};


Die enum "EmployeeQueries" darf hierbei eine Top-Level-Klasse sein, oder auch innerhalb einer Entity definiert werden.

Vorteile wären:

  • Compiletime-Überprüfung
  • Automatisches Refactoring
  • Automatische Ergänzung (z. B. in Eclipse nach Eintippen von "EmployeeQueries." Damit erhält man erstens eine Auflistung der verfügbaren Queries, und zweitens eine direkte Einblendung der Javadoc pro Query.)


Ich persönlich glaube, dass solche Tool-Unterstützung nicht nur die Produktivität erhöht, sondern vor allen Dingen die Fehlerwahrscheinlichkeit deutlich senkt. Dazu muss man einmal die Anzahl der Schritte vergleichen, die ein Entwickler in beiden Fällen durchführen muss:

  • JPA NamedQueries: 
  1. createNamedQuery() schreiben
  2. Entity-Klasse suchen
  3. Query in der Entity-Klasse suchen
  4. Javadoc zur Query suchen (falls es überhaupt eine gibt - denn die Queries sind schließlich keine Java-Elemente, sondern nur als Annotations vorhanden - also falls ja, dann befindet sie sich wohl irgendwo in der Javadoc für die gesamte Entity-Klasse)
  5. Zurück zur richtigen Query
  6. Namensattribut kopieren
  7. Zurück zu createNamedQuery(...) (wo war das jetzt nochmal?)
  8. Namen einfügen
  9. Mit dem weitermachen, was man sich überlegt hatte
  • EnumQuery (oder ähnliche Lösungen):
  1. createNamedQuery() schreiben
  2. Mit den Pfeiltasten eine Query selektieren, dabei Javadoc direkt ablesen
  3. Weitermachen
Es könnte so schön sein (und es wär doch gar nicht soo schwierig gewesen, oder?)

4 Kommentare:

  1. Hallo Chris,
    rund um den Namen der Query, der sich in deinem Beispiel auch nicht refactoren lässt, hab ich mir folgendes bei der Erstellung angewöhnt:


    @NamedQueries( {
    @NamedQuery(name = Printing.findAllByNotBuild, query = "SELECT p FROM printing p WHERE p.pdfAvailable = false ORDER BY p.id DESC") })
    @Entity
    public class Printing {
    public final static String findAllByNotBuild = "lu.arisa.printing.model.findAllNotBuild";
    ...

    eine Suche schaut dann wie folgt aus:

    em.createNamedQuery(Printing.findAllByNotBuild, Printing.class).setParameter(...

    Grüße, Matthias

    AntwortenLöschen
  2. Hallo Matthias,

    ich finde, das ist ein sehr gutes Pattern, das unbedingt ins Repertoire eines jeden JPA-Entwicklers gehört. Der kleine, aber feine Unterschied zu den (hypothetischen) EnumQueries wäre jedoch noch der, dass man den Begriff "printing" nicht im Namen unterbringen müsste. Damit benötigt es dann auch keine Änderung eines Strings, wenn die Klasse Printing umbenannt wird, oder wenn die Query in eine andere Klasse verschoben wird.

    Auch kann es dann nicht passieren, dass man die Query verschiebt, aber vergisst, den String ebenfalls zu verschieben.

    Dennoch: Solange es keine EnumQueries gibt, ist dein Vorschlag die beste Lösung.

    Grüße

    Chris

    AntwortenLöschen
  3. Hallo Matthias,

    ich befasse mich jetzt auch mit der JPA (ich bin noch Anfänger). Ich verstehe nicht das Statement:

    public final static String findAllByNotBuild = "lu.arisa.printing.model.findAllNotBuild";

    Kannst Du bitte kurz erläutern, wie das funktioniert.

    Gruß
    Dieter

    AntwortenLöschen
  4. Eigentlich Java alle NamedQuery Befehle vorkompiliert, und die schneller sind ...

    AntwortenLöschen