by ampatspell
in Code
Gaeds is my tiny AppEngine for Java low-level DatastoreService wrapper. This is third post in three post series about new features that will form 1.2 final release.
The first two are:
Google AppEngine Datastore Entity primary keys are Key instances. Each Key is composed from String kind and either long id or String keyName. Also Key can be encoded as String and then decoded back to Key instance using KeyFactory.
For me the most important new gaeds feature is GaedsKey<T> generic class that wraps Key and respective model class. It is impossible to create GaedsKey with mismatched model class (see notes below about inheritance).
GaedsKey
public interface GaedsKey<T> extends Comparable<GaedsKey<T>> { Key getKey(); Class<? extends T> getModelClass(); <R> GaedsKey<R> cast(Class<R> type); <P> GaedsKey<P> getParent(Class<? extends P> model); String encode(); // and few other methods }
To create GaedsKey from Key or from encoded String GaedsKeyFactory must be used:
GaedsKeyFactory kf = session.key(); // or @Inject GaedsKeyFactory directly // from String or Key GaedsKey<Task> key = kf.create(string, Task.class); // from encoded datastore key GaedsKey<Task> key = kf.create(datastoreKey, Task.class); // from datastore key // Root keys GaedsKey<Task> key = kf.create(Task.class, "something"); // with keyName GaedsKey<Task> key = kf.create(Task.class, 102l); // with id // Keys with parent Key or GaedsKey GaedsKey<Task> key = kf.create(parent, Task.class, "something"); // with keyName GaedsKey<Task> key = kf.create(parent, Task.class, 102l); // with id
First two DatastoreKeyFactory#create calls ensures that provided key matches model class by comparing Key kind to kind declared for model. If kind don’t match, GaedsKeyRuntimeException is thrown. This behavior ensures that encoded keys coming from web request matches expected model kind. If kinds doesn’t match, exception is thrown before Datastore get/put/query operation (fail fast). By using Key directly get operations would quite possibly just fail with ClassCastException after touching Datastore.
String string = keyToString(createKey("User", 101)); // Creates "User(101)" key Key key = stringToKey(string); // Throws ModelNotFoundException or ClassCastException after get. Task task = sess.get(key); // Throws GaedsKeyRuntimeException GaedsKey<Task> key = kf.create(string, Task.class); // Not reached Task task = sess.get(key);
Gaeds models has direct relationship to Entity keyNames. It is possible to use GaedsKey<T> without any changes to Model classes but more beneficial they are if @PrimaryKey, @ParentKey and all @Property Key declarations also are declared as GaedsKey<T>. Lets consider 2 models – Person and Task:
@Model public static class Person { @PrimaryKey private GaedsKey<Person> key; // getters & setters } @Model public static class Task { @PrimaryKey private GaedsKey<Task> key; @Property private String title; @Property private GaedsKey<Person> owner; // getters & setters }
And some straight-forward operations on them:
// First request // create person Person person = new Person(); sess.put(person); // create task with person set as owner Task task = new Task(); task.setTitle("Hello world"); task.setOwner(person.getKey()); sess.put(task); String encoded = task.getKey().encode(); // Second request // Create Task key from encoded string (will throw if encoded Key is not for Task kind) GaedsKey<Task> taskKey = sess.key().create(encoded, Task.class); // get Task, get Person Task task = sess.get(taskKey); Person person = sess.get(task.getOwner());
Task{
key=GaedsKey{Task(2) com.ampatspell.gaeds.test.Task},
title='Hello world',
owner=GaedsKey{Person(1) com.ampatspell.gaeds.test.Person}
}
Person{
key=GaedsKey{Person(1) com.ampatspell.gaeds.test.Person}
}
As we can see, the GaedsKey Session API is the same as for Key and that we needed to access GaedsKeyFactory only once — for string decoding. While GaedsKey API is identical to Key based there are a bit more going on behind the scenes.
Not only GaedsKey creation is validated to conform model-kind relationship, also all model attributes that has GaedsKey type are validated before put and after get, query.
Lets consider a bit different example involving gaeds model inheritance:
@Model public static class Person { @PrimaryKey private GaedsKey<? extends Person> key; } @Model(discriminator = "administrator") public static class Administrator extends Person { }
We have a new class Administrator which extends Person. Gaeds implements inheritance by transparently adding "_discriminator" property to saved entities and because there is no way knowing which entity type is before getting actual Entity, GaedsKey can be created with incorrect key+type pair:
Administrator admin = new Administrator(); sess.put(admin); Person person = new Person(); sess.put(person); String key = person.getKey().encode(); // creating a key with Person Datastore Key and Administrator type GaedsKey<Administrator> admin = kf.create(key, Administrator.class); // Throws an GaedsKeyRuntimeException sess.get(admin);
After Entity is retrieved from Datastore, its type is compared to required type for GaedsKey, if GaedsKey type is not the same or subclass of fetched Model’s class, GaedsKeyRuntimeException is thrown. This prevents getting ClassCastException or incorrect Model when sess.get returns. This also works for messed up GaedsKey unchecked casts:
@Model public static class Task { // ... @Property private GaedsKey<Administrator> owner; }
Person person = new Person(); sess.put(person); // Unchecked cast GaedsKey<Administrator> ownerKey = (GaedsKey) person.getKey(); Task task = new Task(); task.setOwner(ownerKey); // throws GaedsKeyRuntimeException sess.put(task);
com.ampatspell.gaeds.internal.key.GaedsKeyRuntimeException:
GaedsKey{ExamplePerson(2) com.ampatspell.gaeds.test.Person} model class is not assignable
to com.ampatspell.gaeds.test.Administrator
Also GaedsKey exposes cast(T targetType) method that allows up- and down-casting GaedsKey instances but it enforces that:
GaedsKey<Person> cannot be up-casted to GaedsKey<Administrator>GaedsKey<Administrator> can be down-casted to GaedsKey<Person> and then up-casted back to GaedsKey<Administrator>But note: it is possible to create GaedsKey<Administrator> with Person Key and Administrator type, save it in Datastore as GaedsKey<Administrator>. Try fetching entity using that GaedsKey before, only if fetch succeeds, we can be confident that given GaedsKey<Administrator> actually points to Administrator.
For gets and queries, model @Property attributes there is a bit different story. Lets start with creating Task with Zeeba as an owner instead of Administrator:
Entity task = new Entity("Task"); task.setProperty("owner", createKey("Zeeba", "fake")); Key dsKey = ds.put(task); GaedsKey<Task> key = kf.create(dsKey, Task.class); Task model = sess.get(key); assertTrue(Gaeds.asModel(model).getIncompatibleProperties().contains("owner"));
WARNING: Entity to model mapping cast errors
Model: com.ampatspell.gaeds.test.Task
Entity: Task(1)
* 'owner'
com.google.appengine.api.datastore.Key =>
com.ampatspell.gaeds.GaedsKey com.ampatspell.gaeds.test.Administrator (Zeeba("fake"))
As we can see, this is also considered incompatibility because Session wouldn’t allow persisting GaedsKey<Zeeba> instead of GaedsKey<Administrator>. Useful for Entity migration.
Session and Session#query() methods now takes either Key or GaedsKeyGaedsKeyFactory can be injected (@Inject) directly or accessed from Session#key()GaedsBpSupport and GaedsDaoSupport inherits from AbstractGaedsSupport which also implements #key()GaedsKey @Property, @PrimaryKey and @ParentKey can be declared as:
GaedsKey<Task>GaedsKey<? extends Task>GaedsKey<T> for generic classes (@Model class Task<T> { ... })I’ve migrated 10 or so models to GaedsKey for one of the projects I’m currently working on. All tests pass, everything looks fine but I’ll wait few more days before releasing 1.2 final.