はじめに
PostgreSQLのjson型カラムで特定のデータをマスキングしたい時に困ったので、その時に対応した内容の雑なメモ書きです。
ここでは、大文字の JSON はその書式やデータを表し、PostgreSQL の型を指す場合に json や jsonb のように小文字を使って区別します。
json型とjsonb型
PostgreSQL json 更新
と検索すると、とにかく検索でひっかかるjsonb型。
jsonb型とはなんなのかが全くわからない自分がいました。
歴史やPostgreSQLにいつ入れられたかは詳しい記事があると思うので詳細はそちらに任せるとして、ここではざっくりとした違いを説明したいと思います。
jsonb型とは、JSON テキストをバイナリ表現に変換して保持するデータ型のことで、
json型は、JSON テキストをそのまま文字列として保持しただけのものです。
結局json型の実態はJSONテキストなので、特定のデータを参照したい場合、都度JSONテキストをパースする必要があります。
jsonb型 では入力時にのみ JSON テキストを解析し、正規化されたバイナリ書式でカラムに格納するので、json型よりもデータアクセスが速いのが特徴のようです。
参考資料: http://scm.zoomquiet.top/data/20150213210641/index.html
json型のカラムで値をマスキングする
前提条件
今回使うバージョンはPostgreSQL12を使います。
概要
PosgreSQLのjson型/jsonb型で使える関数・演算子はほぼ一緒ですが、
唯一違うのはjsonb型にしかない演算子があることです。
このjsonb型でしか使えない演算子を使えば、JSONをパースする必要がなくてすごく楽にデータ更新等の作業ができるので、こちらをつかってマスキング処理をしていきます。
参考資料: https://www.sraoss.co.jp/PostgreSQL/Manual/document/12/functions-json.html
dockerでPostgreSQL立ち上げ
$ docker run --rm --name postgres -d -e POSTGRES_PASSWORD=test postgres:12
$ docker exec -it postgres bash
root@6820ff8c0b20:/# psql -U postgres
psql (12.6 (Debian 12.6-1.pgdg100+1))
Type "help" for help.
postgres=#
テーブル作成
postgres=# CREATE TABLE test (
id SERIAL,
json_vaule JSON,
jsonb_vaule JSONB
);
Insert
JSON作成関数は何個かあるのですが、
2つの別々の配列からキーと値の対を取る json_object
を使います。
postgres=# INSERT INTO test (
json_vaule,
jsonb_vaule
) VALUES (
json_object('{a, b}', '{1,2}'),
json_object('{a, b}', '{1,2}')
);
postgres=# select * from test;
id | json_vaule | jsonb_vaule
----+------------------------+----------------------
1 | {"a" : "1", "b" : "2"} | {"a": "1", "b": "2"}
(1 row)
マスキング
はじめにレコードの値を更新してマスキングする方法を探してました。
json型/jsonb型の要素の一部を変更する場合なら ||
を使うのが便利です。
ただし、json型に ||
をすると、そんな演算子がないとエラーがでます。
postgres=# UPDATE
test
SET
json_vaule = json_vaule || json_build_object(
'a', XXXX
)::jsonb
WHERE
id = 1;
ERROR: operator does not exist: json || jsonb
LINE 4: json_vaule = json_vaule || json_build_object(
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
この場合は、json型をjsonb型にキャストして、実行します。
そうすれば、json型でもjsonb演算子を使って値の更新ができます。
postgres=# UPDATE
test
SET
json_vaule = json_vaule::jsonb || json_build_object(
'a', 'XXXX'
)::jsonb
WHERE
id = 1;
UPDATE 1
postgres=# select * from test;
id | json_vaule | jsonb_vaule
----+--------------------+----------------------
1 | {"a": "XXXX", "b": "2"} | {"a": "1", "b": "2"}
(1 row)
もちろんjsonb型だとわざわざキャストしなくても、そのまま ||
を使うことができます。
postgres=# UPDATE
test
SET
jsonb_vaule = jsonb_vaule || json_build_object(
'a', 'XXXX'
)::jsonb
WHERE
id = 1;
UPDATE 1
postgres=# select * from test;
id | json_vaule | jsonb_vaule
----+-------------------------+-------------------------
1 | {"a": "XXXX", "b": "2"} | {"a": "XXXX", "b": "2"}
(1 row)
jsonフィールドの値がレコードごとに異なる場合にこの置換方法で、ちゃんとマスキングされるかも検証しました。
まずはデータの作成をします。
postgres=# INSERT INTO test (
json_vaule,
jsonb_vaule
) VALUES (
json_object('{c}', '{3}'),
json_object('{c}', '{3}')
);
postgres=# INSERT INTO test (
json_vaule,
jsonb_vaule
) VALUES (
json_object('{a}', '{4}'),
json_object('{a}', '{4}')
);
postgres=# select * from test;
id | json_vaule | jsonb_vaule
----+-------------------------+-------------------------
1 | {"a": "XXXX", "b": "2"} | {"a": "XXXX", "b": "2"}
2 | {"c" : "3"} | {"c": "3"}
3 | {"a" : "4"} | {"a": "4"}
(3 rows)
そして、 json_vaule
に対して更新をかけます。
そうすると id: 2
の値は "c": "3"
のみだったのが、マスキングしたい値 "a": "XXXX"
が入ってしまいました。 ||
は 2つのjsonb値を結合して、新しいjsonb値を作るjsonb演算子です。既存のjsonフィールドに "a": "XXXX"
を結合して新たなjsonフィールドを作るので、値がなければ追記してしまいます。
postgres=# UPDATE
test
SET
json_vaule = json_vaule::jsonb || json_build_object(
'a', 'XXXX'
)::jsonb;
UPDATE 3
postgres=# select * from test;
id | json_vaule | jsonb_vaule
----+-------------------------+-------------------------
2 | {"a": "XXXX", "c": "3"} | {"c": "3"}
3 | {"a": "XXXX"} | {"a": "4"}
1 | {"a": "XXXX", "b": "2"} | {"a": "XXXX", "b": "2"}
(3 rows)
やりたいことは a
の値があればマスキングするということなので、 a
がなければ何もしないが正解です。そのため、更新するのではなく、 a
があれば削除することにしました。
jsonの要素を評価して削除する場合は、jsonb演算子の #-
を使うと楽に特定の要素を削除できます。
json_vaule
で更新をかけた結果、 a
の値が削除され、それ以外は影響をうけません。
思っていたマスキングとは違いますが、見せたくない値を見せなくするということはできます。
postgres=# UPDATE
test
SET
json_vaule = json_vaule::jsonb #- '{a}';
UPDATE 3
postgres=# select * from test;
id | json_vaule | jsonb_vaule
----+------------+-------------------------
2 | {"c": "3"} | {"c": "3"}
3 | {} | {"a": "4"}
1 | {"b": "2"} | {"a": "XXXX", "b": "2"}
(3 rows)
おわりに
json型をSQLで扱ったことがないので、json型の演算子や関数の挙動がわからず苦労しました。
この内容を調査していて思ったのは、RDBMSでjsonを扱うのは辛いということだけはわかりました。