空谷に吼える

ブロックチェーン/DLTまわりのなにかしらを書いていく所存

23cではBlockchain Table上でのバージョン表現が楽になった話

続・追記オンリーのBlockchain Tableで情報の更新を表現するには

以前に、更新が入るタイプの情報をUPDATEが行えないBlockchain Table上で扱うには?という話を以下のポストで説明しました。

gakumura.hatenablog.com

ざっくりまとめると以下のような話です:

  • ①Blockchain Table上ではバージョン番号にあたる属性を付与したうえでそれをインクリメントしつつ別の行としてINSERTしていく
  • ②さらに、最新バージョンの行だけ抽出するビューを作成し、ビュー側でUPDATEを行えるように仕掛けを作っておくと便利

で、Oracle Database 23cでは、Blockchain Tableの機能強化のひとつとして、こうした更新が入るタイプの情報をBlockchain Table上で扱うようなユースケースがより容易かつ便利に実現できるようになりました!それほど複雑な話ではないので、以下にさくっと紹介していきます。前提として以前のポスト↑にも目を通しておくと理解しやすいと思います。

23cでのBlockchain Table上のバージョン表現関連の機能強化

↑に挙げた以前の記事でも説明した通り、更新不能なBlockchain Table上で更新が入る情報を扱う場合、データとしては複数の行を別々のバージョンとして追加/保持していくことで情報の更新を履歴として表現することになります(例:ある銀行口座の現在残高の情報を持つテーブルに、残高が更新されるたびに行を追加)。23cでは、行バージョン機能と最新バージョンビューでこうしたユースケースについてのサポートが強化されています。

なおその他のものも含めたOracle Database 23cでのBlockchain Tableの機能強化は以下のスライドの後半部分にまとまっていますのでご覧ください。

speakerdeck.com

行バージョン機能

Blockchain Tableに行バージョン(Row Version)という機能が追加されています。

  • テーブル作成時に最大3つのカラムを指定することで、そのカラムの値が同一である行に関しては同一の行グループとして管理される( 例:同一のBANKおよびACCOUNT_NOの値を持つ行を行グループに指定)
  • 行のINSERT時、隠しカラム(ORABCTAB_ROW_VERSION$)に自動で行グループ内のバージョン番号が格納

という機能です。かんたんに言うと、バージョン番号にあたる属性のカラムを自分で用意することも、INSERT時にその値をインクリメントしていくことも不要になったということです。だいぶお手軽になりましたね。

行バージョン機能を使いたい場合、CREATE BLOCKCHAIN TABLE ... WITH ROW VERSION row_version_name (col1 [, col2 [, col3]])のようにDDLを指定します。

最新バージョンビューの自動作成

↑の行バージョン機能を使っている場合、{Blockchain Tableのテーブル名}_LAST$という名前で行グループ内の最新のバージョンのみを抽出するビューが自動的に作成されます。参照については多くの状況では最新のバージョンのみに用があり、古いバージョンは省きたいので、この自動的に作成される最新バージョンビューはけっこう使いでがありそうですね。

なお、ある行グループの中からの最新バージョンの行だけの抽出を自分で行いたい場合には、「ORABCTAB_ROW_VERSION$の値が最大のもの」といったやり方ももちろん取れますが、ORABCTAB_LAST_ROW_VERSION_NUMBER$という最新バージョンかどうかのフラグにあたる隠しカラムが追加されているのでこれを使うのがお手軽かつ高速です。このカラムの値は、最新バージョンの行はNOT NULL*1、それ以外の行についてはNULLになっています。

シン・Blockchain Tableとビュー、INSTEAD OFトリガーを組み合わせて更新が入る情報をいい感じに扱えるようにしてみた

というわけで、以前のポストで説明したやり方についても、23c以降ではより便利で高速なやり方にアップデートできますね。

前述の通り、バージョン番号にあたるカラム(REVISION)をユーザー側で用意する必要がなくなりました。また、INSERT時にバージョン番号をインクリメントしていく必要もありません。

一方、自動で作成される最新バージョン抽出ビュー(bc_document_last$)はBlockchain Tableの隠しカラムも含まれている関係でUPDATEのINSTEAD OFトリガーを仕掛けるのにはやや余分なものが多いので、以前と同様にbc_document_latest_viewのビューを自前で作成し、そちらにトリガーを仕掛けています。ただこのときも、WHERE句は以前のように最新のバージョン番号をサブクエリで抽出する必要はなく、ORABCTAB_LAST_ROW_VERSION_NUMBER$ IS NOT NULL条件だけで良いのでめちゃ楽です!

アップデート版のサンプルスクリプトは以下に置いておきます。

-- bc_documentという名前のBlockchain Tableを作成
-- ROW VERSIONの対象カラムとしてはdocument_idを指定
CREATE BLOCKCHAIN TABLE bc_document (
    document_id VARCHAR2(32) NOT NULL,
    json_content JSON
)
    NO DROP UNTIL 0 DAYS IDLE
    NO DELETE UNTIL 16 DAYS AFTER INSERT
    HASHING USING "SHA2_512"
    WITH ROW VERSION rv(document_id)
    VERSION "v2"
;

-- 行をINSERT
INSERT INTO bc_document VALUES ('Aランチ', '{"price" : "1000", "item1" : "味噌ラーメン", "item2" : "餃子", "item3" : "杏仁豆腐"}' );
INSERT INTO bc_document VALUES ('Bランチ', '{"price" : "900", "item1" : "麻婆豆腐", "item2" : "ライス", "item3" : "杏仁豆腐"}' );
INSERT INTO bc_document VALUES ('Cランチ', '{"price" : "800", "item1" : "醤油ラーメン", "item2" : "杏仁豆腐"}' );

COMMIT;

-- ドキュメントごとに最新バージョンのみを抽出するVIEWを作成
CREATE OR REPLACE VIEW bc_document_latest_view AS
    SELECT document_id, json_content FROM bc_document a
    WHERE ORABCTAB_LAST_ROW_VERSION_NUMBER$ IS NOT NULL
;

-- VIEWに対してのDMLを制御するトリガーを作成
CREATE OR REPLACE TRIGGER trig_bc_document_latest
INSTEAD OF INSERT OR DELETE OR UPDATE
ON bc_document_latest_view
FOR EACH ROW
BEGIN
   IF INSERTING THEN
      INSERT INTO bc_document (document_id, json_content)
      VALUES (
         :new.document_id,
         :new.json_content
      );
   ELSIF DELETING THEN
      RAISE_APPLICATION_ERROR(-20000, '削除は禁止されています' );
   ELSE
      IF  UPDATING( 'document_id' ) THEN
        RAISE_APPLICATION_ERROR(-20000, 'Document ID列は更新できません' );
      ELSIF UPDATING( 'json_content' ) THEN
         INSERT INTO bc_document (document_id, json_content)
         VALUES (
            :old.document_id,
            :new.json_content
         );
      END IF;
   END IF;
END;


-- VIEWに対してDELETEを試す→失敗する
DELETE FROM bc_document_latest_view WHERE document_id = 'Aランチ';

-- VIEWに対してINSERTを試す→成功する
INSERT INTO bc_document_latest_view VALUES('Dランチ', '{"price" : "1100", "item1" : "醤油ラーメン", "item2" : "餃子", "item3" : "半チャーハン", "item4" : "杏仁豆腐"}');

-- VIEWに対してdocument_idの更新を試す→失敗する
UPDATE bc_document_latest_view
   SET document_id = '最安ランチ'
   WHERE document_id = 'Cランチ'
;

-- VIEWに対してJSON内容の更新を試す→成功する
UPDATE bc_document_latest_view
   SET json_content = '{"price" : "800", "item1" : "塩ラーメン", "item2" : "マンゴープリン"}'
   WHERE document_id = 'Cランチ'
;

*1:試したところでは'01'という値になってましたが、ドキュメント上NULL/NOT NULLで判断せよとなっているのでそのようにしたほうが良いはず