はじめに
最近GoとMySQLのJSON型と戯れたのでその足跡を残します。
近頃ののMySQLやPostgreSQL(MySQL5.7やPostgreSQL9.4以降)はJSON型をサポートしています。
https://dev.mysql.com/doc/refman/5.7/en/json.html
上手に使えばRDBの利点を活かしながらNoSQLの利点も享受することができます。
もちろん単純にMongoDB等やPaaSやDaaSなどと比較するのは早計なのでユースケースや要件をよく確認しましょう。
JSON型の利点
ここでは例として多言語対応フィールドについて考えてみます。
JSON型を利用しない場合は多言語フィールドを別テーブルに保存することもあるでしょう。
CREATE TABLE IF NOT EXISTS `user` (
id VARCHAR(36) PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
CREATE TABLE IF NOT EXISTS `i18n_user_name` (
user_id VARCHAR(36) NOT NULL,
locale VARCHAR(15) NOT NULL,
name VARCHAR(50) NOT NULL,
FOREIGN KEY (user_id) REFERENCES `user`(id) ON DELETE CASCADE,
CONSTRAINT `i18n_user_name_pk` PRIMARY KEY (`user_id`, `locale`)
);
もし仮に名前だけでなく別のリソース(例えば自己紹介文とか?)が存在したとしてそれらも多言語対応しようとすると、さらにテーブルを増やさなくてななりません。
しかしJSON型を使うとこのように次のように表現可能です。
CREATE TABLE IF NOT EXISTS `user` (
user_id VARCHAR(36) PRIMARY KEY,
name JSON NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
以下のように多言語リソースを保持すればJSONのままでゆるくデータを保存できます。
{
"ja": "日本語",
"en": "English"
}
またプレーンテキストではなくリッチテキストかもしれません。
Draft.jsなどもReactプロジェクトでよく使われています。
JSON型であればこのようなデータもHTMLに変換することなく構造保ったまま保存できます。
ここではJSON型の詳しい扱いかたには触れません。
様々なJSON型用の関数があるのでクエリに組み込むことも可能ですし、生成カラムを使って特定のキーにインデックスを作成したりもできます。
参考 https://dev.mysql.com/doc/refman/8.0/en/json-function-reference.html
ScannerとValuer
database/sql
のScanner
インターフェースとdatabase/sql/driver
のValuer
インターフェースを使うとスッキリかけます。
まずこんな感じの型を用意しておきます。
type JsonObject map[string]interface{}
func (j *JsonObject) Scan(src interface{}) error {
var _src []byte
switch src.(type) {
case []byte:
_src = src.([]byte)
default:
return errors.New("failed to scan JsonObject")
}
if err := json.NewDecoder(bytes.NewReader(_src)).Decode(j); err != nil {
return err
}
return nil
}
func (j JsonObject) Value() (driver.Value, error) {
b := make([]byte, 0)
buf := bytes.NewBuffer(b)
if err := json.NewEncoder(buf).Encode(j); err != nil {
return nil, err
}
return buf.Bytes(), nil
}