ITエンジニアによるITエンジニアのためのブログ

Ruby on RailsでPostgreSQLのJSONBを扱うための実践ガイド

PostgreSQLはJSONデータ型としてJSONとJSONBを提供しており、Ruby on Railsも両方のデータタイプに対応しています。但し、使用の際には若干の注意が必要なので以下の項目ごとにまとめてみました。

  1. マイグレーション
  2. レコード作成と更新
  3. WHEREクエリ

なお、PostgreSQL公式ドキュメントではほとんどのアプリケーションは特別な理由がない限りJSONBを使うことを推奨していますので、この記事でもJSONデータタイプではなくJSONBデータタイプを使います。

マイグレーション

まずモデルを作成しますが、特に追加設定は必要ありません。

class Example < ApplicationRecord
end

次にマイグレーションを作成し、カラムタイプでjsonbを指定します。NULLは許可せず、デフォルト値で空のJSONを指定しておきます。

class CreateExamples < ActiveRecord::Migration[6.1]
  def change
    create_table :examples do |t|
      t.jsonb :data, null: false, default: {}

      t.timestamps
    end
  end
end

こうすることで、nilチェックを省けるようになります。

# NULLを許可してデフォルト値を設定しない場合
example = Example.create
example.data['foo']
=> NoMethodError: undefined method `[]' for nil:NilClass

# エラー回避するにはnilチェックが必要
example.data&.dig('foo')
=> nil

# NULLを許可してデフォルト値を設定すると、nilチェック不要
example.data['foo']
=> nil

レコード作成と更新

レコード作成

レコード作成ではRubyのHashを入力値とすると、ActiveRecordがJSONでのINSERT文に変換してくれます。Hashキーは文字列、シンボルどちらでも可能です。

但し、入力値をJSON文字列でActiveRecordに渡してしまうと過剰なエスケープがされ、テーブルカラムのデータタイプをJSONBで指定しているにもかかわらず、JSONではなく文字列でDBに保存されてしまうので注意が必要です。

# キーが文字列のHashで作成。
# DBには{"foo": "bar"}と正しいJSONで保存される。example1 = Example.create data: { 'foo' => 'bar' }

# データ取得の際はHashがリターンされる。
example1.data
=> {"foo"=>"bar"}

# キーがシンボルのHashで作成。
# DBには{"foo": "bar"}と正しいJSONで保存される。
example2 = Example.create data: { foo: 'bar' }

# データ取得の際は、キーが文字列のHashがリターンされる。
example2.data
=> {"foo"=>"bar"}

# NG。JSONで作成。
# DBには"{\"foo\":\"bar\"}"と文字列で保存される。
example3 = Example.create data: { foo: 'bar' }.to_json

# データ取得の際は、文字列がリターンされる。example3.data
=> "{\"foo\":\"bar\"}"

ActiveRecordにJSON文字列でデータを渡してしまい文字列でDBに保存されると、後述のPostgresのJSON用演算子を活用したクエリが使えなくなってしまい、データタイプをJSONBにした意味がなくなるので、必ずHashでデータを渡すようにしましょう。

データ更新

データ更新の際は、Hashの該当箇所を変更します。

# Rails
example1.data['foo'] = 'baz'
example2.save

# Postgres
UPDATE "examples" SET "data" = $1, "updated_at" = $2 WHERE "examples"."id" = $3  [["data", "{\"foo\":\"baz\"}"], ["updated_at", "2024-09-14 11:02:54.044939"], ["id", 1]]

Rails上ではHashの一部を変更するだけですが、SQLではカラム全体のUPDATE文に変更されます。

WHEREクエリ

JSONBカラムを使ってレコードを検索するには、PostgreSQLのJSON用演算子を使用します。JSON用演算子はActiveRecordではネイティブサポートされていないので、WHEREの中身を自身で記載する必要があります。

ここでは3つの代表的な演算子の使い方を紹介します。

  1. ->> 演算子
  2. @> 演算子
  3. -> 演算子

->> 演算子

この演算子は、JSONに含まれるキーの値を文字列で返します。比較対象がスカラー値の時に使います。

# dataカラムに保存されたJSONの'foo'キーの値が'bar'のレコードを検索。

# Rails
Example.where("data ->> 'foo' = ?", 'bar')

# SQL
SELECT "examples".* FROM "examples" WHERE (data ->> 'foo' = 'bar');

@> 演算子

‘@>’演算子は包括演算子と呼ばれ、オペレーターの左側のJSONが右側のJSONを含むかを判定します。包括演算子を使う場合は、create文とは違いHashのままではActiveRecordに渡せないので、JSONに変換してから渡します。

# dataフィールドのJSONが{"foo": "bar"}を含むレコードを検索。

# Rails
# 誤り。Hashのままではタイプエラーになる。
Example.where("data @> ?", { 'foo': 'bar' })
TypeError: can't quote Array

# 正しくはJSONに変換して渡す。
Example.where("data @> ?", { 'foo': 'bar' }.to_json)

# SQL
SELECT "examples".* FROM "examples" WHERE (data @> '{"foo":"bar"}')

包括演算子は指定した階層のJSONしかマッチしないため、ネストされたJSONで深い階層のオブジェクトを探すには、ルート階層から比較対象の階層まで全て含んだJSONで比較するか、後述の->演算子を使って階層を辿る必要があります。

Example.create data: {"foo": {"bar": "baz"}}

# ルート階層でマッチさせる。
Example.where("data @> ?", { foo: { bar: 'baz' }}.to_json)

# SQL 
# SELECT "examples".* FROM "examples" WHERE (data @> '{"foo":{"bar":"baz"}}')

->演算子

‘->’演算子は’->>’演算子に似ていますが、こちらは文字列ではなくJSONオブジェクトを返します。

用途としては、ネストされたJSONオブジェクトを辿る時に使い、JSONオブジェクト同士で比較をしたい時にはそのまま取得したJSONを使って包括演算子で比較し、スカラー値と比較したい時には->>演算子と合わせて使います。

JSON同士で比較する場合

Example.create data: {"foo": {"bar": "baz"}}

# ->演算子で1つ下の階層のJSONを取得し、JSON同士を包括演算子(@>)で比較。
Example.where("data -> 'foo' @> ?", { bar: 'baz' }.to_json)

# SQL
SELECT "examples".* FROM "examples" WHERE (data -> 'foo' @> '{"bar":"baz"}')

スカラー値で比較する場合

Example.create data: {"foo": {"bar": "baz"}}

# ネストされたJSONオブジェクトを->で取得し、その後barキーの値を->>演算子で文字列として取得して、スカラー値('baz')と比較演算子(=)で比較
Example.where("data -> 'foo' ->> 'bar' = ?", 'baz')

# SQL
SELECT "examples".* FROM "examples" WHERE (data -> 'foo' ->> 'bar' = 'baz')

まとめ

PostgreSQLのJSONBタイプは、Railsで使う際にはレコード作成のインプットに気をつけなければならなかったり、クエリ発行の際に専用の演算子を使わなければならないなどの注意点はありますが非常に便利ですので、ぜひ使い方をマスターしてみてください。