2014年9月10日水曜日

【GAE】初級実装編 インサート実行3 時間差反映

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

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

ここ数回はDBへのインサートについてご紹介していますが、今回がその最後です。

登録成功!! しかし……

前回までの処理で、無事にレコードをDBに登録する「put」が出来るようになりました。

では、さっそく、登録したレコードを取得する「get」をやってみましょう。

ShohinDao dao = new ShohinDao();
Shohin shohin = dao.get(key);

あれ?取れないな。
おかしいぞ。
もう一回!
よいしょ!!
よいしょ!!
お、取れた。

変ですね。
目の錯覚でしょうか?

違うんですよ。
GAEは遅延するんです!!

分散処理の宿命

そう、普通のDBであれば、テーブルに書き込んでコミットした瞬間に処理が完了しますから、遅延なんて起きません。

しかし、GAEは分散処理システムですからそうは行かないのです。
put処理が完了した後でも、その向こう側では非同期で色々と処理が走っており、それら全てが完了した時、初めてgetでレコードが取得出来るようになります。


この現象、ツイッターなんかやっていらっしゃる方だとご理解頂けると思います。

「ツイートするぜ、ポチッ!」とやっても、送信完了の瞬間に自分のツイートが反映されてませんよね?
でも数秒すると出てくるようになる。
アレが正にこの現象です。

即時性を犠牲する代わりに、可用性や耐久性などを優先しているのです。


こういうのを「結果整合性(eventual consistency)」と言います。


処理が多少遅延したとしても、最終的にはDBへの登録はもちろん、レプリケーションへの反映まで一通り一貫性を保って完了すればOKという考え方です。

「最終的にはちゃんとやっておくから、過程についてゴチャゴチャ言うな」と言わんばかりのこの発想。
日本人では出て来ない発想かもしれません。。。

どうすりゃいいの?

さて、問題はこの遅延とやらの所要時間ですね。

遅延するのは分かったけど、どれくらい遅延するのか?

ネットで調べた所、どうも「長くて1秒くらい」という声が見受けられます。
私も実際何度か実験してみましたが、同感ですね。大体こんなもんかと思います。

しかし、更にネットを調べてみますと「反映まで半日かかった」なんて声も見つかりました。

半日の遅延は流石に看過できませんが、要はこういう時は障害が起きているのです。
流石GAEということか、障害が起きても完全ダウンするわけではなく、大幅遅延でも縮退運転で耐える堅牢性を誇ります。
(半日も遅延させるんだったら、いっそのことダウンさせてくれた方がマシな気もしますが)

何が言いたいかと言いますと、結局、何秒遅延するかは分からないということです。

平常運転なら1秒未満で反映されますが、少々混み合っていた場合は数秒くらい要することだってあります。

つまり、『登録⇒即表示』を厳密にやらなきゃいけない仕様はNGと言う事です。

例えば、


  • 登録完了⇒即座に登録結果確認画面へ自動遷移。


なんて仕様は実現不可能です。
また、

  • 商品発注完了⇒確認画面をチェックしたけど何も出てない。⇒もう一回⇒二重発注。

なんて、運用的に確認が必須な業務の場合もキツいものがありますね。
こういう場合は、

「商品を発注しました。発注番号はXXXXXXです。発注履歴の確認は商品発注確認画面をご覧下さい。
システムが混み合っている場合は反映が遅れる場合があります。予めご了承下さい」

みたいに注意書きで逃れる、とかですかね。

「発注しましたッ!!」「発注番号はこれッ!!」という感じに発注完了メッセージにインパクトを持たせて、
「あれ~? 今、俺ってちゃんと操作したっけ? もう一回やってみよう」みたいな不安感を利用者に与えない画面設計が重要になります。

逆に、「二重送信したら後で消せばいいだろ」みたいな水準で十分な要件である場合は知らんぷりを決め込むのもアリ。

インターフェース設計のセンスが問われる所になりますね。

終わりに

これでDBへの登録関連は完了です。

次はDBから取り出した値を画面に表示するわけですが、GAEではjspを使わないのがお約束。

フルAjaxで勝負します。

次回からはAjaxライブラリを使った描画処理についてご紹介です。

2014年9月3日水曜日

【GAE】初級実装編 インサート実行2 一意制約

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

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

さて、前回ではGAEの最大の特徴である「BigTable」へのデータ登録をご紹介しました。

今回はそのデータ登録についての、ちょっと込み入った話、「一意制約」についてご紹介します。

GAEは主キーだけ

Oracle、postgreSQL、MySQLなどメジャー所のデータベースには、大概はDB独自の「制約」を設ける機能があります。


  • 主キー制約
  • 一意制約
  • 外部キー制約
  • null禁止制約


この辺りがよく使う制約だと思いますが、しかし、GAEには一意制約しか存在しないのです!!

何と不便な!!

要するに、GAEみたいなクラウド基盤は分散処理かつ大量データを取り扱っている都合上、負荷軽減とか仕様の問題で細かいチェックを行うことが出来ないのです。
このうち、外部キー制約とnull禁止制約はロジックで頑張れば対処出来ます。

  • 外部キー制約:親テーブルが無くて子テーブルだけある、みたいなことにならないように、トランザクション等も駆使してキッチリJavaのロジックを組む。
  • null禁止制約:登録、更新時にそのカラムがnullにならないようにJavaロジックを組む。

つまるところ、「バグが無いようにきっちり作れッ!!」という話で済むのです。

しかし、「一意制約」ばかりはそうはいかない。
もちろん、DB上でカラムが重複してはいけないようにJavaのチェック機能を入れるのはアリですれども、
Webシステムでの運用という都合上、「同タイミングの二重アクセス」という宿命からは逃れられません。

一意制約だけは、どれだけキッチリJavaロジックを組んでも担保出来ないのです。

しかしながら、GAE専用ライブラリであるSlim3には、主キー制約を上手く使って一意制約を実現してくれる機能があります。
今回はそれをご紹介します。

実装方法

さて、上記のようにGAEには一意制約が無く、主キー制約しかありません。
そこで、Slim3では「一意制約チェック専用のテーブルを作って主キーチェックを利用する」という機能が備わっています。

メソッドは「Datastore.putUniqueValue」。

例えば、ソース的には以下みたいな漢字に組めば良いというわけです。

public Key insert(Shohin model) throws Exception {

  Transaction tx = Datastore.beginTransaction();

  try {

   tx = Datastore.beginTransaction();

   Key key = null;

   if (Datastore.putUniqueValue(Shohin.UNIQUE_SHOHIN_NAME, model.getShohinName())) {
    key = super.put(model);
   } else {
    throw new BusinessCheckException("対象の商品名は既に登録されています。");
   }

   /*
    * 商品IDのあいまい検索用にIDを文字列化して再保存する。
    */
   model.setShohinId(Long.toString(model.getKey().getId()));
   put(model);
   tx.rollback();

   return key;

  } catch (Exception e) {

   tx.rollback();
   throw e;

  }

 }

こうすると、ほら。
下の図みたいに、一意キーチェックを行う為だけのテーブルが作成されます。
こうやって二つのテーブルを駆使することで一意制約を実現するわけですね。



登録する時が、「Datastore.putUniqueValue」。
削除する時が、「Datastore.deleteUniqueValue」。


つまり、


  • 新規登録時は、putするだけ。
  • 更新時は、先にputして後でdelete。
  • 削除時は、deleteするだけ。


こんな漢字にトランザクションを活用しつつロジックを組むことで、一意制約が実現出来るわけですね。
流石はSlim3。
良い機能が備わっています。

注意点

しかしですね、上記の通り、一意制約の度に新しいテーブルが作られてしまいますので、リソース的にも処理速度的にも、一意制約を多用するのは避けた方が良いと思います。

上記は例として「同じ商品名は登録出来ない」という仕様にしましたけれども、ケースによっては「普通のJavaチェックだけで十分。万が一被った場合は消せば良い」程度で済んでしまう程度の重要度であることも十分考えられますよね。
同時アクセスなんて滅多に無いですから、Javaチェックだけで99.9%はブロック出来ますもの。

そういう場合は一意制約はあえて入れないで、万が一問題が起きた時だけ運用回避する、という方針で十分かと思います。

しかし、逆に「絶対に何が何でも、死んでも重複して貰っては困る!!」なんて場合もあるでしょう。
例えば「メールアドレスは一意」などがそうです。
ログインIDの代わりにメールアドレスを使っているサイトなのに、メールアドレスが重複して入ってしまっていたら、これは大問題ですよね!!
万が一にもこんなバグは許されません。

このように、本気の本気で一意制約が必要な重用機能だけチェックを入れて、それ以外はJavaによるチェックのみで楽観的に済ませる。
これくらいの方針が良いでしょう。

終わりに

流石、Slim3は便利な機能が揃っています。事実上の標準フレームワークたるシェアを占めているだけのことはあります。

次回も引き続き、DBへのインサート処理の特記事項をご紹介します。

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」に該当する機能の実装を行います。

2014年7月31日木曜日

【GAE】初級実装編 BigTable導入

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

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

前回でシステムへのログインに成功しましたので、今度は主立った機能を作って行きます。

しかし、ここで問題となるのが、BigTable。
GAE実装の上での最大の要所です。

機能概要

さて、とりあえず今から時間をかけて作っていく機能は、以下4機能です。

  • 商品の登録
  • 商品の更新
  • 商品の削除
  • 商品の検索

手始めに、代表的なCRUD機能を作って使い方をマスターしていくという作戦なわけです。

RDBMSではない

GAEで使用するBigTableの特徴、それは「RDBMSではない」と言うことです。

「RDBMS」という用語がピンと来ない方もいらっしゃるかもしれませんので軽くご説明しますと、
要はRDBMSとは普通のDBのことです。

「SELECT * FROM aaa,bbb WHERE aaa.id = bbb.id and ……」みたいにSQLを発行して結果を取得出来るアレです。

Oracle、PostgreSql、MySQL、これら代表的は全部RDBMSです。
実際の所、この業界のDBとは殆どRDBMSですので、「DBとはRDBMSのことだ!!」くらいの勢いで考えていても一生困らない人が大半かと思います。

しかし、GAEで採用しているDB「BigTable」はRDBMSの範疇に入りません。

Bigtableは「列指向データベースマネジメントシステム」というカテゴリに入るDBです。

しかし、RDBMSではありませんので、RDBMSの常識がこちらでは通用せず、カルチャーショックを受けることになります。
細かい話は順次判明次第調査していくとしまして、今回はかじり程度にご紹介したいと思います。

BigTableの長所

BigTableの長所は、「大量データOK」と「超高速」の2つです。
何せ、世界中で使われているGoogleの分散システムを支える技術の中枢ですからね、名実共に世界最強の超高速DBだと思います。

具体的には「レコード件数が増えても検索速度は変わらない」という特徴があります。

普通のRDBMSの場合、100レコード中の10レコードを検索するのと、1億レコード中の10レコードを検索するのではパフォーマンスに差が出て来ます。
もちろん大量レコードの中から検索する方が遅い。
なのでDBプログラマーはインデックスを張ったり、テーブル自体を分割したりと、何とかその検索を高速で行おうと日夜チューニングを必死で行っているわけです。

しかし、GAEはそういう心配がありません。
総レコード数が100だろうが100億だろうが、検索速度は常に一定です。
そして速い。

「大量データ、大量アクセスに無敵の強さを発揮する」と言うのがGAEの強みで、そのDBであるBigTableも同様の性質を有しているのです。

BigTableの短所

一方で短所もあるって言うか、RDBMSに慣れている身としては、「大量データと超高速以外は短所しか無い」くらいの気分ですけどね。

一例を挙げると「joinが無い」があります。
同時に2つ以上のテーブルを検索出来ません。

イメージとしては、エクセル表なのですよ。


まあ、縦列と横列の2次元であるという点ではRDBMSもエクセルも同じですが、RDBMSはSQLがあります。
BigTableの場合はフィルターです。


エクセルのフィルター機能だけで検索するようなイメージ。
それがBigTableです。

なので、RDBMSと違って、


  • 複数のテーブルを結合したり出来ません。
  • 「group byでグルーピングして最大値を取得する」とかも出来ません。
  • 「count(*)」も出来ません。
  • 曖昧検索も制限あり。


「え~ッ1? こんな事も出来ないの?」と言うくらいのカルチャーショックを受けます。

もし上記のような機能を実現したい場合は、まずテーブルのデータを全部ガボッと取得してJavaのロジックで算出するとか、そういう泥臭いやり方になります。
もちろん大量データの場合はガボッと全部取得とか出来ませんから、別途集計テーブルを用意しておいて都度更新していくなど、やり方を一考する必要が出て来ます。



「こんなんじゃ開発やってらんないよ!!」



みたいな気分になる事も多いですが、いやいや、代わりに「安い」ですから。
開発が大変になる代わりに圧倒的低コスト運用を実現出来るという可能性を秘めたインフラなので、何とか開発者の方は頑張ったって下さい。

世界に誇るGoogleのGoogle検索、Google画像検索、Googleマップ、Google+、youtube、このブログ。

あれらは全部BigTableで作っているのです。
我々があれらを全部タダみたいなコストで使わせて貰っているのは、開発者の方が知恵を使って頑張って開発してくれたからなのです。

開発者にはかなりの力量を要求されますが、代わりに成功すれば大きなメリットを会社にもたらしてくれるでしょう。

終わりに

ひとまず、BigTableのご挨拶という程度ではこの辺で終了します。

詳しく書いていくと書籍になってしまいますので、このブログでは開発過程で出て来たポイントを都度ご紹介、という形になるでしょう。

次回からは、一先ず簡単な「レコード1件のcreate」から初めていきたいと思います。

2014年7月21日月曜日

【GAE】初級実装編 シングルサインオン

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

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

今回はシングルサインオンでログイン機能を作っていきます。

画面レイアウト

ログイン画面は以下のレイアウトにしました。


お馴染み「ユーザID」と「パスワード」を入力する欄がありませんね?

はい。
このシステムでは「シングルサインオン」を使ってログイン機能を実装したいと思います。

シングルサインオン

シングルサインオンとは、「一回のログイン認証によって、複数のシステムへ同時にログイン出来る機能」のことです。

シングルサインオンを使用する場合、ログインIDとパスワードは利用先に丸投げすることになるので、
自社でログインIDとパスワードを保持することはありません。

昨今問題になっている「個人情報流出」のリスクも、自社で持たないことで軽減することが可能なのです。


「何も持たないことが長所」


クラウド時代におけるスマートな姿勢の一つですね。

身の回りでよく使用されている例と言えば……、ちょっと「一般的」と言える程普及はしていないかも。

探せばあるんですけどね。
僕がシングルサインオン界で一番幅を利かせていると思うのは、FaceBookでしょうか。
「このシステムはFaceBookのIDでログイン出来ます」というFaceBookのシングルサインオンが最も良く登場するイメージです。

しかし、「常識」と呼べる程は完全普及していないと思います。


私はこの状況は改善されるべきものであると考えています。

処理効率を考えれば、「全人類が共通で一つのアカウントを持っていて、どのシステムでもそれを使ってログイン出来る」という状況がベストです。
世界的な標準化機構が主導して世界統一アカウントを作るくらいの動きがあって然るべきと考えています。

まあ、色々と難しいハードルがあるみたいで、現状、世界統一アカウントと呼べるものは存在しません。

しかし、世界統一アカウントに準ずる存在と言えるものはあると思います。
それが「Googleアカウント」と「AppleID」です。

AndroidスマホとiPhoneを持っている人はみんなこれらのアカウントを持っているわけですから、
現実のシェアを考えると、この2つが世界統一アカウントに最も近いものだと思います。

現在、私が作っているアプリはGAEのアプリですので、ここは「Googleアカウント」に便乗させて頂こうと思います。

「俺のシステムを使いたいならGoogleアカウントを持って来い!!」というスタイルです。

この辺りはアプリの性質を鑑みての判断になるでしょうね。

「いや、このアプリはGoogleIDを持っていない人も使うものだし……」みたいな懸念があるなら普通にログインIDとパスワードをDBに保有すれば良いです。
(代わりに流出リスクという業を背負うことになりますが)

ただ、GAEはGoogleアカウントを使うことで解禁される機能が存在しますので、
GAEシステムの利用はGoogleアカウント保有者に限定し、持っていない人には新規作成するようガイダンスする、という方向で努力した方が合理的だと思います。

開発

では、自分のGAEアプリの中でGAEにログインする方法を解説します。

まずは現状ログインチェックです。

protected void loginCheck() throws ValidationCheckException {

 UserService userService = UserServiceFactory.getUserService();
 User user = userService.getCurrentUser();

 if (user == null) {
  logger.fine("このユーザはログイン認証を行っていません。");
  throw new ValidationCheckException(ResponseCode.NOT_LOGIN);
 }

};

「UserService」クラスの「getCurrentUser()」の結果がnullだったら未ログイン。
nullでなかったらログイン済みです。

そして、nullだった場合は、Googleのログイン画面に飛ばします。

そこから先はGoogleユーザならお馴染みのログイン画面です。
流石に顔を恥ずかしいのでマスクした画像を載せますが、別に正体を隠さねばならない理由はありません。(;^_^)



ここがポイントです。

自分でログイン画面を作るのではなく、Google画面にワープさせて、ログインさせてから戻ってきて貰う。

これによって、自社でパスワード認証をすることは無いので、自社の責任で流出することは絶対に無いのです。

次に問題となるのは、どうやってGoogle画面にワープさせるか、です。その方法は以下になります。

protected LoginOutUrlJet doResponse() throws Exception {

  LoginOutUrlJet jet = new LoginOutUrlJet();

  UserService userService = UserServiceFactory.getUserService();

  jet.setUrl(userService.createLoginURL("戻ってきて欲しいパス"));

  jet.setResponseCode(ResponseCode.SUCCESS);

  return jet;
 }

「UserService」クラスの「createLoginURL()」メソッドで、Google画面のURLを取得出来ますので、
後はこのURLの先にJavaScript等で強制遷移させれば良いというワケです。

ちなみに、このURLは「ログインさせてから戻ってきて貰う」の機能も内包しているものです。

「userService.createLoginURL("戻ってきて欲しいパス")」

こうすることで、ログイン後に自動的に指定したパスのURLに戻ってきてくれるよう、Google側で制御してくれます。

例えば、ログイン後は常にトップ画面に戻ってきて欲しい場合は、

「jet.setUrl(userService.createLoginURL("/"));」と書けばOKです。

分かり易いですね。

終わりに

今回のシングルサインオンは、開発者に「持たないメリット」を理解する手助けになると思います。

昨今は個人情報保護法など色々とセキュリティ面で厳しい世の中になってきました。
その厳しいセキュリティ水準に耐えられるシステムを作るのは……、ハッキリ言って至難です。

「一応ちゃんとやってるつもりだけど、スーパーハッカーに襲われたら知らんし……」
「っていうか、このシステム、品質ボロボロでセキュリティなんてザルみたいなもんだし……」

こんな有様のシステムも少なく無い現状です。
こういう面倒さ、難しさををGoogleに丸投げ出来るのもGAEのメリットなのです。

引き続き、GAE開発を続けて行きます。

2014年7月10日木曜日

【GAE】初級実装編 疎通確認 後半 ~レスポンス送信2~

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

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

前回でレスポンスパラメータを格納するDTO、「JETクラス」を作成しましたので、
次はそれをJSON変換します。

JSON

JSONは正式名称を「JavaScript Object Notation」と言う、軽量なデータ記述言語です。
JavaScriptという名称が入っていますが、別にJavaScript専用というわけではありません。(変なネーミングですね)

同じデータ記述言語の兄弟分にはXMLがありますが、XMLの方が詳細な情報が定義出来る一方、処理が重くなります。
JSONは余り複雑なデータ表現には不向きですが、軽量という利点があります。

Ajax処理で使用するデータ程度ならJSONで十分対処出来るかと思いますので、基本はJSONを使っていくことになるでしょう。

JSON変換ライブラリ

さて、JavaのBEANクラスをJSON文字列に変換するのは、手動で行う必要はありません。
便利なJSONライブラリがありますので、これを使用しましょう。

  • Jackson
  • JSONIC
  • GSON

ザッと有名所ではこの3つがあります。
世界中でベンチマークが行われており、この中で最も高性能なのはJacksonです。

ですが。

今回はGAEですので、Google製ライブラリであるGSONを使うことにしようと思います。

以下からGSONライブラリをダウンロードして下さい。




しかし、ここで注意事項です。
どうも、最新型であるGSON2系を使用すると、GAE本番環境デプロイ時に「java.lang.VerifyError」が発生するようなのですよ。

どうも不安定らしく、発生したり、しなかったりする困った事象です。

「GAE GSON java.lang.VerifyError」で検索しますと同様の事象が数多く報告されていることが確認出来ますが、解決策は見つかっていないようです。
私も以前にこの問題にぶつかりましたが、その時はGSONのバージョンをGSON1系に下げることで解決出来ました。

この為、私はあえて古いバージョンであるGSON1系を導入することをオススメします。

まあ、VerifyErrorが出る問題につきましても、GAEのバージョンが進んだら発生しなくなるかもしれませんし、
今の所は現場の知恵ということで小耳に挟んでおく程度が良いかと思います。

変換実行

GSONライブラリの導入が終わりましたら、次は実際に実装してみましょう。

JavaのBEANクラスをGSONを使ってJSON文字列に変換するサンプルは、こちら!!


Gson gson = new Gson();
String json = gson.toJson(jet);

これだけ。超簡単なのです!!

そう、私がGSONをオススメするもう一つの理由は、実装が超簡単な点です。

Jacksonだと「Factoryクラス」「マッピングクラス」とか何とか色々ゴニョゴニョしてきますが、GSONは超簡単です。

開発が楽になるという点においても、ここはGSONの採用をオススメしたい所です。

この結果、例としては以下のような文字列が出力されることになります。
これがJSON文字列です。

{"shohinList":[{"key":{"kind":"Shohin","id":3},"seqNo":1,"shohinId":"3","shohinName":"Tacy%E3%83%86%E3%82%B9%E3%83%883","stock":3333,"deliveryDate":"2014/07/04","createdAt":"2014/07/03 14:36:11","updatedAt":"2014/07/03 14:36:12"},{"key":{"kind":"Shohin","id":2},"seqNo":2,"shohinId":"2","shohinName":"Tacy%E3%83%86%E3%82%B9%E3%83%882","stock":1111,"deliveryDate":"2014/07/03","createdAt":"2014/07/03 14:36:01","updatedAt":"2014/07/03 14:36:01"},{"key":{"kind":"Shohin","id":1},"seqNo":3,"shohinId":"1","shohinName":"Tacy%E3%83%86%E3%82%B9%E3%83%88","stock":1234,"deliveryDate":"2014/07/04","createdAt":"2014/07/03 14:35:30","updatedAt":"2014/07/03 14:35:30"}],"responseCode":0}

レスポンス返却

最後に、出力した文字列をHttpServletResponseに乗せて返却すれば一連の処理は完了です。

response.getWriter().write(json);

HttpServletResponseで返却するのはGAEならではの話ではなく、Java Webシステム全般で共通の手法です。
Ajaxを使う時は必ず出て来ますので、覚えておくときっと良いことがあるでしょう。


終わりに

今回は楽勝な内容でしたね。

これでGAE開発の最も根幹部分であるAjax通信は完成です。
後はこれを経由して各種機能をガンガン作って行くだけです。

次回はログイン機能を作ってみようと思います。