原文:MySQL JSON Duality Views: Now with granular write and ETAG controls

原著者:Tarang Ranpara

本記事は、上記の英語記事をAIを使用して日本語に翻訳したものです。翻訳には誤訳や不正確な表現が含まれる可能性があります。内容の確認にあたっては英語原文を参照してください。


JSON Duality Views(JDV)を使用すると、アプリケーションはリレーショナル・テーブルを基盤とするJSONドキュメントを読み書きできます(JDVの紹介記事を参照してください)。MySQLがドキュメントとテーブルのマッピングを処理するため、リレーショナルな整合性を保ちながらアプリケーション・コードを削減できます。

それでも、ドキュメントのどの部分をアプリケーションが変更できるかは決める必要があります。以前のJDVでは選択肢が限られ、テーブルへの書込みは原則としてすべて許可するか、すべて禁止するかのどちらかで、射影されたすべての値がETAGチェックに参加していました。複数のリリースを通じて、JDVでは書込みと、どの変更が編集を無効にするかを、より細かく制御できるようになりました。本記事では、それらの変更を1つの例にまとめて紹介します。

訳注: ここでのETAG(Entity Tag)は、MySQLのJSON Duality ViewsにてJSONドキュメントの内容が変更されたかどうかを判定するための識別子です。主に楽観的同時実行制御(Optimistic Concurrency Control)に使用します。

新機能

  • MySQL 9.6.0では、テーブル・レベルのINSERT、UPDATE、DELETEを個別に設定できるようになり、NO INSERT、NO UPDATE、NO DELETEが導入されました。それ以前は、射影されたテーブルは原則として読取り専用か、3つの操作をすべて許可するかのどちらかでした。
  • MySQL 9.7.0では、それまでクローズド・ソースだったJDVのDMLがオープン・ソース化され、コミュニティ版で利用できるようになりました。
  • MySQL 26.10.0では、列レベルのUPDATEとNO UPDATE、およびテーブルと列の両レベルのCHECKとNO CHECKが追加されました。列がテーブルの設定を上書きできるようになり、ETAGの計算では、射影されたすべての値を含める代わりに、選択した値を除外できます。

テーブルと列への書込みを制御する

射影された各テーブルについて、ビューを通じて許可する書込み操作を制御できます。INSERT、UPDATE、DELETEで操作を有効にし、NO INSERT、NO UPDATE、NO DELETEで無効にします。書込みタグを省略した場合、対応する操作はデフォルトで無効になります。

列は所属するテーブルのUPDATE設定を継承しますが、WITH UPDATEまたはWITH NO UPDATEで上書きできます。これにより、テーブルを更新不可にしても特定の列の更新を許可したり、テーブルが更新を許可していても一部の列の更新を禁止したりと、どちらの方向にも柔軟に設定できます。

上の図では、ordersテーブルは挿入と更新を許可し、削除は禁止しています。amount列はUPDATEを継承しますが、product列はNO UPDATEで上書きしています。例として、顧客と注文用の2つのテーブルを作成し、顧客としてAliceを追加します。

CREATE DATABASE jdv_controls_demo;
USE jdv_controls_demo;
SET SESSION autocommit = 1;

CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
) ENGINE = InnoDB;

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT NOT NULL,
    product VARCHAR(100) NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    last_viewed DATETIME NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
) ENGINE = InnoDB;

INSERT INTO customers VALUES (1, 'Alice');

次に、上の図のJSON Duality Viewを作成し、それを通じてAliceの注文を挿入します(このようなビューの定義方法はDDLの記事、ビューに対するDMLの実行方法はDMLの記事で説明しています)。

CREATE JSON RELATIONAL DUALITY VIEW order_dv AS
SELECT JSON_DUALITY_OBJECT(
    WITH (INSERT, UPDATE, NO DELETE, CHECK)
    '_id': order_id,
    'product': product WITH NO UPDATE,
    'amount': amount,
    'last_viewed': last_viewed WITH NO CHECK,
    'customer': (
        SELECT JSON_DUALITY_OBJECT(
            WITH (NO INSERT, NO UPDATE, NO DELETE, NO CHECK)
            'customer_id': customer_id WITH CHECK,
            'customer_name': name
        )
        FROM customers
        WHERE customers.customer_id = orders.customer_id
    )
)
FROM orders;

INSERT INTO order_dv VALUES ('{
  "_id": 101,
  "product": "Laptop",
  "amount": 1299.99,
  "last_viewed": "2026-09-28 09:00:00",
  "customer": {"customer_id": 1, "customer_name": "Alice"}
}');

SELECT JSON_PRETTY(data)
FROM order_dv WHERE data->'$._id' = 101;

この挿入によって注文101が作成され、既存の顧客が再利用されます。ドキュメントを読み取ると、次の結果が返ります。

{
  "_id": 101,
  "amount": 1299.99,
  "product": "Laptop",
  "customer": {
    "customer_id": 1,
    "customer_name": "Alice"
  },
  "_metadata": {
    "etag": "843df2d201edf7dbf253ca3691d8ac39"
  },
  "last_viewed": "2026-09-28 09:00:00.000000"
}

これらの出力に示すETAG値は、今回の実行で得られたものです。NO UPDATEはproductの初期値を許可しますが、その後の変更は禁止します。更新を試す前に、JDVのメタデータ・テーブルで、実効的な書込み設定とCHECK設定を確認しましょう。

まずテーブルのタグを確認します。値が1なら有効、0なら無効です。

SELECT REFERENCED_TABLE_NAME AS base_table,
       ALLOW_INSERT, ALLOW_UPDATE, ALLOW_DELETE, ALLOW_CHECK
FROM INFORMATION_SCHEMA.JSON_DUALITY_VIEW_TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'order_dv'
ORDER BY REFERENCED_TABLE_NAME;

実行結果:

base_table ALLOW_INSERT ALLOW_UPDATE ALLOW_DELETE ALLOW_CHECK
customers 0 0 0 0
orders 1 1 0 1

次に、これらの設定が個々のフィールドにどのように適用されるかを確認します。

SELECT REFERENCED_TABLE_NAME AS base_table,
       JSON_KEY_NAME AS json_field, ALLOW_UPDATE, ALLOW_CHECK
FROM INFORMATION_SCHEMA.JSON_DUALITY_VIEW_COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'order_dv'
ORDER BY REFERENCED_TABLE_NAME, JSON_KEY_NAME;

射影されたフィールドについての実行結果:

base_table json_field ALLOW_UPDATE ALLOW_CHECK
customers customer_id 0 1
customers customer_name 0 0
orders _id 0 1
orders amount 1 1
orders last_viewed 1 0
orders product 0 1

productフィールドは、所属するテーブルが更新を許可していても更新できません。これらのフラグは、継承と列による上書きの適用後の実効設定を示しています。ETAGの制御を試す際に、ALLOW_CHECKの値を使用します。

ドキュメントを読み取り、金額を変更し、元の_metadata.etagをそのまま保持して送信しましょう。

SELECT data INTO @order_doc
FROM order_dv WHERE data->'$._id' = 101;

SET @order_doc = JSON_SET(@order_doc, '$.amount', 1199.99);

UPDATE order_dv SET data = @order_doc
WHERE data->'$._id' = 101;

SELECT product, amount FROM orders WHERE order_id = 101;

テーブルのUPDATEタグによって金額の変更が許可されます。最後のクエリは次の結果を返します。

product  amount
Laptop  1199.99

次に、現在のドキュメントを読み取り、保護されたproductの変更を試します。

SELECT data INTO @order_doc
FROM order_dv WHERE data->'$._id' = 101;

SET @order_doc = JSON_SET(@order_doc, '$.product', 'Monitor');

-- Expected failure: product is marked NO UPDATE.
UPDATE order_dv SET data = @order_doc
WHERE data->'$._id' = 101;

更新は次の結果を返します。

ERROR 6497 (HY000): Missing UPDATE tag on `jdv_controls_demo`.`orders`.product, path '$.product'.

注文はLaptop、1199.99のままです。ordersにはNO DELETEが設定されているため、次の削除も失敗します。

DELETE FROM order_dv WHERE data->'$._id' = 101;
ERROR 6498 (HY000): Missing DELETE tag on table `jdv_controls_demo`.`orders`, path '$'.

ETAGに影響する値を選択する

読取りと書込みの競合を防ぐ記事では、編集時にドキュメントの元のETAGを返すことで、MySQLがその間の変更を検知する仕組みを説明しています。そのモデルでは、射影されたすべての値がETAGに寄与していました。CHECKとNO CHECKによって、どの値を対象とするかを選べるようになり、除外した値の変更ではETAGの競合が起こらなくなります。

テーブル・レベルのデフォルトはCHECKです。WITH CHECKまたはWITH NO CHECKを指定しない限り、列は所属するテーブルの設定を継承します。列はどちらのテーブル設定も上書きでき、ネストされた各テーブルにはそれぞれ独自の設定があります。

上の図は両方向の上書きを示しています。last_viewedはordersテーブルがCHECKでも除外され、customer_idはcustomersテーブルがNO CHECKでも含まれます。書込みのルールはこれらの設定とは独立して適用されます。

先ほどの例を使い、ベース・テーブルでAliceの名前を直接変更してETAGを比較します。この比較でサンプル・データが変更されたままにならないように、トランザクションを使用します。

START TRANSACTION;

SELECT data INTO @customer_snapshot
FROM order_dv WHERE data->'$._id' = 101;

UPDATE customers SET name = 'Alice Smith' WHERE customer_id = 1;

SELECT JSON_EXTRACT(data, '$._metadata.etag') =
       JSON_EXTRACT(@customer_snapshot, '$._metadata.etag') AS name_ignored
FROM order_dv WHERE data->'$._id' = 101;
name_ignored
1

タグを付けていないnameは、顧客テーブルのNO CHECKを継承します。次に、同じ名前を持つ別の顧客に注文を関連付け、射影された顧客識別子だけが変わるようにします。

INSERT INTO customers VALUES (2, 'Alice Smith');
UPDATE orders SET customer_id = 2 WHERE order_id = 101;

SELECT data->'$.customer.customer_id' AS customer_id,
       JSON_EXTRACT(data, '$._metadata.etag') <>
       JSON_EXTRACT(@customer_snapshot, '$._metadata.etag') AS identity_checked
FROM order_dv WHERE data->'$._id' = 101;

ROLLBACK;
customer_id  identity_checked
2            1

列のCHECKがテーブルのNO CHECKを上書きします。ROLLBACKによって、Aliceの元の名前と、注文の元の顧客が復元されます。

次に、注文を読み取り、last_viewedのバックグラウンド更新を再現します。この列のNO CHECKタグにより、取得済みのETAGは有効なままになるはずです。

SELECT data INTO @snapshot
FROM order_dv WHERE data->'$._id' = 101;

UPDATE orders SET last_viewed = '2026-09-28 10:00:00'
WHERE order_id = 101;

SELECT JSON_EXTRACT(data, '$._metadata.etag') =
       JSON_EXTRACT(@snapshot, '$._metadata.etag') AS etag_unchanged
FROM order_dv WHERE data->'$._id' = 101;
etag_unchanged
1

タイムスタンプは変わりましたが、NO CHECKタグによってETAGの計算から除外されるため、ベース・テーブルを直接変更してもETAGは変わりません。

次に、チェック対象の金額を変更し、取得済みのETAGを使って編集内容を送信してみます。

UPDATE orders SET amount = 1099.99 WHERE order_id = 101;

SET @snapshot = JSON_SET(@snapshot, '$.amount', 999.99);

-- Expected failure: the checked amount changed after the read.
UPDATE order_dv SET data = @snapshot
WHERE data->'$._id' = 101;

更新は次の結果を返します。

ERROR 6494 (HY000): Cannot update JSON duality view. The ETAG of the document in the database did not match the ETAG '"d5dae930097111af8842b4b3f8b60fba"' passed in.

ベース行を確認すると、値は1099.99のままであることが分かります。

SELECT product, amount FROM orders WHERE order_id = 101;
product  amount
Laptop  1099.99

NO CHECKが変更するのは競合の検出であり、書込み権限ではありません。古いドキュメントでも、チェック対象外で更新可能なフィールドを上書きできます。そのため、変更されても編集を無効にする必要がない値だけを除外すべきです。

まとめ

JSON Duality Viewsでは、ビュー定義でドキュメントへの書込みと同時実行チェックの両方を制御できます。ビューを通じて各テーブルに許可する書込み操作を設定し、特定の列の更新設定を上書きし、ETAGに寄与する値を選択できるようになりました。

詳細を確認し、MySQLをご活用ください。