Beliebte Suchanfragen
|
//

Persistenz in der Google App Engine – Generische Repositories mit Objectify

10.10.2011 | 6 Minuten Lesezeit

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 RoleRepositorys 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.

|

Beitrag teilen

//

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.