Morphia - POJO-Persistenz mit mongoDB - JAXenter
POJO-Persistenz nach JPA-Art mit mongoDB

Morphia – POJO-Persistenz mit mongoDB

Uwe Schäfer

mongoDB hat aufgrund seiner Besonderheiten innerhalb der noSQL-Familie gezeigt, dass es nicht nur für spezielle Anwendungsfälle geeignet ist, sondern eine Vielzahl solcher Applikationen bedienen kann, die bisher mithilfe von relationalen Datenbanken erstellt wurden. Allerdings ist die direkte Schnittstelle zu Java, der mongoDB-Java-Treiber, für den JPA-verwöhnten Anwendungsentwickler nicht unbedingt attraktiver als JDBC. Morphia versucht diese Lücke zu schließen und einen Teil der von JPA bekannten Techniken aufzugreifen, ohne deren Komplexität zu erben.

Die Vertreter der noSQL-Familie, falls eine Gruppierung dieser Art überhaupt Sinn macht, könnte unterschiedlicher nicht sein. Während typische Key-Value-Datenbanken, Graphdatenbanken und Map-Reduce-basierte Systeme eher Probleme zu lösen, für die relationale Datenbanken ungeeignet erscheinen, bietet mongoDB eine Reihe von Eigenschaften, die bei grober Betrachtung denen einer relationalen Datenbank ähnlich sind. Neben horizontaler Skalierbarkeit, einfacher Administration und Replikation erfreut mongoDB als dokumentbasierte Datenbank den Umsteiger mit sekundären Indexen und spontanen Abfragen in einer Sprache, die dem Datenbankadministrator nicht gleich das Blut gefrieren lässt. Eine Einführung in mongoDB bietet der gute Artikel aus dem Java Magazin 7/10.

Was liegt also näher, als mongoDB ähnlich einzusetzen, wie man es bisher von der JPA gewohnt ist? Morphia versucht deshalb, das für den mongoDB-java-Treiber zu sein, was JPA für JDBC darstellt. Da es sich aber bei mongoDB nicht um eine relationale Datenbank handelt, haben sich die Entwickler dagegen entschieden, die JPA-Spezifikation zu implementieren. Stattdessen bedient sich Morphia der Konzepte der JPA, stellt aber ein speziell auf dem mongoDB-Java-Treiber basierendes, nicht portierbares API dar.

Initialisierung

Nach der Erzeugung einer Morphia-Instanz können ihr mittels der Methoden mapClass und mapPackage Entitätenklassen bekannt gemacht werden. Dies ist zwar streng genommen nicht notwendig – Morphia stellt ein Mapping auch zur Laufzeit her, wenn notwendig – hat aber den Vorteil, dass auftretende Mapping-Fehler nicht erst mitten in der Anwendung erkannt werden. Üblicherweise existiert eine Morphia-Instanz pro Anwendung. Morphia kommuniziert mit mongoDB über den mongoDB-Java-Treiber. Daher wird zur Erzeugung eines Datastore eine Instanz der Mongo-Klasse benötigt.

// Beispiel 1: Persistenz ohne Morphia-eigene Annotationen
public class Person {
  protected Person() {}
  private String firstName, lastName;
  private List phoneNumbers = new LinkedList();

  public Person(String firstName, String lastName) {
    this.firstName=firstName;this.lastName=lastName;
  }	
  public List getPhoneNumbers() {
    return phoneNumbers;
  }
  // ...
}
public class PhoneNumber {
  protected PhoneNumber() {}
  private int prefix, number;
  public PhoneNumber(int prefix, int number) {
    this.prefix = prefix; this.number = number;
  }
  // ...
}
public class PersonTest {
  public static void main(String[] args) throws Exception {
    Person bob = new Person("Bob", "Lee");
    bob.getPhoneNumbers().add(new PhoneNumber(1234, 5678));

    Datastore ds = new Morphia().createDatastore(new Mongo(), "testdb");
    ds.save(bob);
  }
}

/* erzeugt:
 *
 * db.Person.find();
 *
 * { "_id" : ObjectId("4c90bacfeba0d5a67db9eff2"), 
 *   "className" : "example.pojo.Person",
 *   "firstName" : "Bob", 
 *   "lastName" : "Lee",
 *   "phoneNumbers" : [  
 *      { "prefix" : 1234, "number" : 5678 }  
 *   ] 
 * }
 */
Datastore API

Morphia bietet dem Anwendungsentwickler zur Interaktion die Schnittstelle Datastore. Diese bietet unter anderem Methoden zum Laden, Speichern und Aktualisieren von Entitäten sowie der Erzeugung von Query-Objekten und Indizes. Auch diese Instanz kann durch die Anwendung hindurch benutzt werden und wird oft als Singleton innerhalb des Anwendungskontexts (Spring, Guice) ausgelegt. Ein definierter Lebenszyklus des Datastore ist nicht notwendig, da mit dem Datastore im Gegensatz zu einem JPA-EntityManager kein First-Level-Cache verbunden ist. Neben dem Datastore existiert auch ein DAO-API, das aber auf dem Datastore aufsetzt.

POJO Persistenz

Morphia persistert POJOs; auch solche, die von Morphia nichts wissen. Morphia versucht in diesem Fall, ein Default-Mapping zu finden. Allein ein Kriterium müssen die beteiligten Klassen zurzeit noch erfüllen: Sie müssen einen Default-Konstruktor besitzen. Ein Anwendungsszenario findet sich da, wo Objekte gespeichert werden sollen, deren Quellcode nicht verändert werden kann/soll, zum Beispiel bei Objekten aus fremden Bibliotheken. Beispiel 1 zeigt das Speichern eines simplen Beans ohne Morphia-eigene Annotationen. Ein Blick in die Datenbank offenbart, dass das resultierende Dokument zusätzlich, zu den erwarteten Daten noch die Attribute _id und className bekommen hat. Der Klassenname wird von Morphia immer da eingefügt, wo theoretisch auch Subklassen verwendet werden können, sodass Morphia beim Lesen aus der Datenbank die ursprünglich verwendeten Objektklassen benutzt. Falls die _id noch nicht im Objekt vorhanden ist, wird sie vom Datenbankserver erzeugt. Allerdings entgeht dem Benutzer so die Möglichkeit ein Objekt in der Datenbank via Morphia zu identifizieren und somit auch zu aktualisieren, da das Person-Objekt die vom Server vergebene ID nicht enthält.

Mapping-Annotationen einer Entitätenklasse

So nützlich sich die Möglichkeit der Speicherung von nichtannotierten Klassen in speziellen Fällen auch sein mag: Der üblichere Weg ist der, dedizierte Entitätenklassen zu definieren, wie sie von der JPA bekannt sind. Beispiel 2 demonstriert die wichtigsten Annotationen anhand einer Blogdatenstruktur.

@Entity definiert den von einer Klasse zu verwendenden Kollektionsnamen. Diese Annotation kommt damit am ehesten der von JPA bekannten @Table-Annotation gleich. Es wurde weiterhin ein Feld id hinzugefügt, das mit @Id annotiert wurde. Dieses Feld dient also der Identifikation des dem Objekt entsprechenden Dokuments in der Datenbank und muss daher einen eindeutigen Wert besitzen. Es bietet sich hier an, den von mongoDB bekannten Typ ObjectID zu verwenden, da dieser ähnlich der UUID eine globale Eindeutigkeit verspricht, aber mit nur vier Byte deutlich kürzer als eine solche ist. Sie können allerdings auch long oder ähnliche Datentypen verwenden, müssen sich dann aber um die Vergabe und Eindeutigkeit der Werte selbst kümmern. Bei allen anderen Feldern kann eine der Annotationen @Property, @Embedded, @Serialized, @Reference oder @Transient verwendet werden.

Felder, die mit @Transient annotiert sind, werden beim Speichern und Laden eines Objekts ignoriert. Alle anderen genannten Annotationen haben gemein, dass mittels value der Attributname im Dokument festgelegt werden kann. Geschieht dies nicht, wird standardmäßig der Feldname verwendet.

Die Annotation @Property definiert, dass das Feld direkt, also nicht durch Einbettung, zu konvertieren ist. Dieses Vorgehen ist für durch den Treiber nativ konvertierbare Werte (primitives, String, byte[] etc.) oder für solche Klassen möglich, die mittels eines eigens dafür registrierten Konverters (s. u.) direkt übersetzbar sind.

@Embedded hingegen definiert, dass ein Objekt dadurch persistiert wird, dass Morphia versucht, dessen Status wiederum aus seinen (nicht als @Transient markierten) Feldern zu ermitteln. Zusätzlich wird ein Attribute className eingefügt, falls der zu speichernde Wert nicht der Felddeklaration entspricht, sondern die Instanz eines Subtyps ist (z. B. @Embedded Color c = SystemColor.desktop; ).

@Serialized hingegen wird verwendet, um Morphia anzuweisen, den Wert des Felds zu serialisieren. Hierbei ist darauf zu achten, dass das resultierende Byte-Array nicht allzu groß wird, da ja mongoDB die maximale Dokumentengröße auf vier Megabyte begrenzt. Aus diesem Grund wird das Ergebnis der Serialisierung standardmäßig komprimiert. Eine Erweiterung dieser Funktionalität unter Verwendung des GridFS ist in Planung, sodass beliebig große Datenmengen auf diese Art gespeichert werden können.

// Beispiel 2: Exemplarische Datenstruktur Blog
@Entity("BlogArticles")
public class Article {
  @Id	     	private ObjectId id;
  @Reference 	private Author author;
  @Property  	private String title;
  @Property  	private String text;
  @Embedded  	private List comments;
  @Embedded("meta") 
             	private MetaData metaData;
  // ...
}
@Entity("Author")
public class Author {
  @Id           private ObjectId id;
  @Property 	private String email;
  @Property @Indexed 	
                private String name;
  @Serialized 	private Image image;
  // ...
}
public class Comment {
  private State state=State.HOLD;
  private Date created = new Date();
  private String text;
  // Enums werden durch ihren Namen im Dokument dargestellt
  public static enum State{ HOLD, PUBLISHED }
  // ...
}
public class MetaData {
  private int stars = 0;
  private long pageViews = 0;
  // ...
}

Ist ein Feld mittels @Reference annotiert, so wird der Wert des Felds als eigene Entität betrachtet und in diesem Dokument lediglich eine Referenz darauf gespeichert. Handelt es sich bei dem Typ des Feldes um eine Collection, eine Map oder ein Array, so werden stattdessen die entsprechenden Elemente als Referenzen interpretiert. So können Relationen zwischen Dokumenten in Java auf natürliche Weise abgebildet werden. Allerdings gibt es bei der Verwendung von @Reference einiges zu beachten:

  • Ein referenziertes Objekt muss zum Zeitpunkt des Speicherns des referenzierenden Objekts bereits eine ID besitzen, sonst wird es ignoriert
  • Wird ein Objekt mit einer Referenz geladen, so wird standardmäßig auch das referenzierte Objekt geladen (siehe Attribut lazy)
  • Existiert ein referenziertes Objekt beim Lesen nicht mehr in der Datenbank, wird standardmäßig eine Exception erzeugt (siehe Attribut ignoreMissing)
  • Bei Verwendung einer Map ist als Schlüsseltyp ein als Property zu konvertierender Datentyp zu wählen.

Fehlen all diese Annotationen bei einem Feld, wird Morphia versuchen den Wert des Felds zu konvertieren, oder falls diese Konversion nicht möglich ist, da es sich beispielsweise um ein Bean wie in unserem Fall Comment handelt, diesen einzubetten. Es gibt sowohl für das Einbetten als auch für die Verwendung von Referenzen gute Anwendungsfälle. Zu diesem Thema sei auf das mongoDB-Wiki bzw. das kürzlich erschienene Buch „MongoDB – The Definitive Guide“ verwiesen. Die Dokumentation weiterer Mapping-Annotationen wie @AlsoLoad, @NotSaved, etc. würde den Rahmen des Artikels sprengen. Sie finden diese auf der Morphia-Website.

// Beispiel 3: Automatische Validierung vor dem Speichern via JSR303
public class MorphiaValidation extends AbstractEntityInterceptor {
  private ValidatorFactory validationFactory;
  public MorphiaValidation(Morphia m) {
    this.validationFactory = Validation.byDefaultProvider()
      .configure().buildValidatorFactory();
    m.getMapper().addInterceptor(this);
  }
  @Override
  public void prePersist(final Object ent, final DBObject dbObj, 
                         final Mapper mapr) {
    Set failures = validationFactory.getValidator().validate(ent);
    if (!failures.isEmpty()) {
      // throw new RuntimeException(...
    }
  }
}
Versionsnummern

Ein weiteres von der JPA entliehenes Konzept ist die Erkennung konkurrierender Änderungen. Hierzu wird eine Entitätenklasse um eine Versionsfeld erweitert: @Version private long version;. Wird dann ein Objekt dieser Klasse gespeichert, wird die persistente Versionsnummer mit der zu speichernden verglichen. Nur wenn beide gleich sind, wird die Versionsnummer erhöht und das Objekt gespeichert und andernfalls eine ConcurrentModificationException erzeugt. Auf diese Weise kann ein optimistisches Sperren realisiert werden.

Geschrieben von
Uwe Schäfer
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.