*

Datastoreモデル変更の影響調査

公開日: : 最終更新日:2015/11/05 投稿者: GAE ,

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;
 (ゲッター、セッター省略)
 }

MetaクラスでのmodelToEntity()は、

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);
 }

今回はパターン別にDatastoreのEntityについてどのように取り扱えば良いかを説明しました。
必要のないkindやプロパティの削除などのDatastoreの整理をする時に使えますので、この記事を参考にしていただければ幸いです。
次回は発表がのびのびになってしまっていた『GAE負荷テスト その2「吉積情報株式会社の旧トップページ」』を発表したいと思います。

この記事を書いた人

ayatoshi
ayatoshi
徳島県出身、吉積情報株式会社最高技術責任者
愛光高校、東京大学卒業後、アクセンチュアにて5年間システム開発を経験。
CP300のアジア初取得者。
Google基盤上でのシステム開発の普及を目標として日々活動中。

関連記事

WordPressをGAEで簡単に使う10のステップ

2015/10/05 テーマやプラグインの追加・変更方法について下部に追記しております。 み

記事を読む

専用Memcacheは共有Memcacheよりも本当に性能が高いのか検証する

GAEでMemcacheを利用した経験はありますでしょうか? MemcacheとはGAEで利用

記事を読む

1つのエンティティにプロパティをいくつまで作れるか

1つのエンティティにプロパティをいくつまで作れるか 1つのエンティティにプロパティをいくつまで

記事を読む

AppEngineでTwilioを試してみた(応用編)

AppEngineでTwilioを試してみた(基本編) AppEngineでTwilioを試し

記事を読む

Prediction API入門(後編)

今回はPrediction API on GAE/J みなさん、こんにちは。Prediction

記事を読む

GAEのスケーリング 後編 <最適化の実践>

この記事では、こちらの公式ドキュメントをもとに、GAEのスケーリングの仕組みと最適化のやり方について

記事を読む

GCEにPuttyから簡単接続する

年末ではありますが、先日ちょっとGCE(Google Compute Engine)を触る機会がまた

記事を読む

Search API詳細解説 Part4「Search API 詳細 検索性能編」

Search API詳細解説シリーズ タイトル Part1Search API 概要説明

記事を読む

Datastoreの仕組み 〜Consistencyについて〜

はじめに スケーラビリティと可用性が高いと言われているGoogle Cloud Datastore

記事を読む

静的HTMLをGAE上で公開する

みなさんこんにちは。 本日は、静的HTMLをGAEで公開する手順を紹介します。 本サイトを運営し

記事を読む

PAGE TOP ↑