GoogleAppEngine(以下GAEと呼称)に限らず、開発を行なっていると『テーブルの構造を変更しなければならない』ということが起こると思います。
Datastoreの場合、単純にモデルクラスの変数の型を変更しますと、変更前後の型によってはキャストエラーが出てしまい、動かないというような
事態になってしまいます。
今回は弊社研究員がDatastoreモデルのEntityの構造とプロパティの変更方法と変更に伴う影響までを調べましたので発表したいと思います。
なお、今回の調査ではSlim3フレームワーク(以下Slim3と呼称)を使用しています。
目次
Slim3におけるEntityの扱いについて
DatastoreモデルのEntityが構造であるのかとSlim3ではEntityがどのように扱われるのかを見てみましょう。
【注意点】
この項目では、Slim3でEntityがどのように扱われているかを書いています。Slim3に詳しい方は「2. クラス型に変更がある場合」から読み始めてください。
まずDatastoreでは1行分のデータを『Entity』と呼んでいます。
※EntityはDatastoreのローレベルAPIで、Datastoreに保存するときの基本的な単位です。
Datastoreへの保存時にEntityには一意のKeyが割り当てられています。
Entityは『kind』でその種類が区別されます。RDBで言うところのテーブル名です。
Entityは『property』と呼ばれる値を持つことができ、propertyは名前と値のペアで設定できます。RDBで言うところのカラムと値になります。
例えば、Exampleというkind名のEntityを保存する場合は以下のようになります。
public void exampleTest {
// DatastoreにアクセスするためのDatastoreServiceを取得します。
DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
// Exampleというkindのオブジェクトを作成します。
Entity entity = new Entity("Example");
// property名:versionに値:1を設定します。
entity.setProperty("version","1");
// Datastoreに保存します。戻り値は、Entityに割り当てられた一意のKey値です。
Key key = datastoreService.put(entity);
}
Slim3では、POJOを「Entityにする・Entityから戻す」ということをします。
これは、Metaクラスの中のentityToModelまたmodelToEntityメソッドを見てみるとよく解ると思います。
※Slim3では、「@Model」でクラスをモデルとして定義すると、Metaクラスを自動生成します。
MetaクラスはEntityをPOJOとして扱えるようにするためのクラスです。
例えば以下のようなTestOneというモデルクラスがあった場合、
@Model
public class TestOne {
private Key key;
private String stringMember;
private int intMember;
(ゲッター、セッター省略)
}
public Entity modelToEntity(Object model) {
TestOne m = (TestOne) model;
Entity entity = null;
if (m.getKey() != null) {
entity = new Entity(m.getKey());
} else {
entity = new Entity(kind);
}
entity.setProperty("intMember", m.getIntMember());
entity.setProperty("stringMember", m.getStringMember());
entity.setProperty("version", m.getVersion());
return entity;
}
のようにして、POJOをEntityに変換しています。
EntityではgetProperty()・setProperty()により、Mapの様に扱うことができます。
modelToEntity()では、この性質を利用してクラスのフィールド値をEntityに変換しています。
このことから、DatastoreにあるEntityにはクラスの情報が保存されていないことが解ります。
(ただ、Slim3だと「@Model」のkind属性がnullだった場合、kind名にクラス名があてられます。)
また、Datastoreから読み込んだEntityをクラスオブジェクトに戻す場合は、
public TestOne entityToModel(Entity entity) {
TestOne model = new TestOne();
model.setIntMember(longToPrimitiveInt((Long) entity.getProperty("intMember")));
model.setStringMember((String) entity.getProperty("stringMember"));
model.setKey(entity.getKey());
model.setVersion((java.lang.Long) entity.getProperty("version"));
return model;
}
のようにして、EntityからPOJOに変換しています。
これらは全部、Slim3が裏側でやってくれていることで、プログラマーが意識する必要はありません。
ただ、Slim3ではタイプなし(typeless)のEntityの読み込みも可能です。
例えば、クラスとしてはTestOneが存在していなくても、下記のようにすればEntityのままDatastoreから読み出すことができます。
List<Entity> entities = Datastore.query("TestOne").asList();
稼働中システムのクラス型(Entity)に変更がある場合の挙動と問題点
次はDatastoreモデルの変更について、4つのパターンで説明したいと思います。
モデル単位での追加・削除の場合
【結果】
削除の場合 ⇒ モデル・Metaクラスがなくても、すでにDatastoreにあったEntityはそのままDatastoreに残っています。
例:モデルクラス『ReadHistory』を削除した場合。
削除前DatastoreのKind一覧 | 削除後DatastoreのKind一覧 | |
Photo | Photo | |
PersonalInformation | → | PersonalInformation |
Diary | → | Diary |
ReadHistory | → | ReadHistory← Datastoreには残っています。 |
Friend | Friend |
追加の場合 ⇒ 新しいモデルのMetaクラスが現れます。
例:モデルクラス『Option』を新規作成・追加した場合。
削除前DatastoreのKind一覧 | 削除後DatastoreのKind一覧 | |
Photo | Photo | |
PersonalInformation | → | PersonalInformation |
Diary | →→ | Diary |
ReadHistory | ReadHistory | |
Friend | Friend | |
Option ← Datastoreに追加されます。 ただし、Entityが作成されなければ追加はGAEの管理コンソールからkindは確認できません。 |
【対応方法】
削除の場合 ⇒ 不要なら元のモデルのモデル名で残っているEntityを読み込んで、削除します。
追加の場合 ⇒ 対応の必要はありません。
しかし、追加されたモデル名と同じモデル名が既にあり、プロパティ名が同じで型だけが違うという場合は、キャストエラーが発生する可能性がありますので、プロパティの型変更や削除、もしくはモデルの削除が必要になります。
プロパティの追加、削除の場合
【結果】
削除の場合 ⇒ MetaクラスのentityToModel()・modelToEntity()からその項目が消える。
例:モデルクラス『Diary』のプロパティ『number』を削除した場合。
プロパティ削除前Diaryクラス(entityToModel)
public TestOne entityToModel(Entity entity) {
Diary model = new Diary();
model.setKey(entity.getKey());
model.setText((String) entity.getProperty("text"));
model.setDate(entity.getProperty("date"));
model.setNumber(longToPrimitiveInt((Long) entity.getProperty("number")));
…
return model;
}
↓↓↓
public TestOne entityToModel(Entity entity) {
Diary model = new Diary();
model.setKey(entity.getKey());
model.setText((String) entity.getProperty("text"));
model.setDate(entity.getProperty("date"));
…
return model;
}
追加の場合 ⇒ MetaクラスのentityToModel()・modelToEntity()にそのプロパティが現れる。
例:モデルクラス『Diary』のプロパティ『page』を追加した場合。
プロパティ追加前Diaryクラス(entityToModel)
public TestOne entityToModel(Entity entity) {
Diary model = new Diary();
model.setKey(entity.getKey());
model.setText((String) entity.getProperty("text"));
model.setDate(entity.getProperty("date"));
…
return model;
}
プロパティ追加後Diaryクラス(entityToModel)
public TestOne entityToModel(Entity entity) {
Diary model = new Diary();
model.setKey(entity.getKey());
model.setText((String) entity.getProperty("text"));
model.setDate(entity.getProperty("date"));
model.setPage(longToPrimitiveInt((Long) entity.getProperty("page")));
…
return model;
}
【対応方法】
削除の場合 ⇒ 古いモデルを新しいモデルに読み込んでも問題ありません。
ただし、古いモデルを改めて保存する(上書き)まで、Datastoreには古いモデルのプロパティが残っています。
例:モデルクラス『Diary』のプロパティ『number』を削除した場合。
プロパティ削除前 Datastore Entity[Diary] |
プロパティ削除後 Datastore Entity[Diary] |
|
key value : xxxxxx type : Key date value : 2012-12-03 04:59:09.870000 type : gd:when text value : 今日はいい天気です。 type : string number value : 2 type : int … |
→ → → |
key value : xxxxxx type : Key date value : 2012-12-03 04:59:09.870000 type : gd:when text value : 今日はいい天気です。 type : string number ← 削除しても上書きするまでは残っています value : 2 type : int … |
追加の場合 ⇒ 古いモデルのものを読み込んでも問題ありません。新しいプロパティがデフォルト値になります。
プロパティの名称変更の場合
【結果】
新しいプロパティが追加される。(「②プロパティの追加、削除の場合」と同じです。)
例:モデルクラス『PersonalInformation』のプロパティ『name』を『fullName』に変更した場合。
プロパティ名変更前Datastore Entity[PersonalInformation] | プロパティ名変更後Datastore Entity[PersonalInformation] | |
name value : xxxxxx type : string age value : 20 type : int address value : ○○県??市 type : string … |
→ → → |
name ← プロパティを削除したときのように古いものも残っている。 value : xxxxxx type : string fullName ← 新しい項目として作成される。 value : xxxxxx type : string age value : 20 type : int address value : ○○県??市 type : string … |
【対応方法】
「2:項目の追加、削除の場合」の対応方法と同じです。
ただし、古い名称のプロパティにはアクセスできませんので、古いプロパティの値を更新したい場合は「3.解決策の例 2」をご覧ください。
項目の型変更の場合(パターンは、数値⇒文字列)
【結果】
MetaクラスのentityToModel()・modelToEntity()での型キャストが変更になります。
例えば IntMemberをString型にすると:
model.setIntMember(longToPrimitiveInt((java.lang.Long)entity.getProperty("intMember")));
model.setIntMember((java.lang.String) entity.getProperty("intMember"));
ただし、Datastoreに既にあるモデルの型は変更されないので、クラスオブジェクトとして読み込むとキャストエラーが発生する可能性が高いです。
(int⇒long変更くらいは大丈夫ですが)
【対応方法】
型変更によって影響のあるすべてのモデルをEntityとしてDatastoreから読み込んでから、変更をEntityレベルで行うの一番いいと思います。
後述する「3. 解決策の例」にコードを紹介していますので、参考にしてみてください。
解決策の例
最後は2の例を踏まえ、実際に「古いクラスの削除」と「クラスメンバーの追加・削除・型変更」のコードを紹介します。
1:Datastoreから古いクラスのEntityを削除
// TestOneという名前のEntityのKeyリストを取得する。
List<Key> keyList = Datastore.query("TestOne").asKeyList();
for (Key k : keyList) {
// Keyを使ってDatastoreのEntityを削除する。
Datastore.delete(k);
}
2:クラスメンバー追加・削除・型変更
以下のようにモデルクラスTestOneのプロパティを変更
コード9
モデルクラスのプロパティ変更前 | モデルクラスのプロパティ変更後 | |
@Model public class TestOne { private Key key; private int oldProperty; private int oldIntegerProperty; … } |
→ → → |
@Model public class TestOne { private Key key; private String newProperty; private String newStringProperty; … } |
例:コード9のようにモデルクラスを変更しましたが、前述したとおり、プロパティ名を変更しただけでは、DatastoreのEntityには古いプロパティ『oldProperty』と『oldIntegerProperty』がまだ存在しています。
下記のコード10では、古いプロパティの削除と、新しいプロパティの追加をEntityから行いたいと思います。
// TestOneという名前のEntityをリストで取得する
List<Entity> list = Datastore.query("TestOne").asList();
for (Entity entity : list) {
// Entityにプロパティが設定されているか確認する
if (entity.hasProperty("oldProperty")) {
// Entityのプロパティ削除 ⇒ クラスメンバー削除
entity.removeProperty("oldProperty");
}
if (!entity.hasProperty("newProperty")) {
// Entityのプロパティ追加 ⇒ クラスメンバー追加
entity.setProperty("newProperty", "default value");
}
// Entityのプロパティの型変更とプロパティの削除、新規プロパティの追加を行なう
if (entity.hasProperty("oldIntegerProperty")) {
// 注意点:int型はエンティティにlongになる可能性がある
long oldValue = (Long)entity.getProperty("oldIntegerProperty"); // 元の値を読込み
String newValue = Long.toString(oldValue); // 型の変更の例:IntからStringへ
entity.removeProperty("oldIntegerProperty"); // 元のプロパティ削除
entity.setProperty("newStringProperty", newValue); // 新しいプロパティ追加
}
// 変更を保存する
Datastore.put(entity);
}
必要のないkindやプロパティの削除などのDatastoreの整理をする時に使えますので、この記事を参考にしていただければ幸いです。
次回は発表がのびのびになってしまっていた『GAE負荷テスト その2「吉積情報株式会社の旧トップページ」』を発表したいと思います。