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アプリケーションでの使用を検討してみてください。
