GAE/Objectify/IntroductionToObjectify

イントロダクション

ここではObjectifyを使ってどのようにデータをget,put,delete,queryするかを説明する。 これを読みながらObjectifyのjavadocを開くことは多いに助けることになると思う。以下の例では、明瞭さのためにgetter/setterメソッドは省略されている。

エンティティクラスを作成する

最初のステップはエンティティクラスを定義することだ。 以下にCarという例を示す。

public class Car
{
    @Id Long id;
    String vin;
    int color;
    @Transient String doNotPersist;

    private Car() {}
    
    public Car(String vin, int color)
    {
        this.vin = vin;
        this.color = color;
    }
}

注意すべきことは以下だ。

より多くの情報がAnnotationReferenceにある。

※訳注:実際の可能な型はhttp://code.google.com/intl/ja/appengine/docs/java/datastore/entities.html#Properties_and_Value_Typesに説明がある。

クラスの登録

データストア操作の前に、ObjectifyServiceを使って、エンティティクラスを登録する必要がある。

ObjectifyService.register(Car.class);
ObjectifyService.register(Motorcycle.class);

Objectifyは@Entityクラスを探してクラスパスをスキャンすることはしない。 この理由と反対意見がある。BestPracticesの議論を参照のこと。 Springを使っているなら、objectify-appengine-springを参照のこと。

※訳注: サーバサイドでGuiceを使用している場合には、これらの初期化はGuiceモジュールのstatic初期化として行う方法があるようだ。 GWTPの場合には、以下のようなコードになる。

....
import com.googlecode.objectify.*;
import com.gwtplatform.dispatch.server.guice.*;

public class ServerModule extends HandlerModule {
  static {
    ObjectifyService.register(Car.class);    
  }  
  @Override
  protected void configureHandlers() {
    bindHandler(SendTextToServer.class, SendTextToServerActionHandler.class);
  }
}

基本的な操作:Get、Put、Delete

ObjectifyServiceからObjectifyインターフェースを取得することができる。

Objectify ofy = ObjectifyService.begin();

// 単純なcreate
Car porsche = new Car("2FAST", "red");
ofy.put(porsche);
assert porsche.id != null;    // idは自動生成される

// 読み込んでみる
Car fetched1 = ofy.get(new Key<Car>(Car.class, porsche.id));
Car fetched2 = ofy.get(Car.class, porsche.id);    // 上と等価だが、より簡単
assert areEqual(porsche, fetched1, fetched2);

// データを変更して書きこむ
porsche.color = "blue";
ofy.put(porsche);

// 削除する
ofy.delete(porsche);

インターフェースはバッチ操作をサポートしている。

Objectify ofy = ObjectifyService.begin();

// Create
Car porsche = new Car("2FAST", "red");
Car unimog = new Car("2SLOW", "green");
Car tesla = new Car("2NEW", "blue");
ofy.put(tesla, unimog, porsche);    // 可変引数。Car[]やIterable<Car>でもよい

// 読み込んでみる
List<Key<Car>> carKeys = new ArrayList<Key<Car>>();
carKeys.add(new Key<Car>(Car.class, porsche.id));
carKeys.add(new Key<Car>(Car.class, unimog.id));
carKeys.add(new Key<Car>(Car.class, tesla.id)));
Map<Key<Car>, Car> fetched1 = ofy.get(carKeys);

// より便利なショートカット。返り値に注意
Map<Long, Car> fetched2 = ofy.get(Car.class, new Long[] { porsche.id, unimog.id, tesla.id });

// こういうやり方もある
Map<Long, Car> fetched3 = ofy.get(Car.class, Arrays.asList(porsche.id, unimog.id, tesla.id));

// バッチオペレーションはホモジニアスでなくてもよい
List<Key<? extends Vehicle>> vehKeys = new ArrayList<Key<? extends Vehicle>>();
vehKeys.add(new Key<Car>(Car.class, porsche.id));
vehKeys.add(new Key<Motorcycle>(Motorcycle.class, ktm.id));
Map<Key<Vehicle>, Vehicle> fetched4 = ofy.get(vehKeys);

// データを削除してみる
ofy.delete(fetched1.values());

// オブジェクトをロードせず、キーだけで削除することもできる
ofy.delete(
    new Key<Car>(Car.class, porsche.id),
    new Key<Car>(Car.class, unimog.id),
    new Key<Car>(Car.class, tesla.id));

クエリ

以下にいくつかのクエリ例を示す。 Objectifyのクエリは、ヒューマンフレンドリなGAE/PythonのQueryクラスを模倣しており、マシンフレンドリなGAE/Javaバージョンではない。

Objectify ofy = ObjectifyService.begin();

Car car = ofy.query(Car.class).filter("vin", "123456789").get();

// クエリそれ自身はIterableであえる
Query<Car> q = ofy.query(Car.class).filter("vin >", "123456789");
for (Car car: q) {
    System.out.println(car.toString());
}

// キーのみを問い合わせることもできる。キーオブジェクトのリターンは、オブジェクト全体よりもずっと効率がよい。
Iterable<Key<Car>> allKeys = ofy.query(Car.class).fetchKeys();

// (キーのみの取得は)アイテムを削除するのに都合がよい
ofy.delete(allKeys);

クエリはインデックスに依存していることに注意すること。 詳細についてはthe appengine documentation for indexes を参照し、フィルタリング可能な場合と不可能な場合をチェックすること。

カーソル

カーソルは結果セットにおけるチェックポイントとして利用できる。 カーソルをどこか別の場所に保存しておき、あとでそこから再開することができる。 これはTask Queue APIと共に用いられることが多く、一つのリクエストが30秒以内という制限のもとで処理ができないような大きなデータセットに対する繰り返し処理の際に用いることができる。 このアルゴリズムを大まかにいうと以下のとおり。

カーソル例

Objectifyによって提供されるIterable(Queryオブジェクトを含む)は、実際にはQueryResultIterableである。 これはQueryResultIteratorを作成し、カーソルを取得することができる。

以下は、すべてのCarエンティティについて繰り返すサーブレットの例である。

public static final long LIMIT_MILLIS = 1000 * 25; // provide a little leeway

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    long startTime = System.currentTimeMillis();

    Objectify ofy = ObjectifyService.begin();
    Query<Car> query = ofy.query(Car.class);

    String cursorStr = request.getParameter("cursor");
    if (cursorStr != null)
        query.startCursor(Cursor.fromWebSafeString(cursorStr));

    QueryResultIterator<Car> iterator = query.iterator();
    while (iterator.hasNext()) {
        Car car = iterator.next();

        ... // process car

        if (System.currentTimeMillis() - startTime > LIMIT_MILLIS) {
            Cursor cursor = iterator.getStartCursor();
            Queue queue = QueueFactory.getDefaultQueue();
            queue.add(url("/pathToThisServlet").param("cursor", cursor.toWebSafeString()));
            break;
        }
    }
}

非同期呼び出し

GAEの低レベルデータストアAPIは並行非同期操作をサポートしている。 GAEの非同期モデルはJavascriuptの"pass in a callback function"モデルには従っていない。 そうではなく、非同期呼び出しを作成すると、ペンディング操作への参照を受け取る。 並行実行する複数の参照を作成することができるが、完全な結果を取得しようとするいかなるリクエストも、結果が得られるまでブロックさせられる。

このことは例を使って説明したほうがよいだろう。

非同期キュー

すべてのクエリはデフォルトでは非同期である。 クエリへの「参照」はIteratorオブジェクトである。 例えば、以下の2つのクエリは並行実行される。

Iterator<Fruit> fruitIt = ofy.query(Fruit.class).filter("color", "red").iterator();
Iterator<Animal> animalIt = ofy.query(Animal.class).filter("hair", "short").iterator();

// 2つのクエリはバックエンドで実行される

while (fruitIt.hasNext()) {    // hasNext()はクエリ結果が得られるまでブロックする
    ... fruitsを処理する
}

複数のイテレータを作成してから、それらのイテレータについて実行を行うこと。

非同期のget()/put()/delete()

注意: 以下はObjectify v3.xを必要とする。

注意: もしObjectifyのグローバルmemcacheを非同期操作で用いるのであれば、com.googlecode.objectify.cache.AsyncCacheFilterをインストールしておく必要がある。そうしないとcacheはデータストアと適切に同期しなくなる。これは、GAE SDKの制限を回避する方法であり、星印をつけておいてほしい。

クエリを並列実行するのに特別なインターフェースを必要としない。なぜなら、Iteratorインターフェースはペンディングされた操作への参照として振る舞うからだ。 しかし、get(),put(),delete()は完全な結果を戻す。

Objectifyは同様の並列メソッドを提供するが、それらはResult<?>を返す。Future<?>と似たようなものだが、しかしまともな例外ハンドリングが可能になる。 以下にObjectifyのAPIの顕著な点を示す。

public interface Result<T>
{
    T get();
    Future<T> getFuture();
}

public interface Objectify
{
    ...
    public AsyncObjectify async();
}

public interface AsyncObjectify
{
    ...
    <T> Result<T> get(Key<? extends T> key);
    <T> Result<T> get(Class<? extends T> clazz, long id);
    ...
    <T> Result<Key<T>> put(T obj);
    ...
    Result<Void> delete(Object... keysOrEntities);
    ...
}

You get the picture. The AsyncObjectify interface has methods that parallel the synchronous Objectify methods, but return Result<?> instead. You can issue multiple parallel requests like this:

Objectify ofy = ObjectifyService.begin();
Result<Fruit> fruit = ofy.async().get(Fruit.class, "apple");
Result<Map<Long, Animal>> animals = ofy.async().get(Animal.class, listOfAnimalIds);
Result<Key<Fruit>> key = ofy.async().put(new Fruit("orange"));
Iterator<City> citiesIterator = ofy.query(City.class).filter("population >", 1000).iterator();

// すべてのリクエストは並列に実行される

String color = fruit.get().getColor();  // Result<?>.get()の呼び出しはリクエストが完了するまでブロックされる。

非同期リクエストの考慮事項

並列リクエストは注意深く使用しなければならない。

ストレージを最適化する

インデックスはクエリに必須であるが、作成や更新は高くつく。 api_cpu_msにおいて、一つのインデックス無しのエンティティをput()するのに48msかかる。 標準的なインデックス付フィールドを追加すると、これに17msが加算される。 インデックスは並列に書き込まれるので、実時間が加算されるわけではないが、しかし、週末には支払いに実コストが加算されていることだろう。 インデックスはまた、膨大なストレージスペースを消費する。 オリジナルデータの数倍の量になることもある。

@Indexedと@Unindexed

デフォルトでは、TextとBlobを除くすべてのエンティティフィールドはインデックス付けされる。 フィールドあるいはクラスに対する@Indexedと@Unindexedアノテーションによって、これを制御することができる。

// デフォルトではフィールドはインデックス付けされる
public class Car
{
    @Id Long id;
    String vin;
    @Unindexed String color;
}

// 以下は同じ効果になる
@Unindexed
public class Car
{
    @Id Long id;
    @Indexed String vin;
    String color;
}

部分インデックス

一つのフィールドの特定の値の集合についてのみ、クエリを行う必要のある場合がある。 これらがエンティティのうちの小さなパーセンテージを占めていると、残りをインデックス付けする必要はない。 例をあげる。

Objectifyは任意のフィールドについての任意の状態を定義する方法を提供する。 You can create your own If classes or use one of the provided ones:

public class Person
{
    @Id Long id;
    String name;

    // adminフィールドはtrueの時にのみインデックス付けされる
    @Unindexed(IfFalse.class) boolean admin;

    // You can provide multiple conditions, any of which will satisfy
    @Unindexed({IfNull.class, IfEmptyString.class}) String title;
}

These If conditions work with both @Indexed and @Unindexed on fields. You cannot specify If conditions on the class-level annotations.

Check the javadocs for available classes. Here are some basics to start: IfNull.class, IfFalse.class, IfTrue.class, IfZero.class, IfEmptyString.class, IfDefault.class IfDefault.class

IfDefault.class is special. It tests true when the field value is whatever the default value is when you construct an object of your class. For example:

public class Account
{
    @Id Long id;

    // Only indexed when status is something other than INACTIVE
    @Unindexed(IfDefault.class) StatusType status = StatusType.INACTIVE;
}

Note that you can initialize field values inline (as above) or in your no-arg constructor; either will work.

カスタムコンディション

You can easily create your own custom conditions by extending ValueIf or PojoIf. ValueIf is a simple test of a field value. For example:

public static class IfGREEN extends ValueIf<Color>
{
    @Override
    public boolean matches(Color value)
    {
        return color == Color.GREEN;
    }
}

public class Car
{
    @Id Long id;
    @Unindexed(IfGREEN.class) Color color;
}

You can use PojoIf to examine other fields to determine whether or not to index! This example is inspired by the example in the Partial Index Wikipedia page, and will use a static inner class for convenience:

// We are modeling:  create index partial_salary on employee(age) where salary > 2100;
@Unindexed
public class Employee
{
    static class SalaryCheck extends PojoIf<Employee>
    {
        @Override
        public boolean matches(Employee pojo)
        {
            return pojo.salary > 2100;
        }
    }

    @Id Long id;
    @Index(SalaryCheck.class) int age;
    int salary;
}

You can examine the source code of the If classes to see how to construct your own. Most are one or two lines of code. @NotSaved

If you would like to exclude a field value from being stored, you can use the @NotSaved annotation. The field will not be saved and will not occupy any space in the datastore. This works well in concert with IfDefault.class:

@Unindexed
public class Player
{
    @Id Long id;
    @Indexed String name;
    @NotSaved(IfDefault.class) RankType rank = RankType.PRIVATE;
    @NotSaved(IfDefault.class) int health = 100;
    @NotSaved(IfDefault.class) Date retired = null;
}

Note that @NotSaved values are not stored at all, so they aren't indexed and you can't query for them.

ポリモーフィズム

NOTE: 以下はv3.xを必要とする

Objectify lets you define a polymorphic hierarchy of related entity classes, and then load and query them without knowing the specific subtype. Here are some examples:

@Entity
public class Animal {
    @Id Long id;
    String name;
}
        
@Subclass
public class Mammal extends Animal {
    boolean longHair;
}
        
@Subclass
public class Cat extends Mammal {
    boolean hypoallergenic;
}

Things to note:

In a polymorphic hierarchy, you can get() and query() without knowing the actual type:

Objectify ofy = ObjectifyService.begin();

Animal annie = new Animal();
annie.name = "Annie";
ofy.put(annie);

Mammal mam = new Mammal();
mam.name = "Mam";
m.longHair = true;
ofy.put(mam);

Cat nyan = new Cat();
nyan.name = "Nyan";
nyan.longHair = true;
nyan.hypoallergenic = true;
ofy.put(nyan);

// This will return the Cat
Animal fetched = ofy.get(Animal.class, nyan.id);

// This query will produce three objects, the Animal, Mammal, and Cat
Query<Animal> all = ofy.query(Animal.class);

// This query will produce the Mammal and Cat
Query<Mammal> mammals = ofy.query(Mammal.class);

Implementation Considerations

When you store a polymorphic entity subclass (but not an instance of the base type), your entity is stored with two additional, hidden synthetic properties:

The indexed property is what allows polymorphic queries to work. It also means that you cannot simply change your hierarchy arbitrarily and expect queries to continue to work as expected - you may need to re-put() all affected entities to rewrite the indexed field.

There are two ways you can affect this:

Relationships

A relationship is simply a Key stored as a field in an entity. Objectify does not provide "managed" relationships in the way that JDO or JPA does; this is both a blessing and a curse. However, because Key is a generified class, it carries type information about what it points to.

There are fundamentally three different kinds of relationships in Objectify: Parent Relationship

An entity can have a single Key field annotated with @Parent:

public class Person
{
    @Id Long id;
    String name;
}

public class Car
{
    @Id Long id;
    @Parent Key<Person> owner;
    String color;
}

Each Car entity is part of the parent owner's entity group and both can be accessed within a single transaction. When loading the child entity, the parent Key must be used to generate the child's key:

Objectify ofy = ObjectifyService.begin();

Key<Person> owner = new Key<Person>(Person.class, somePersonId);
Car someCar = ofy.get(new Key<Car>(owner, Car.class, someCarId));

Note that this is an inappropriate use of the @Parent entity; if a car were to be sold to a new owner, you would need to delete the Car and create a new one. It is often better to use Single Value Relationships even when there is a conceptual parent-child or owner-object relationship; in that case you could simply change the parent.

If you get() an entity, change the @Parent key field, and put() the entity, you will create a new entity. The old entity (with the old parent) will still exist. You cannot simply change the value of a @Parent key field. This is a fundamental aspect of the appengine datastore; @Parent values form part of an entity's identity.

Single-Value Relationship

In Objectify (and the underlying datastore), Keys are just properties like any other value. Whether it defines a one-to-one relationship or a many-to-one relationship is up to you. Furthermore, a Key field could refer to any type of entity class.

One To One

The simplest type of single-value relationship is one-to-one.

public class Person
{
    @Id String name;
    Key<Person> significantOther;
}

Objectify ofy = ObjectifyService.begin();
Person bob = ofy.get(Person, "bob");
Person bobswife = ofy.get(bob.significantOther);

Many To One

A Key field can represent a many-to-one relationship.

public class Employee
{
    @Id String name;
    Key<Employee> manager;
}

Objectify ofy = ObjectifyService.begin();
Employee bob = ofy.get(Employee.class, "bob");
Employee fred = ofy.get(bob.manager);

It looks identical to the one-to-one relationship because it is. The only difference is a conceptual one. What if you want to know all the employees managed by Fred? You use a query.

Objectify ofy = ObjectifyService.begin();

Iterable<Employee> subordinates = ofy.query(Employee.class).filter("manager", fred);

Multi-Value Relationship

The datastore can persist simple object types (Long, String, etc) and collections of simple object types. It can also persist collections (and arrays) of Keys. This creates an alternative approach for defining one-to-many (and many-to-many) relationships.

public class Employee
{
    @Id String name;
    Key<Employee>[] subordinates;
}

This is sometimes useful, but should be used with caution for two reasons:

Because appengine stores an index entry for each value in the collection, it is possible to issue queries like this:

Objectify ofy = ObjectifyService.begin();

// should contain Fred
Iterable<Employee> managers = ofy.query(Employee.class).filter("subordinates", bob);

The decision to use a Multi-Value Relationship will depend heavily upon the shape of your data and the queries you intend to perform.

Transactions

Working with transactions is almost the same as working with Objectify normally.

Objectify ofy = ObjectifyService.beginTransaction();  // instead of begin()
try
{
    ClubMembers cm = ofy.get(ClubMembers.class, "k123");
    cm.incrementByOne();
    ofy.put(cm);

    ofy.getTxn().commit();
}
finally
{
    if (ofy.getTxn().isActive())
        ofy.getTxn().rollback();
}

All data manipulation methods are the same as you would normally use.

Since entities in Objectify really are Plain Old Java Objects and transactions are tied to the Objectify object, it's easy to work with data inside and outside of transactions (or multiple transactions running in parallel!):

Objectify ofyNoTxn = ObjectifyService.begin();
Objectify ofyTxn = ObjectifyService.beginTransaction();
try
{
    Foo f = ofyTxn.get(Foo.class, "k123");
    Bar b = ofyNoTxn.get(f.barKey);

    if (b.wantsUp())
        f.increment();
    else
        f.decrement();

    ofyTxn.put(f);

    ofyTxn.getTxn().commit();
}
finally
{
    if (ofyTxn.getTxn().isActive())
        ofyTxn.getTxn().rollback();
}

You can interleave multiple transactions or nontransactional actions as long as you obey the the cardinal rule: Within a single transaction (defined by an Objectify object created with beginTransaction()), you may only read or write from a single entity group.

Yes, this means you can get() objects from a transactional Objectify and put() to a nontrasactional Objectify.

Lifecycle Callbacks

Objectify supports two of the JPA lifecycle callbacks: @PostLoad and @PrePersist. If you mark methods on your POJO entity class (or any superclasses) with these annotations, they will be called:

You can have any number of these callback methods in your POJO entity class or its superclasses. They will be called in order of declaration, with superclass methods called first. Two parameter types are allowed:

class MyEntityBase {
    String foo;
    String lowercaseFoo;
    @PrePersist void maintainCaseInsensitiveSearchField() { this.lowercaseFoo = foo.toLowerCase(); }
}

class MyEntity extends MyEntityBase {
    @Id Long id;

    @Transient Date loaded;
    @PostLoad void trackLoadedDate() { this.loaded = new Date(); }

    List<String> stuff = new ArrayList<String>();
    int stuffSize;   // indexed so we can query by list size
    @PrePersist void maintainStuffSize() { this.stuffSize = stuff.size(); }

    @PrePersist void doMore(Objectify ofy, Entity ent) { ... }
}

Caution: You can't update @Id or @Parent fields in a @PrePersist callback; by this time, the low-level Entity has already been constructed with a Key so it can be passed in to the callback as an optional parameter. You can, however, update any other fields and the new values will be persisted.

スキーマのマイグレーション

アプリケーションの生涯にわたってスキーマが変更されないというのは稀なケースだろう。 BigTableのスキーマレスアーキテクチャは祝福でもあり呪いでもある。 オブジェクトごとに、その場で簡単にスキーマを変更することができるが、しかしALTER TABLEによるバルク操作をすることはできない。 Objectifyは、構造変更のための単純だがパワフルなツールを提供している。

Objectifyによる基本的なスキーママイグレーションは以下のようになる。

以下に一般的なケースを示す。

フィールドを追加あるいは削除する

これは簡単だ。やるだけだ!

クラスにはいかなるフィールドを追加することもできる。 そのフィールドに結びついたデータがデータストアに無いとしたら、クラスの初期化時にデフォルト値のままになる。 JDOでしばしば起こる例外の世界よりも良いだろう。

クラスからフィールドを削除することもできる。エンティティがget()されるとき、データストア中のデータは無視される。 put()すると、このフィールドを除いてエンティティがセーブされる (訳注:データストア中のフィールド値が無くなるという意味なのかは不明だが、後の説明を見ると、無くなるらしい)。

フィールドの名称変更

以下のようなエンティティがあるものとする。

public class Person
{
    @Id Long id;
    String name;
}

リファクタリングの後に、「name」というフィールドを「fullName」という名前に変更したいとする。もちろん可能だ。

public class Person
{
    @Id Long id;
    @AlsoLoad("name") String fullName;
}

Personがget()されるとき、fullNameフィールドには、fullNameかあるいはnameの値がロードされる。 両方共存在する場合はIllegalStateExceptionが投げられる。 put()すると、fullNameのみが書き込まれる。

警告: クエリは名称変更のことは知らない。もし"fullName"でフィルタリングしたとすると、変換済のエンティティしか得ることはできない。また、古いエンティティについて"name"でフィルタリングすることができる。

データ変換

さて、データを新しいPersonのフォーマットにマイグレーションできたとする。次は、単一のfullNameフィールドではなく、分離したfirst/last nameにしてみよう。 Objectifyでは次のようにする。

public class Person
{
    @Id Long id;
    String firstName;
    String lastName;

    void importCruft(@AlsoLoad("fullName") String full)
    {
        String[] names = full.split(" ");
        this.firstName = names[0];
        this.lastName = names[1];
    }
}

You can specify @AlsoLoad on the parameter of any method that takes a single parameter. The parameter must be type-appropriate for what is in the datastore; you can pass Object and use reflection if you aren't sure. Process the data in whatever way you see fit. When the entity is put() again, it will only have firstName and lastName.

Caution: Objectify has no way of knowing that the importCruft() method has loaded the firstName and lastName fields. If both fullName and firstName/lastName exist in the datastore, the results are undefined.

Enumの変更

Changing enum values is just a special case of transforming data. Enums are actually stored as Strings (and actually, all fields can be converted to String automatically), so you can use an @AlsoLoad method to process the data.

Let's say you wanted to delete the AQUA color and replace it with GREEN:

public enum Color { RED, GREEN }    // AQUA has been removed from code but it still exists in the datastore

public class Car
{
    @Id Long id;
    Color color;

    void importColor(@AlsoLoad("color") String colorStr)
    {
        if ("AQUA".equals(colorStr))
            this.color = Color.GREEN;
        else
            this.color = Color.valueOf(colorStr);
    }
}

The @AlsoLoad method automatically overrides the loading of the Color field, but the Color field is what gets written on save. Note that you cannot have conflicting @AlsoLoad values on multiple methods.

フィールドの移動

エンティティの構造を変更することは、スキーママイグレーションの中でも間違いなく困難な類だろう。 おそらく、二つのスキーマを一つにしたり、@Embeddedフィールドを分離したエンティティにするといったものだ。 多くの異なるアプローチを要求する、多くの可能なシナリオがあるのだが、重要なツールとしては以下だ。

Let's say you have some embedded address fields and you want to make them into a separate Address entity. You start with:

public class Person
{
    @Id Long id;
    String name;
    String street;
    String city;
}

You can take two general approaches, either of which can be appropriate depending on how you use the data. You can perform the transformation on save or on load. Here is how you do it on load:

public class Address
{
    @Id Long id;
    String street;
    String city;
}

public class Person
{
    @Id Long id;
    String name;

    @NotSaved String street;
    @NotSaved String city;

    Key<Address> address;

    @PostLoad void onLoad(Objectify ofy)
    {
        if (this.street != null || this.city != null)
        {
            this.address = ofy.put(new Address(this.street, this.city));
            ofy.put(this);
        }
    }
}

If changing the data on load is not right for your app, you can change it on save:

public class Address
{
    @Id Long id;
    String street;
    String city;
}

public class Person
{
    @Id Long id;
    String name;

    @NotSaved String street;
    @NotSaved String city;

    Key<Address> address;

    @PrePersist void onSave(Objectify ofy)
    {
        if (this.street != null || this.city != null)
        {
            this.address = ofy.put(new Address(this.street, this.city));
        }
    }
}

If you have an especially difficult transformation, post to the objectify-appengine google group. We're happy to help.

@Embedded

Objectify supports embedded classes and collections of embedded classes. This allows you to store structured data within a single POJO entity in a way that that remains queryable. With a few limitations, this can be an excellent replacement for storing JSON data.

Embedded Classes

You can nest objects to any arbitrary level.

class LevelTwo {
    String bar;
}

class LevelOne {
    String foo;
    @Embedded LevelTwo two
}

class EntityWithEmbedded {
    @Id Long id;
    @Embedded LevelOne one;
}

Embedded Collections and Arrays

You can use @Embedded on collections or arrays:

class EntityWithEmbeddedCollection {
    @Id Long id;
    @Embedded List<LevelOne> ones = new ArrayList<LevelOne>();
}

Some things to keep in mind:

Indexing Embedded Classes

As with normal entities, all fields within embedded classes are indexed by default. You can control this:

@Indexed
class LevelTwo {
    @Indexed String gamma;
    String delta;
}

@Indexed
class LevelOne {
    String beta;
    @Unindexed @Embedded LevelTwo two;
}

@Unindexed
class EntityWithComplicatedIndexing {
    @Id Long id;
    @Embedded LevelOne one;
    String alpha;
}

If you persist one of these EntityWithComplicatedIndexing objects, you will find:

alpha not indexed one.beta indexed one.two.gamma indexed one.two.delta not indexed

Note that one.two.delta is not indexed; the annotation on LevelOne.two overrides LevelTwo's class default. Querying By Embedded Fields

For any indexed field, you can query like this:

Objectify ofy = ObjectifyService.begin();
ofy.query(EntityWithEmbedded.class).filter("one.two.bar =", "findthis");

Filtering works for embedded collections just as it does for normal collections:

Objectify ofy = ObjectifyService.begin();
ofy.query(EntityWithEmbeddedCollection.class).filter("ones.two.bar =", "findthis");

Entity Representation

You may wish to know how @Embedded fields are persisted so that you an access them through the Low-Level API. Here is an example:

class LevelTwo {
    String bar;
}

class LevelOne {
    String foo;
    @Embedded LevelTwo two
}

class EntityWithEmbedded {
    @Id Long id;
    @Embedded LevelOne one;
}

EntityWithEmbedded ent = new EntityWithEmbedded();
ent.one = new LevelOne();
ent.one.foo = "Foo Value";
ent.one.two = new LevelTwo();
ent.one.two.bar = "Bar Value";

Objectify ofy = ObjectifyService.begin();
ofy.put(ent);

This will produce an entity that contains:

one.foo "Foo Value" one.two.bar "Bar Value"

You can see why query filters work the way they do.

For @Embedded collections and arrays, the storage mechanism is more complicated:

EntityWithEmbeddedCollection ent = new EntityWithEmbeddedCollection();
for (int i=1; i<=4; i++) {
    LevelOne one = new LevelOne();
    one.foo = "foo" + i;
    one.two = new LevelTwo();
    one.two.bar = "bar" + i;

    ent.ones.add(one);
}

Objectify ofy = ObjectifyService.begin();
ofy.put(ent);

This will produce an entity that contains:

ones.foo        ["foo1", "foo2", "foo3", "foo4"]
ones.two.bar    ["bar1", "bar2", "bar3", "bar4"]

This is what the entity would look like if the second and third values in the ones collection were null:

ones.foo^null   [1, 2]
ones.foo        ["foo1", "foo4"]
ones.two.bar    ["bar1", "bar4"]

The synthetic ^null property only exists if the collection contains nulls. It is never indexed.

Schema Migration

The @AlsoLoad annotation can be used on any field, including @Embedded fields. For example, this class will safely read in instances previously saved with EntityWithEmbeddedCollection:

class Together {
    @AlsoLoad("foo") String partOne;
    @AlsoLoad("two.bar") String partTwo;
}

class NextEntity {
    @Id Long id;
    @AlsoLoad("ones") @Embedded List<Together> stuff = new ArrayList<Together>();
}

@AlsoLoad methods work as well, however you cannot use @Embedded on method parameters.

Embedded Maps

There is one additional special behavior of @Embedded: If you put it on a Map keyed by String, this will allow you to create "expando" dynamic properties. For example:

class MyEntity {
    @Id Long id;
    @Embedded Map<String, String> stuff = new HashMap<String, String>();
}

MyEntity ent = new MyEntity();
ent.stuff.put("foo", "fooValue");
ent.stuff.put("bar", "barValue");
ofy.put(ent);

...will produce this entity structure:

stuff.foo       "fooValue"
stuff.bar       "barValue"

If the Map field is indexed, you can filter by "stuff.foo" or "stuff.bar".

Note that while the Map value can be of any type, the Map key must be String.

@Serialized

An alternative to @Embedded is to use @Serialized, which will let you store nearly any Java object graph.

class EntityWithSerialized {
    @Id Long id;
    @Serialized Map<Object, Object> stuff;
}

There are some limitations:

However, there are significant benefits to storing data this way:

You are strongly advised to place serialVersionUID on all classes that you intend to store as @Serialized. Without this, any change to your classes will prevent stored objects from being deserialized on fetch. Example:

class SomeStuff implements Serializable {
    /** start with 1 for all classes */
    private static final long serialVersionUID = 1L;

    String foo;
    Object bar;
}

class EntityWithSerialized {
    @Id Long id;
    @Serialized SomeStuff stuff;
}

Caching

Objectify provides two different types of caches:

You must explicitly decide to use these caches. If you do nothing, every get() will read through to the datastore.

Session Cache

The session cache associates your entity object instances with a specific Objectify instance. You must explicitly enable it by passing in ObjectifyOpts to the ObjectifyService.begin() method:

    ObjectifyOpts opts = new ObjectifyOpts().setSessionCache(true);
    Objectify ofy = ObjectifyService.begin(opts);

Note:

Global Cache

Objectify can cache your entity data globally in the appengine memcache service for improved read performance. This cache is shared by all running instances of your application.

The global cache is enabled by default, however you must still annotate your entity classes with @Cached to make them cacheable:

@Cached
public class MyEntity {
    @Id Long id;
    ...
}

That's it! Objectify will utilize the memcache service to reduce read load on the datastore.

What you should know about the global cache:

Warning: Objectify's global cache support prior to v3.1 suffered from synchronization problems under contention. Do not use it for entities which require transactional integrity, and you are strongly advised to apply an expiration period to all cache values.

The cache in 3.1 has been rewritten from scratch to provide near-transactional consistency with the datastore. Only DeadlineExceededException should be able to produce synchronization problems.

For more commentary about the new v3.1 cache, see MemcacheStandalone.

Example

Andrew Glover wrote an excellent article for IBM developerWorks: Twitter Mining with Objectify-Appengine, part 1 and part 2.

last edited 2012-01-14 05:25:57 by ysugimura