2014年8月20日水曜日

【GAE】初級実装編 インサート実行1

株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。

只今、クラウド基盤「Google App Engine(以下、GAE)」の連載しています。

今回は前回に引き続き、GAEにおける「インサート」の処理についてご紹介です。

put

ではまず、何も考えずにスポッと値を入れるだけの処理を記載します。

前回の段階で「モデル」と「DAO」は作成済みですので、これを使ってレコードの登録を行います。
そのソースが以下です。

 Shohin shohin = new Shohin();
  ShohinDao dao = new ShohinDao();
  dao.put(shohin);

これだけです。
put一発で終わり。空っぽですが、1レコード増えています。
実に簡単ですね。

とはいえ、余りにシンプル過ぎるが故に、「これってどういう風にデータの辻褄を合わせているんだろう?」という疑問が沸いて当然。
この為、今回はその辺りについての疑問解決がメインテーマになります。

登録更新の区別が無い

まず第一の特徴は、GAEにおけるデータ保存のやり方は「insert」ではなく「put」なんです。
put処理というのは、レコードが存在していたら上書き、無ければ新規保存です。

SQLと違って、「新規登録」と「更新」の区別が無いのです。

これはJavaのMapと全く同じです。

 Shohin shohin = new Shohin();
  Map map = new HashMap();
  map.put("ID_0001",shohin);

これが大量データ処理の秘密の一つなのです。
要はDBがハッシュで管理されているから、レコード数がどれだけ大量に増えても均一の速度を保てているというわけなのです。
(ハッシュを使うと検索速度がレコード数に依存しなくなるという件については、基本情報処理試験等でご理解下さい)

なので、GAEで「新規登録」と「更新」を実現したい場合は、前もって検索して、

  • GigTable上に予定レコードが存在していなかった場合、単純にput。
  • GigTable上に予定レコードが存在していた場合、まずテーブル上の値を丸ごと取得し、更新したいパラメータだけを差し替えてput。

こういうロジックを組んで対応する必要があります。

ここで一つ、GAEの特徴が垣間見えてきましたね。そう、GAEは基本的にロジック対応で乗り切るモノです。

例えばですね、「商品の値段が100円になっているものを、全部150円に一括で値上げしたい」とかいう要求があるとするじゃないですか。

その場合、SQLだったら「update shohin set price = 150 where price = 100;」みたいな感じに一発で全レコード更新すればOKです。
しかし、GAEの場合は「更新」なんか存在しませんので、まずは「値段が100円である商品」で検索を行い、それから1レコードずつJavaでpriceの値を150に書き換えてputするのです。

GAEは1レコード単位でしか登録、更新、削除出来ない!!

要望は殆ど全部Javaで頑張ってロジックを組んで何とかするのです!!



ちなみに、上記の通り、GAE大量レコードの更新をする場合でも1レコードずつの逐次処理になってしまいますので、「一括更新、一括削除」が主力になるシステムには向かないということです。
こういった検索機能についての向き不向きは追々検証していきたいと思いますので、今回の所は触り程度にご認識頂ければと思います。

主キー

次に疑問に思うのは、「主キー」です。

上記にある「無ければ登録、あれば上書き」という挙動は「主キー検索して、ある/無い」という話です。
その「主キー」とはどこにあるかと言いますと、商品モデルの中の「Key」というフィールドです。

 /** キー */
 @Attribute(primaryKey = true)
 private Key key;

このKeyはちょっと独特な動きをしまして、「自動モード」と「手動モード」があります。

自動モード

まず、上記でご紹介したように「何もせずにput」を行うと、勝手に上記「key」にシーケンス番号がセットされます。
PostgreSql等で「serial」という型を主キーに定義しておくと、空で保存した時に勝手にシーケンス番号で値が入っていきますが、あれとそっくりな動きです。


この「主キーがシリアル」というのは、ちょっとしたDB設計のポリシーが入ってくる部分ですので、ちょっと込み入った話を。


例えば、商品を管理するようにあシステムの場合、「商品毎に商品IDを明示的に決めて、それを主キーにする」という設計になることが多いと思います。
「商品IDで一意」と決まっている以上は、それを主キーにするのが最も合理的ですよね?

しかし、昨今のシステム業界を見ますと、「商品IDは一意キーとして別途制約を入れて、主キーはシリアルにする」という思想が出て来ています。
Ruby On Railsはこの思想です。

このやり方の場合、DB的には冗長になりますが、「プログラミングし易い」「ソース粒度が均質化する」「マッピングし易い」などの利点があるのです。

私もこのやり方に賛同する所がありまして、多少ファイルスペースを無駄遣いしてもシリアルを使わせて貰っちゃったりするのが最近のブームです。

GAEもこれに近い思想で構築されておりまして、「主キーはシリアル」がGAEの標準です。

特に拘りが無ければ、この標準の自動シリアルモードで作って行くのが良いのではないでしょうか?

手動モード

一方で、「主キーは商品IDを明示的にセットする」みたいな手動モードが必要になる場合もあります。

その場合は、「KeyFactory.createKey」というツールでキーを生成しなければなりません。

 Shohin shohin = new Shohin();
  Key key = KeyFactory.createKey("Shohin", "49033011234567");
  shohin.setKey(key);
  ShohinDao dao = new ShohinDao();
  dao.put(shohin);

「KeyFactory.createKey("Shohin", "49033011234567");」のうち、「Shohin」がテーブル名を意味して、「49033011234567」が商品IDを意味します。

こんな感じで任意パラメータを主キーにすることが出来るのです。

ただ、やっぱ作りとしては汚くなるという印象がありますね。
やはり基本は自動モードで実装した方が合理的で、もし一意に保ちたいカラムがある場合は、別途「一意制約」を設ける方がシステムとして整合性が取れると思います。

続く

しかしですね、上記に「一意キー」とありますが、実は、GAEに一意キーは無いのです。

GAEのDB制約は「主キー制約」しかありませんので、「主キー制約を上手く使って一意制約を入れる」というテクニックが必要になります。

次回はインサート実行後編、「一意制約の作り方」をご紹介します。

2014年8月17日日曜日

【GAE】初級実装編 Slim3モデルクラス作成

株式会社ジェニシス 技術開発事業部の遠藤 太志郎(Tacy)です。

只今、クラウド基盤「Google App Engine(以下、GAE)」の連載しています。

では、今回よりGAEの目玉である「BigTable」の使い方をご紹介してきます。

ただし、GAEには標準でBigTableを利用する手段が用意されていますが、今回のサンプルアプリではSlim3を導入していますので、Slim3経由でBigTableを使用する方法として、ご紹介致します。

テーブル作成

さて、まずSlim3では「データモデル型」にてデータをやりとりし、それがそのままDBの定義になります。

普通のJavaシステムの場合、まず最初にDBのデータモデル定義があり、後でそれとピッタリに合わせたJavaモデルクラスを作成しますよね?

まあ、先にJavaクラスを作って、後でDBをリバース作成するライブラリもありますが、とにかく普通のシステムの場合は、
テーブルを作る時はJavaのモデル作成とは別途「create table」を発行しています。

Slim3の場合は違いまして、「Javaクラスがそのまま勝手にテーブルになる」という構成です。

Testという名前のクラスを作って保存処理を実行したら、自動的にTestというテーブルが出来て保存されるというカラクリです。

もちろん、テキトーにクラスを作れば良いという話ではなく、ちゃんと指定されたインターフェースを実装したり、アノテーションを宣言したりが必要となりますが、
その辺りはSlim3の自動機能にお任せです。

ビルド

では、さっそく作ってみましょう。

Slim3でプロジェクトを作成すると、以下のような構成になっているはずです。


一番下に「build.xml」がありますよね?
コイツを右クリックから実行します。

すると、以下のような画面が出て来ます。


親切にも色々と作ってくれる機能が揃っておりますが、今回の所は「gen-model-with-dao」を選択します。

MODELクラスとDAOクラスを1:1のセットで作成してくれる機能です。

その次にクラス名を指定する画面が出ますので、Shohinとでもセットします。



これにより、以下のようなShohinモデルのクラスが出来上がりました。
簡単ですね。

@Model(schemaVersion = 1)
public class Shohin implements Serializable {

 private static final long serialVersionUID = 1L;

 @Attribute(primaryKey = true)
 private Key key;

 @Attribute(version = true)
 private Long version;

 /**
  * Returns the key.
  * 
  * @return the key
  */
 public Key getKey() {
  return key;
 }

 /**
  * Sets the key.
  * 
  * @param key
  *            the key
  */
 public void setKey(Key key) {
  this.key = key;
 }

 /**
  * Returns the version.
  * 
  * @return the version
  */
 public Long getVersion() {
  return version;
 }

 /**
  * Sets the version.
  * 
  * @param version
  *            the version
  */
 public void setVersion(Long version) {
  this.version = version;
 }

 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((key == null) ? 0 : key.hashCode());
  return result;
 }

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (obj == null) {
   return false;
  }
  if (getClass() != obj.getClass()) {
   return false;
  }
  Shohin other = (Shohin) obj;
  if (key == null) {
   if (other.key != null) {
    return false;
   }
  } else if (!key.equals(other.key)) {
   return false;
  }
  return true;
 }
}


フィールド設定

次に、モデルクラスにフィールド変数を定義してテーブルカラムを作成します。

と言っても、「変数名が勝手にDBのカラムになる」というだけですので、普通に「private String shohinId;」とか定義していけばOKです。

その結果がこちら。

/**
 * 商品モデル。
 * 
 * @author Tashiro Endo
 * 
 */
@Model(schemaVersion = 1)
public class Shohin implements Serializable {

 /** シリアルバージョン */
 private static final long serialVersionUID = 1L;

 /** キー */
 @Attribute(primaryKey = true)
 private Key key;

 /** バージョン */
 @Attribute(version = true)
 private Long version;

 /** 商品ID(keyのIDの文字列化) */
 private String shohinId;

 /** 商品名 */
 private String shohinName;

 /** 在庫数 */
 private Integer stock;

 /** 出庫可能日 */
 private Date deliveryDate;

 /** 登録日時 */
 @Attribute(listener = CreationDate.class)
 private Date createdAt;

 /** 更新日時 */
 @Attribute(listener = ModificationDate.class)
 private Date updatedAt;

 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((key == null) ? 0 : key.hashCode());
  return result;
 }

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (obj == null) {
   return false;
  }
  if (getClass() != obj.getClass()) {
   return false;
  }
  Shohin other = (Shohin) obj;
  if (key == null) {
   if (other.key != null) {
    return false;
   }
  } else if (!key.equals(other.key)) {
   return false;
  }
  return true;
 }

 /**
  * キーを取得します。
  * 
  * @return キー
  */
 public Key getKey() {
  return key;
 }

 /**
  * キーを設定します。
  * 
  * @param key キー
  */
 public void setKey(Key key) {
  this.key = key;
 }

 /**
  * バージョンを取得します。
  * 
  * @return バージョン
  */
 public Long getVersion() {
  return version;
 }

 /**
  * バージョンを設定します。
  * 
  * @param version バージョン
  */
 public void setVersion(Long version) {
  this.version = version;
 }

 /**
  * 商品ID(keyのIDの文字列化)を取得します。
  * 
  * @return 商品ID(keyのIDの文字列化)
  */
 public String getShohinId() {
  return shohinId;
 }

 /**
  * 商品ID(keyのIDの文字列化)を設定します。
  * 
  * @param shohinId 商品ID(keyのIDの文字列化)
  */
 public void setShohinId(String shohinId) {
  this.shohinId = shohinId;
 }

 /**
  * @inheritDoc
  */
 @Override
 public String toString() {

  StringBuilder bul = new StringBuilder();

  bul.append("key=").append(key).append(",");
  bul.append("version=").append(version).append(",");
  bul.append("shohinId=").append(shohinId).append(",");
  bul.append("shohinName=").append(shohinName).append(",");
  bul.append("stock=").append(stock).append(",");
  bul.append("deliveryDate=").append(deliveryDate).append(",");
  bul.append("createdAt=").append(createdAt).append(",");
  bul.append("updatedAt=").append(updatedAt);

  return bul.toString();
 }

 /**
  * 商品名を取得します。
  * 
  * @return 商品名
  */
 public String getShohinName() {
  return shohinName;
 }

 /**
  * 商品名を設定します。
  * 
  * @param shohinName 商品名
  */
 public void setShohinName(String shohinName) {
  this.shohinName = shohinName;
 }

 /**
  * 在庫数を取得します。
  * 
  * @return 在庫数
  */
 public Integer getStock() {
  return stock;
 }

 /**
  * 在庫数を設定します。
  * 
  * @param stock 在庫数
  */
 public void setStock(Integer stock) {
  this.stock = stock;
 }

 /**
  * 出庫可能日を取得します。
  * 
  * @return 出庫可能日
  */
 public Date getDeliveryDate() {
  return deliveryDate;
 }

 /**
  * 出庫可能日を設定します。
  * 
  * @param deliveryDate 出庫可能日
  */
 public void setDeliveryDate(Date deliveryDate) {
  this.deliveryDate = deliveryDate;
 }

 /**
  * 登録日時を取得します。
  * 
  * @return 登録日時
  */
 public Date getCreatedAt() {
  return createdAt;
 }

 /**
  * 登録日時を設定します。
  * 
  * @param createdAt 登録日時
  */
 public void setCreatedAt(Date createdAt) {
  this.createdAt = createdAt;
 }

 /**
  * 更新日時を取得します。
  * 
  * @return 更新日時
  */
 public Date getUpdatedAt() {
  return updatedAt;
 }

 /**
  * 更新日時を設定します。
  * 
  * @param updatedAt 更新日時
  */
 public void setUpdatedAt(Date updatedAt) {
  this.updatedAt = updatedAt;
 }
}

単純にフィールド変数を追加して、getter/setterを自動出力すればOKです。
getter/setterは必須ですので、ご注意を。

なお、上記では「String」「Integer」が追加されていますが、これは何でもアリというわけではなく、限られた一部のクラスしかフィールドとして定義してはいけません。


  • 500文字以下の短い文字列の場合は、String
  • 整数なら、Ingeter
  • Ingeterで収まらない大きい数字の場合は、Long
  • 日付の場合は、Date


こんな感じに決まっています。
直感で大体分かると思いますが、公式サイトにちゃんとした一覧がありますので、必読です。


アノテーション「@Attribute」の「CreationDate.class」と「ModificationDate.class」

上のソースをご覧になると気付かれたかと思いますが、モデルクラスには「@Attribute」というアノテーションがフィールドに付与されているものがありまして、
これがモデルクラス特有の役割を持ちます。

「(primaryKey = true)」とか大事な機能につきましては、次回以降について記事にするとしまして、
今回の所は小ネタ機能である「CreationDate.class」と「ModificationDate.class」についてご紹介します。

これは、「登録日と更新日を自動セットする」という機能です。
これについても上記URLに記載があります。

「登録日と更新日」は、まあこれから作るシステム上、特に要件には考慮されていないものですけれども、保存しておいて損は無い情報ですよね?
なので、私は全モデルクラスに、この登録日と更新日は持たせるということでルールを統一して作っています。

この記事ではソースを張る都合でShohinクラスに実装していますが、実際には「AbstractModel」というクラスを自分で作って、
そこに「CreationDate.class」と「ModificationDate.class」を定義しています。

モデルクラスは継承も出来ますので、共通で使うフィールドは継承してOK!!


このように、Slim3は探してみるとチョコチョコ便利な機能が色々揃っていたりしてくれていますので、
公式サイトを見ながらあれこれ実験してみると面白いですね。


終わりに

以上で、通常システムで言う所の「create table」が完了した状態にあります。

次回は、実際にレコードを登録する「insert」に該当する機能の実装を行います。