Persistenz in der Google App Engine – Generische Repositories mit Objectify

Die Google App Engine ist ein Platform-as-a-Service (PAAS) – Dienst, der von Google angeboten wird. Im Prinzip können dort beliebige Web-Anwendungen deployed werden, allerdings unterliegen sie einigen Einschränkungen, deren Ursprung jeweils die Cloud-Eigenschaften der Umgebung sind:
Google versorgt jederzeit eine beliebige Anzahl von Instanzen mit der Anwendung, jede Instanz kann dabei zu jedem Zeitpunkt hoch- und heruntergefahren werden. Instanzen können auf Rechnern laufen, die räumlich weit getrennt sind. Ein Benutzer, der gerade noch mit einer Anwendung kommuniziert, die in den USA deployed ist, kann im nächsten Moment schon mit einer Anwendung kommunizieren, die in Irland deployed ist.

Eine Einschränkung, die daraus natürlich erwächst, ist, dass eine herkömmliche relationale Datenbank in so einer hochdynamischen Umgebung nicht funktionieren kann, siehe dazu auch Grundlagen Cloud Computing: CAP-Theorem. Google bietet daher in der App Engine mit BigTable eine tabellenorientierte NoSQL-Persistenzlösung.

Zugriff auf Daten in der Google App Engine

Die Google App Engine bietet für Java eine Low-Level-API an, die allerdings nicht dafür gedacht ist, direkt aus einer Anwendung heraus mit ihr zu interagieren, sondern eher, um neue Adaptoren zu entwickeln. Auf High-Level-Ebene bietet die App Engine die Integration mit JPA und JDO, allerdings mit Einschränkungen, denn wir haben es nun einmal nicht mit einer relationalen Datenbank zu tun.

Wir haben uns für eine dritte Variante entschieden: Objectify.

Objectify bietet im Gegensatz zur Low-Level-API die Möglichkeit, typisierte POJOs zu persistieren, ein einfaches Transaktionsmodell, typisierte Schlüssel und Abfragen, hat einen geringen Footprint und gaukelt einem nicht vor, dass man mit einer relationen Datenbank arbeitet.

Objectify-Entität

Hier haben wir eine sehr einfache Entität.

@Entity
public class UserObjectify {
 
	@Id
	private Long id;
	@Unindexed
	private String name;
	@Unindexed
	@Embedded
	private AddressObjectify address;
	@Indexed
	private Key role;
 
   ...
}

@Entity und @Id sind selbsterklärend, hier können die Annotationen aus der Java Persistence API genutzt werden. @Indexed und @Unindexed entscheiden darüber, ob die entsprechenden Daten in der BigTable indiziert werden sollen. Mit @Embedded können ganze Objekte mit dieser Entität persistiert werden. Diese Objekte müssen mit @Embeddable annotiert werden, nach ihnen kann nicht direkt gesucht werden. Eine Assoziation wird gelöst, indem ein Key vom Typ des assoziierten Objekts abgelegt wird.

Get, put, delete und query mit Objectify

Die Klasse Objectify bietet diverse Methoden für das Laden, Speichern, Löschen und Suchen von Entitäten. Zum Erzeugen eines Objectify-Objekts wird die ObjectifyFactory genutzt, die intern auf den DatastoreService zugreift, der in der Google App Engine verfügbar ist. Wir nutzen die von Objectify mitgelieferte Klasse DAOBase als Basis unserer Repositories. Diese Klasse liefert ein lazy initialisiertes Objectify-Objekt über die Methode ofy(). Dieses kann dann zum Beispiel wie folgt genutzt werden.

Get

UserObjectify userObjectify = ofy().get(UserObjectify.class, id);

Put

ofy().put(userObjectify);

Delete

ofy().delete(userObjectify);

Query

List users = ofy().query(UserObjectify.class)
    .filter("role", new Key(RoleObjectify.class, roleId)).list();

Über das Query-Objekt sind diverse Möglichkeiten für Abfragen vorhanden.

Mismatch zwischen Domain- und Persistenzklassen

Unsere Domainklasse User sieht so aus:

public class User {
 
	private Long id;
	private String name;
	private Address address;
	private Role role;
 
   ...
}

In erster Linie fällt als Unterschied auf, dass wir Assoziationen natürlich nicht über Schlüssel abbilden, sondern direkt zum entsprechenden Objekt ziehen, in diesem Fall Role. Zusammen mit der Tatsache, dass wir die proprietären Objectify-Annotationen ungern in unserer Domain haben wollen, bleibt der Schluss, dass wir tatsächlich zwei Klassen benötigen.

BaseRepository

Unsere Domain soll sauber bleiben. Daraus folgt dann ebenfalls, dass unsere Repositories in den Methoden Domain-Klassen annehmen, keine Objectify-Klassen. Wir erstellen ein BaseRepository-Interface, das die Funktionen beinhaltet, die allen Entitäten gemein sind. EntityAggregateRoot ist das gemeinsame Interface aller Domain-Entitäten.

public interface EntityAggregateRoot {
 
	Long getId();
 
	void setId(Long id);
 
}
public interface BaseRepository {
 
	Long put(T entity);
 
	T get(Long id);
 
	void delete(T entity);
 
}

Mapping zwischen Domain- und Persistenzklassen

EntityAggregateRootObjectify ist das gemeinsame Interface aller Objectify-Entitäten.

public interface EntityAggregateRootObjectify {
 
	Long getId();
 
	void setId(Long id);
 
}

Das Interface Mapping wird für jedes Domain- und Objectifyklassen – Paar implementiert, um die Daten zu mappen. Diese Klassen werden sehr simpel gehalten.

public interface Mapping<T extends EntityAggregateRoot, U extends EntityAggregateRootObjectify> {
 
	T fromObjectify(U entityObjectify);
 
	U toObjectify(T entity);
 
}

Oberklasse für alle Repositories: AbstractRepository

Das AbstractRepository erbt von DAOBase, um auf das Objectify-Object ofy() zugreifen zu können. Es implementiert BaseRepository. Die Entitätsklassen und die Mappingklasse werden per Generics gesetzt. Da wir die konkrete Objectify-Entitätsklasse (beispielsweise UserObjectify) für get() und query() benötigen, übergeben wir sie hier im Konstruktor, der von der konkreten Unterklasse aufgerufen wird.

public abstract class AbstractRepository<T extends EntityAggregateRoot, 
		U extends EntityAggregateRootObjectify, V extends Mapping<T, U>>
		extends DAOBase implements BaseRepository<T> {
 
	protected V mapping;
	private Class<U> entityAggregateRootObjectifyClass;
 
	protected AbstractRepository(V mapping,
			Class<U> entityAggregateRootObjectifyClass) {
		super();
		this.mapping = mapping;
		this.entityAggregateRootObjectifyClass = entityAggregateRootObjectifyClass;
	}

In der put()-Methode sieht man, wie zunächst die Domain-Entität in ihre Objectify-Entität gemappt wird, um dann per ofy() die Persistierung durchzuführen. Abschließend wird die ID auch in der Domain-Entität gesetzt und die ID zurückgegeben. Die delete()-Methode funktioniert auf ähnliche Art und Weise.

	public Long put(T entity) {
		U entityObjectify = mapping.toObjectify(entity);
		ofy().put(entityObjectify);
		entity.setId(entityObjectify.getId());
		return entityObjectify.getId();
	}
 
	public void delete(T entity){
		U entityObjectify = mapping.toObjectify(entity);
		ofy().delete(entityObjectify);
	}

In der get()-Methode wird das gewünschte Objekt geladen und dann in die Domain-Entität konvertiert. Die Methode handleAssociations() kann von Subklassen überschrieben werden, um Assoziationen zu laden. Wie das konkret funktioniert, sehen wir später am ObjectifyUserRepository.

	public T get(Long id) {
		U entityObjectify = ofy().get(entityAggregateRootObjectifyClass, id);
		T entity = mapping.fromObjectify(entityObjectify);
		return this.handleAssociations(entity, entityObjectify);
	}
 
	protected T handleAssociations(T entity, U entityObjectify) {
		return entity;
	}

Alle Methoden des BaseRepository-Interfaces sind nun implementiert. Damit auch Abfragen in Subklassen unterstützt werden, fügen wir noch eine Methode hinzu, die mit einem Callback-Interface arbeitet. Durch den QueryCallback kann die Subklasse eine beliebige Abfrage erstellen, die von der Methode dann inklusive Mapping durchgeführt wird.

	protected List<T> getEntities(QueryCallback<U> queryCallback) {
		List<T> entityList = new ArrayList<T>();
		Query<U> query = ofy().query(entityAggregateRootObjectifyClass);
		query = queryCallback.manipulateQuery(query);
		for (U entityObjectify : query) {
			T entity = mapping.fromObjectify(entityObjectify);
			entityList.add(this.handleAssociations(entity, entityObjectify));
		}
		return entityList;
	}
 
	protected interface QueryCallback<U extends EntityAggregateRootObjectify> {
 
		public Query<U> manipulateQuery(Query<U> query);
 
	}

Implementierung: ObjectifyUserRepository

Die konkrete Implementierung für die Entität User ist nun relativ kurz, da get(), put() und delete() bereits durch die Oberklasse abgedeckt sind. Hinzu kommt nur noch eine spezielle Abfrage-Methode, die alle Benutzer findet, die eine bestimmte Rolle haben. Die handleAssociations-Methode sorgt dafür, dass die Assoziation von User auf Role aufgelöst wird, indem diese mit Hilfe des RoleRepositorys geladen wird.

public class ObjectifyUserRepository extends
		AbstractRepository{
 
	static {
		ObjectifyService.register(UserObjectify.class);
	}
 
	private RoleRepository roleRepository;
 
	public ObjectifyUserRepository(UserMapping userMapping, RoleRepository roleRepository) {
		super(userMapping, UserObjectify.class);
		this.roleRepository = roleRepository;
	}
 
	public List findUserByRoleId(final Long roleId) {
		return this.getEntities(new QueryCallback() {
 
			@Override
			public Query manipulateQuery(
					Query query) {
				return query.filter("role", new Key(RoleObjectify.class, roleId));
			}
		});
	}
 
	protected User handleAssociations(User entity,
			UserObjectify entityObjectify) {
		if (entityObjectify.getRole() != null) {
			entity.setRole(roleRepository.get(entityObjectify
					.getRole().getId()));
		}
		return entity;
	}
}

Fazit

Objectify ist einfach zu benutzen und bringt weniger Overhead mit als JDO und JPA, die in der Google App Engine eingeschränkt verwendbar sind.

In unserer Anwendung haben wir unsere Persistenzschicht und unsere Domain klar getrennt. Objectify wird nur da verwendet und ist nur da sichtbar, wo es wirklich benötigt wird.

Außerdem vermeiden wir durch das AbstractRepository jegliche Code-Duplication und erleichtern das Erstellen von neuen Repositories für neue Entitäten.

  • Facebook
  • Delicious
  • Digg
  • StumbleUpon
  • Reddit
  • Blogger
  • LinkedIn
Tobias Flohre

Eine Antwort auf Persistenz in der Google App Engine – Generische Repositories mit Objectify

  1. Kai Wähner sagt:

    Hallo,

    toller Artikel über GAE und Objectify! Ich bevorzuge dieses Framework ebenfalls gegenüber JPA / JDO.

    Noch zwei Anmerkungen:

    1) Leider wird man durch Objectify noch abhängiger von der GAE als man es ohnehin schon ist. Hier sind langfristig andere Ansätze wie VMware CloudFoundry oder Red Hat OpenShift erfolgsversprechender.

    2) GAE unterstützt in Zukunft endlich auch SQL (in Form von MySQL und JDBC): http://googlecode.blogspot.com/2011/10/google-cloud-sql-your-database-in-cloud.html

    Gruß
    Kai Wähner (Twitter: @KaiWaehner)

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>