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.
1@Entity 2public class UserObjectify { 3 4 @Id 5 private Long id; 6 @Unindexed 7 private String name; 8 @Unindexed 9 @Embedded 10 private AddressObjectify address; 11 @Indexed 12 private Key role; 13 14 ... 15}
@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
1UserObjectify userObjectify = ofy().get(UserObjectify.class, id);
Put
1ofy().put(userObjectify);
Delete
1ofy().delete(userObjectify);
Query
1List users = ofy().query(UserObjectify.class)
2 .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:
1public class User {
2
3 private Long id;
4 private String name;
5 private Address address;
6 private Role role;
7
8 ...
9}
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.
1public interface EntityAggregateRoot {
2
3 Long getId();
4
5 void setId(Long id);
6
7}
1public interface BaseRepository {
2
3 Long put(T entity);
4
5 T get(Long id);
6
7 void delete(T entity);
8
9}
Mapping zwischen Domain- und Persistenzklassen
EntityAggregateRootObjectify
ist das gemeinsame Interface aller Objectify-Entitäten.
1public interface EntityAggregateRootObjectify {
2
3 Long getId();
4
5 void setId(Long id);
6
7}
Das Interface Mapping
wird für jedes Domain- und Objectifyklassen – Paar implementiert, um die Daten zu mappen. Diese Klassen werden sehr simpel gehalten.
1public interface Mapping<T extends EntityAggregateRoot, U extends EntityAggregateRootObjectify> { 2 3 T fromObjectify(U entityObjectify); 4 5 U toObjectify(T entity); 6 7}
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.
1public abstract class AbstractRepository<T extends EntityAggregateRoot, 2 U extends EntityAggregateRootObjectify, V extends Mapping<T, U>> 3 extends DAOBase implements BaseRepository<T> { 4 5 protected V mapping; 6 private Class<U> entityAggregateRootObjectifyClass; 7 8 protected AbstractRepository(V mapping, 9 Class<U> entityAggregateRootObjectifyClass) { 10 super(); 11 this.mapping = mapping; 12 this.entityAggregateRootObjectifyClass = entityAggregateRootObjectifyClass; 13 }
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.
1public Long put(T entity) { 2 U entityObjectify = mapping.toObjectify(entity); 3 ofy().put(entityObjectify); 4 entity.setId(entityObjectify.getId()); 5 return entityObjectify.getId(); 6 } 7 8 public void delete(T entity){ 9 U entityObjectify = mapping.toObjectify(entity); 10 ofy().delete(entityObjectify); 11 }
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
.
1public T get(Long id) { 2 U entityObjectify = ofy().get(entityAggregateRootObjectifyClass, id); 3 T entity = mapping.fromObjectify(entityObjectify); 4 return this.handleAssociations(entity, entityObjectify); 5 } 6 7 protected T handleAssociations(T entity, U entityObjectify) { 8 return entity; 9 }
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.
1protected List<T> getEntities(QueryCallback<U> queryCallback) { 2 List<T> entityList = new ArrayList<T>(); 3 Query<U> query = ofy().query(entityAggregateRootObjectifyClass); 4 query = queryCallback.manipulateQuery(query); 5 for (U entityObjectify : query) { 6 T entity = mapping.fromObjectify(entityObjectify); 7 entityList.add(this.handleAssociations(entity, entityObjectify)); 8 } 9 return entityList; 10 } 11 12 protected interface QueryCallback<U extends EntityAggregateRootObjectify> { 13 14 public Query<U> manipulateQuery(Query<U> query); 15 16 }
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 RoleRepository
s geladen wird.
1public class ObjectifyUserRepository extends
2 AbstractRepository{
3
4 static {
5 ObjectifyService.register(UserObjectify.class);
6 }
7
8 private RoleRepository roleRepository;
9
10 public ObjectifyUserRepository(UserMapping userMapping, RoleRepository roleRepository) {
11 super(userMapping, UserObjectify.class);
12 this.roleRepository = roleRepository;
13 }
14
15 public List findUserByRoleId(final Long roleId) {
16 return this.getEntities(new QueryCallback() {
17
18 @Override
19 public Query manipulateQuery(
20 Query query) {
21 return query.filter("role", new Key(RoleObjectify.class, roleId));
22 }
23 });
24 }
25
26 protected User handleAssociations(User entity,
27 UserObjectify entityObjectify) {
28 if (entityObjectify.getRole() != null) {
29 entity.setRole(roleRepository.get(entityObjectify
30 .getRole().getId()));
31 }
32 return entity;
33 }
34}
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.
Weitere Beiträge
von Tobias Flohre
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Tobias Flohre
Senior Software Developer
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.