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

Ruby on RailsでPostgreSQLのhstoreを使う

Ruby on Railsではハッシュデータを頻繁に扱いますが、その保存先としてPostgreSQLのhstore型は有力な候補となります。ただ、Railsで扱う際にはhstoreならではのクエリ方法があったり、保存できるデータ構造に制限があったりするので、使用方法をまとめてみました。

  • マイグレーションとモデル
  • レコード作成と更新
  • アクセス方法
  • WHEREクエリ
  • hstoreの制限と使用例

マイグレーションとモデル

enable_extensionでhstoreエクステンションを有効化しないとマイグレーションエラーになりますので忘れないようにしましょう。

class CreateExamples < ActiveRecord::Migration[6.1]
  def change
    enable_extension "hstore"
    create_table :examples do |t|
      t.hstore :data, nil: false, default: {}

      t.timestamps
    end
    add_index :examples, :data, using: :gist
  end
end

nilを許可せずデフォルトを空のハッシュにしているのは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

また、gistインデックスを追加していますが、これはgistもしくはginインデックスを追加すると、後述の’?’や'@>'などhstore用演算子によるクエリを高速化できるからです。

レコード作成と更新

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

example = Example.create(data: { foo: 'bar' })

example.data
=> {"foo"=>"bar"}

レコード更新はハッシュの中身を変更して保存するだけです。

example.data['foo'] = 'baz'
=> "baz"

example.save
=> true

アクセス方法

DBに保存されたhstoreはActiveRecordによりRubyハッシュに変換されますので、文字列のキーでアクセスします。

example = Example.last

example.data.class
=> Hash

example.data['foo']
=> "bar"

なお必須ではありませんが、store_accessorメソッドを使って特定のキーをgetter/setterメソッドとして定義することも可能です。

class Example < ApplicationRecord
  store_accessor :data, :foo
end

example = Example.create(data: { foo: 'bar' })

# getter
example.foo
=> "bar"

# setter
example.foo = 'baz'
=> "baz"
example.foo
=> "baz"

WHEREクエリ

hstoreカラム内のキーや値を使ってレコードを検索するには、PostgreSQLのhstore用演算子を使用します。

特定のキーを持ったレコードを検索

特定のキーを持ったレコードを探すには’?’演算子を使います。

# fooというキーを持ったレコードを検索
Example.where("data ? :key", key: 'foo')

複数のキーを全て持ったレコードを探すには’?&’演算子を使います。

# fooとbaz両方のキーを持ったレコードを検索
Example.where("data ?& ARRAY[:first, :second]", first: 'foo', second: 'baz')

複数のキーの内、どれか1つでも持っているレコードを探すには’?|’オペレータを使います。

# fooかbazいずれかのキーを持ったレコードを検索
Example.where("data ?| ARRAY[:first, :second]", first: 'foo', second: 'baz')

特定の値を持ったレコードを検索

あるキーの値が特定の文字列のレコードを探すには複数の方法があります。

1つ目の方法は’@>’演算子を使います。これは包括演算子と呼ばれ、演算子の左側が右側を含むかどうかを判定します。演算子の左右は両方ともhstoreデータ型である必要があります。

# dataカラムが'foo' => 'bar'というhstoreを含むレコードを探す
Example.where("data @> hstore(:key, :value)", key: 'foo', value: 'bar')

もう一つの方法は’->’演算子を使う方法です。この演算子は特定のキーの値を返すので、その値と探したい文字列を’=’もしくは’LIKE’演算子を使って比較します。

# fooキーの値がbarのレコードを探す
Example.where("data -> 'foo' = :value",  value: 'bar')

# fooキーの値がbarという文字列を含むレコードを探す
Example.where("data -> 'foo' LIKE :value",  value: '%bar%')

但し、hstoreのgistやginインデックスは’@>’、’?’、’?&’、’?|’演算子のみ有効ですので、’=’や’LIKE’演算子には対応していません。そのため後者を使ったクエリは非常に遅くなってしまうため、避けたほうが無難でしょう。

hstoreの制限と使用例

hstoreには以下のような制限があります。

  • キーは文字列のみ。
  • 文字列の値しか扱えない。
  • ネスト構造のハッシュは取り扱えない。

キーは文字列のみ

hstoreのキーは文字列のみです。そのため、キーがシンボルのハッシュを保存すると、文字列のキーに変換されます。

example = Example.create(data: { foo: 'bar' })

example.data
=> {"foo"=>"bar"}

# シンボルキーでアクセスしてもnilが返ります。
example.data[:foo]
=> nil

# 文字列のキーでアクセスします。
example.data['foo']
=> "bar"

文字列の値しか扱えない

キーだけでなく、値も文字列のみ扱うことができます。そのため、文字列以外のデータを保存しようとしても文字列に変換されてしまいます。

# 整数をアサイン
example.data['foo'] = 1
=> 1

# 保存すると文字列に変わってしまう。
example.save
example.data['foo']
=> "1"

ネスト構造のハッシュは取り扱えない

文字列の値しか取り扱えないので、必然的にネスト構造のハッシュは取り扱えません。

example = Example.create data: {'foo' => { 'bar' => 'baz'}}

# fooの値はハッシュではなく、ただの文字列に変換されてしまう。
example.data['foo']
=> "{\"bar\"=>\"baz\"}"

example.data['foo'].class
=> String

以上のような制限はありますが、値が文字列のみで、ネストされていないフラットなデータ構造である場合は有力な候補となります。

例えば、同じEコマースサイトで服と食品両方を販売したい場合を考えてみます。この場合、商品属性は服ならサイズ、色、性別が必要ですが、食品でアレルギー物質が入っているかなどの表示が必要になります。それぞれの属性をデータベースカラムとしてしまうと、食品レコードの場合は服用のカラムはまったくの無駄となってしまいますが、hstoreカラムを使うことで解決します。

また、これらのデータはフラットなデータ構造かつ文字列のみの値なので、hstoreの適切な使用方法と言えます。

# 服の場合
product.data
=>
{ 'size' => 'S', 'color' => 'red', 'gender' => 'female' }

# 食品の場合はアレルギー物質など。
{ 'allergic' => '牡蠣' }

もし文字列以外のデータやネスト構造のデータを扱いたい場合はhstoreではなく、別記事で紹介しているjsonbを検討してみてください。

まとめ

hstoreは扱えるデータ構造に制限はありますが、フラットで文字列のみの複合データであれば有力な候補となります。特にデータ内部のキーや値を使った検索機能は便利なので、ぜひ特徴を理解してRailsアプリケーションでの使用を検討してみてください。