2015年5月28日木曜日

【GAE】初級実装編4 一意制約(更新パターン)

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

現在はクラウド基盤「Google App Engine(以下、GAE)」の連載中です。

今回も前回に引き続き一意制約についてです。

やっぱりGAEに一意制約は無い


前回にもテーマにしましたが、GAEに一意制約はありません。
しかし主キー制約はあります。
よって、主キーのみのテーブルを作って主キー制約を実現するしか無いです。

まずは前回にも記載した新規登録の一意制約ソースを以下に張ります。

public Key insert(Shohin model) throws Exception {

  /*
   * トランザクション開始
   */
  Transaction tx = Datastore.beginTransaction();

  try {

   Key key = null;

   /*
    * 一意制約専用テーブルに登録する。
    */
   if (Datastore.putUniqueValue(Shohin.UNIQUE_SHOHIN_NAME, model.getShohinName())) {

    /*
     * 一意制約専用テーブルに登録が成功した場合
     */
    key = super.put(model);

   } else {

    /*
     * 一意制約エラー
     */
    throw new BusinessCheckException("対象の商品名は既に登録されています。");

   }

   /*
    * 正常な場合はコミットする。
    */
   tx.commit();

   return key;

  } catch (Exception e) {

   /*
    * エラーになったらロールバックする。
    */
   tx.rollback();
   throw e;

  }

 }


フローを手短に表現すれば以下になります。

①一意制約テーブルに新規登録する。
⇒失敗したら処理終了。
⇒成功したら次に進む。
②新規登録する。

でも、これって新規登録ですよね。

更新の時はどうやるの?というのが今回のテーマです。

ガチンコ勝負

実はこのブログは、執筆前にネットサーフィンしてネタをかき集めてきておりまして、
上記のソースも、実は本質的には他サイトのパクリのようなもの。(自分で書き直していますけどね)

上のソースの元となった、新規登録系のサンプルソースはネット上に沢山転がっていました。

しかし、どれだけ探しても「更新系」の話題がサッパリ見つからないのです。

恐らく、「更新系」については、特に便利な機能なんか存在しないのでしょう。
ガチンコでロジックを組み上げる以外に手は無いというのが私の結論です。

気合い入れて行きましょう。

ロジック


正確に言いますと、GAEにあるのは「put」と「delete」ですから、「新規登録/更新」などという区分けは技術的にはありません。
しかし人間の操作感では「新規登録/更新」では別物でしょう。
「put」を「新規登録/更新」で使い分けるには、以下ロジックが必要になります。


・対象データを検索する。
⇒データが存在していなければ新規登録。
⇒データが存在していれば更新。


一回find処理が必要になります。普通のSQLみたいにupdateは存在しません。

その上で、delete&insertで一意制約ロジックを実現することになります。

そのフローはこちら。

①一意制約対象のカラムが更新前と更新後で違うかどうかを判定する。
⇒同じである場合は普通にputして終わり。
⇒違う場合胃は次に進む。
②一意制約テーブルに更新後のカラムを新規登録する。
⇒失敗したら処理終了。
⇒成功したら次に進む。
③putする。
④一意制約テーブルに更新前のカラムを削除する。

一意制約カラム削除のメソッドは「Datastore.deleteUniqueValue」です。

これを使用したソースはこちら。

public Shohin update(Shohin updateModel) throws Exception {

   Transaction tx = Datastore.beginTransaction();

   try {

       /*
        * 既存を取得
        */
       Shohin model = super.get(updateModel.getKey());
       model.setEntityUpdate(updateModel);
       super.put(model);

       /*
        * 一意制約項目が既存と同じであれば、そのまま更新する。
        */
       if (StringUtils.equals(model.getShohinName(), updateModel.getShohinName())) {

           model.setEntityUpdate(updateModel);
           super.put(model);

       } else {

           /*
            * 一意制約項目と異なる場合は、更新を行ってから過去の一意制約照合用レコードを消す。
            */
           if (Datastore.putUniqueValue(Shohin.UNIQUE_SHOHIN_NAME, model.getShohinName())) {

               model.setEntityUpdate(updateModel);
               super.put(model);
               Datastore.deleteUniqueValue(Shohin.UNIQUE_SHOHIN_NAME, updateModel.getShohinName());

           }

       }

       tx.commit();

       return model;

   } catch (Exception e) {

       tx.rollback();
       throw e;

   }

}

普通のSQLであればUPDATE一発で済むのに、GAEではこんなにロジックを作らなければなりません。
超面倒ですね!!

GAEの特性を認識せよ


これにより、GAEには個性があるということがお分かり頂けたかと思います。
要点は二点です。

  • GAEで一意制約を実現するのは大変である。
  • GAEはputであって、insert/updateではない。新規登録と更新を分けるのは大変である。

要件定義する時に、エンジニアがこの辺りの事情に配慮して定義しなければいけないのです。

やっぱり、基本的にGAEの一意制約は、本当に本当に必要な場合以外は適応しないというのが大原則と考えて良さそうです。

終わりに

なかなかGAEの個性に悩まされた要件でした。

次はちょっと変わった実装、「メール送信」についてご紹介します。

2015年5月18日月曜日

【GAE】初級実装編3 一意制約(新規登録パターン)

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

現在はクラウド基盤「Google App Engine(以下、GAE)」の連載中です。

今回は一意制約について検証してみます。

GAEに一意制約は無い


一意制約とは何か」は、割愛させて頂きます
(読者ならみんな知っているでしょう)

とにかく、結論から言わせて頂ければ、GAEに一意制約はありません。

GAEのBigTableは、要するにJavaの動きで言う所のMapだからなんでしょうね。

Mapは「key」を基準にvalueを登録しますよね?
だから「keyが重複しているか?」は主キー検索一発でチェック出来ますし、重複も物理的にありえません。

しかし、valueの重複が無いかはmap総当たりでチェックするしか無いのです。
GAEも同じような状況にあるのです。

BigTableで一意制約はありません。不可能です。

しかし、BigTableはデータベースですから、本当に厳密な一意性を確保しなければならないシチュエーションも存在します。

そこで活躍するのが、「一意制約専用テーブルを作って主キーを一意制約みたいに使う」というテクニックです。

GAEライブラリであるslim3にはこのテクニックを支援する機能があります。
今回はそれのご紹介です。


Datastore.putUniqueValue


この機能を実現してくれるメソッドは「Datastore.putUniqueValue」、これです。

まずはソースをご覧下さい。

public Key insert(Shohin model) throws Exception {

  /*
   * トランザクション開始
   */
  Transaction tx = Datastore.beginTransaction();

  try {

   Key key = null;

   /*
    * 一意制約専用テーブルに登録する。
    */
   if (Datastore.putUniqueValue(Shohin.UNIQUE_SHOHIN_NAME, model.getShohinName())) {

    /*
     * 一意制約専用テーブルに登録が成功した場合
     */
    key = super.put(model);

   } else {

    /*
     * 一意制約エラー
     */
    throw new BusinessCheckException("対象の商品名は既に登録されています。");

   }

   /*
    * 正常な場合はコミットする。
    */
   tx.commit();

   return key;

  } catch (Exception e) {

   /*
    * エラーになったらロールバックする。
    */
   tx.rollback();
   throw e;

  }

 }

これは「商品名の一意制約チェック」を想定したものです。

いくつかの新登場の情報が出てきていますね。

トランザクション


まず、GAEのトランザクションはコレです。
Transaction tx = Datastore.beginTransaction();

  • Transaction tx = Datastore.beginTransaction();
  • tx.commit();
  • tx.rollback();

読んで字のごとく。説明不要。

疑似一意制約


そして、今回の主題となるのはココです。

if (Datastore.putUniqueValue(Shohin.UNIQUE_SHOHIN_NAME, model.getShohinName())) {

「Datastore.putUniqueValue」

これで「そのキーだけが主キーのテーブル」が作成され、データが投入されます。
ちなみに、Shohin.UNIQUE_SHOHIN_NAMEは単なる固定値。制約のイメージとして一意に定義しています。

/** 商品名一意性約 */
 public static final String UNIQUE_SHOHIN_NAME = "UniqueShohin_ShohinName";


この状況で商品を登録すると、結果はこちら。



「UniqueShohin_ShohinName」というテーブルが作られて、その値は一意制約指定したキー1コだけですね?

こうすることで、主キー制約を一意制約として使い回すことが出来るわけです。


考察

以上のことから分かりますように、「一意制約一個作るのも面倒臭い!!」というのが結論です。
一意制約の数だけテーブル数も増えていってしまいますので、そう気軽に搭載するわけにはいきません。

よって、「何があっても厳密に一意を維持しなければならない!!」という場合を除いて、基本的に一意制約は付与しない方向が良いと思います。

例:メールアドレス

世の中には「メールアドレスをログインIDとして使う」というシステムがあります。
ログインIDが重複しては大変なので、メールアドレスの一意制約は必須。
こういう場合は手間を惜しまず一意制約を搭載しなければなりません。

例:商品名

一方で、この例で出した商品名はどうか?

「同じ商品のレコードが2つ以上あっては業務に混乱を来すから防ぎたい」

という需要があるとしましょう。
しかしですね、商品名なんて重複してたら更新すれば良いのですよ。

この一意制約チェックは「複数ユーザが同時に同じタイミングにレコードを登録した場合」に威力を発揮するもの。

「別々のユーザが同じタイミングで同じ商品名を入れるなんて、まず滅多に考えられない。商品登録前に普通にDB検索チェックするだけで99.999%は防げる。それでも重複した場合は手動修正して貰えばいいや」

という楽観的対処で十分です。

一意制約に厳密性を求めず、ロジックで制御した楽観的一意制約で済ませる。

手を抜ける所は手を抜くのも、GAE開発の一つです。

終わりに

今回は「新規登録」の一意制約をご紹介しました。
しかし、この一意制約はロジックで作り出している擬似的なものですので、「更新」の一意制約はまた違ったロジックを組まなければなりません。

次回は「更新一意制約」についてご紹介します。