루비 Hash를 데이타베이스 칼럼에 저장하기
루비 해시(Hash) 타입의 데이타를 저장할 text형 칼럼을 생성한다.
rails g migration add_bio_to_users bio:text
User 모델에서 ActiveRecord::Store#store 메쏘드를 사용하여 위 bio 칼럼이 저장되거나 로드될 때 JSON으로 인코딩/디코딩되도록 지정한다.

# app/models/user.rb
class User < ActiveRecord::Base
  store :bio, coder: JSON
end
이제 레일즈에서 다음과 같은 작업을 수행할 수 있다.

user = User.new(name: 'foo')
user.bio = {gender: 'Male', height: 180}
user.save
user.reload
user.bio[:gender] # => "Male"
user.bio[:height] # => 180
store()에서 accessors 옵션을 지정하면 해당 attributes에 대한 게터(Getter)/세터(Setter)가 만들어진다.

# app/models/user.rb
class User < ActiveRecord::Base
  store :bio, coder: JSON, accessors: [ :gender, :height ]
end
이제 위 예제는 아래와 같이 새로 쓸 수 있다.

user = User.new(name: 'foo')
user.gender = "Male"
user.height = 180
# 또는 user.bio = {gender: 'Male', height: 180}
user.save
user.reload
user.gender # => "Male"
user.height # => 180
이와같이 루비의 해시(Hash)형 자료를 데이타베이스에 저장하는 것은 비교적 간단하나 쿼리 작업을 수행할 때는 문제가 발생한다. DB 저장시 해시 구조가 스트링 형태로 그대로 저장되기 때문에 구조화된 쿼리가 불가능해진다.
Hstore 컬럼 사용하기
hstore는 PostgreSQL 9.2부터 지원된 데이타 타입으로 키(Key)-값(Value) 형태의 데이타 셋을 저장하기 위해 사용된다. 레일즈에서도 버전 4.0부터 이 기능을 지원하고 있다. hstore를 사용하기 위해서는 PostgreSQL 서버에 hstore extension을 설치해야 한다. 마이그레이션 파일을 만든 후 hstore형의 칼럼을 생성하고 gin 방식의 인덱스를 지정한다.
rails g migration add_settings_to_users settings:hstore

# db/migrate/xxxx_add_settings_to_users.rb
class AddSettingsToUsers < ActiveRecord::Migration
  def change
    enable_extension 'hstore'
    add_column :users, :settings, :hstore
    add_index :users, :settings, using: :gin
  end
end
rake db:migrate
이제 아래와 같이 hstore를 이용할 수 있다.

user = User.new(name: 'foo')
user.settings = {timezone: 'KST', lang: 'Korean', level: 5}
user.save

user.reload
user.settings["timezone"] # => "KST"
user.settings["lang"] # => "Korean"
user.settings["level"] # => "5"

user.settings["age"] = 30
user.save
user.reload
user.settings["age"] # => "30"

user.settings.delete("age")
user.save
user.reload
user.settings # => {"lang"=>"Korean", "level"=>"5", "timezone"=>"KST"}
hstore의 장점중의 하나는 쿼리를 자유롭게 수행할 수 있다는 점이다.

# 'lang' 키를 갖는 User
User.where("settings ? :key", key: "lang")

# 'lang' 키의 값이 "Korean"인 User
User.where("settings -> 'lang' = 'Korean'")
User.where("settings @> hstore(:key, :value)", key: "lang", value: "Korean")

# 'lang' 키의 값이 서브스트링을 만족하는 User
User.where("settings -> 'lang' ILIKE '%kor%'")
User.where("settings -> :key ILIKE :value", key: "lang", value: "%kor%")

# 타입 캐스팅으로 int 변환 후 사용하는 예
User.where("(settings -> 'level')::int > :value", value: 2)

# scope를 사용해서 커스터마이즈한 쿼리 수행 하기
scope :has_settings, lambda { |key, value| where("settings @> ('#{key} => #{value}')")}
User.has_settings(:lang, "Korean")
hstore는 DB 저장시 키와 밸류 모두 스트링형으로 저장되기 때문에 사용 목적에 따라 형 변환을 일일히 지정해야 하는 불편함이 발생하는 경우가 있다. 이를 해결하기 위해서 settings 속성을 시리얼라이즈화 시키면서 Virtus 젬에 의해 형 변환이 자동으로 발생하도록 한다.
vi Gemfile
gem 'virtus'
vi user.rb

# app/models/user.rb
serialize :settings, UserSettings
vi user_settings.rb

# app/models/concerns/user_settings.rb
class UserSettings
  include Virtus.model

  attribute :lang, String
  attribute :timezone, String
  attribute :level, Integer

  def self.dump(settings)
    settings.to_hash
  end

  def self.load(hash)
    new(hash)
  end
end
UserSettings 클래스에 Virtus.model을 믹신(Mix-in)한 후, attribute() 메쏘드를 사용해서 키(key)와 이 값에 대한 형(type)을 지정한다. 현재 UserSettings는 시리얼라이즈 오브젝트이기 때문에 클래스 메쏘드인 dump()load()가 정의되어 있어야 한다. 이 두 메쏘드는 DB에 저장될 때와 읽을때 각각 호출된다. 저장될 때는 Virtus화된 UserSettings 인스턴스 hash로 바꿔서 저장하고, 읽어드릴때는 hash 형태의 데이타를 UserSettings 인스턴스로 변환해서 사용자에게 제공한다.

user = User.first
user.settings.level # => 5
user.settings["level"] # => 5
user.settings[:level] # => 5

user.settings # => #
user.settings.to_hash # => {:lang=>"English", :timezone=>"KST", :level=>5}
Jsonb 컬럼 사용하기
jsonb PostgreSQL 9.4부터 지원된 데이타 타입으로 비관계형 디큐멘트형의 데이타를 저장하기 위해 사용된다. 레일즈에서는 버전 4.2부터 이 기능을 지원하기 시작한다. 먼저 아래와 같이 jsonb 타입의 칼럼을 생성한다.
rails g migration add_preferences_to_users preferences:jsonb

# db/migrate/xxxx_add_preferences_to_users.rb
class AddPreferencesToUsers < ActiveRecord::Migration
  def change
    add_column :users, :preferences, :jsonb, null: false, default: '{}'
    add_index :users, :preferences, using: :gin
  end
end
rake db:migrate
레일즈 콘솔에서 아래와 같이 동작하는지를 확인한다.

user = User.new(name: 'foo')
user.preferences = { browser: 'chrome', os: 'linux', language: 'ruby', theme: {color: 'red', size: 10}, rss: true }
user.save

user.reload
user.preferences["browser"] # => "chrome"
user.preferences["theme"]["size"] # => 10
user.preferences["rss"] # => true
위 예에서 보듯이 jsonb 형은 hstore와 달리 중첩된 해시(nested hash)를 지원할 뿐 아니라 밸류(Value)의 저장시 원래 타입을 그대로 유지하는 장점이 있다.
만약 키(key)에 스트링형 뿐 아니라 심볼을 사용하고 싶다면 시리얼라이저를 사용해서 동작을 수정한다.
vi user.rb

# app/models/user.rb
serialize :preferences, HashSerializer
vi hash_serializer.rb

# app/serializer/hash_serializer.rb
class HashSerializer
  def self.dump(hash)
    hash.to_json
  end

  def self.load(hash)
    (hash || {}).with_indifferent_access
  end
end
아래 두 오퍼레이션은 같은 결과를 낳는다.

user.preferences[:language]
user.preferences["language"]
만일 jsonb 칼럼의 특정 키(key)에 대한 전용 액세서가 필요한면 store_accessor()를 활용한다.
vi user.rb

# app/models/user.rb
serialize :preferences, HashSerializer
store_accessor :preferences, :language
store_accessor()에 의해서 language=() 와 language()가 주어진다.

user.language = 'python'
user.language # => 'python'
user.preferences[:language] # => 'python'
user.preferences["language"] # => 'python'
jsonb 칼럼에 대한 쿼리는 아래와 같이 수행한다. PostgreSQL에서 지원하는 jsonb 타입에 대한 오퍼레이터에 대한 자세한 설명은 이곳을 참조한다.

User.create(name: 'foo', preferences: {editor: 'vim', languages: [:ruby, :javascript, :python]})

# editor가 vim인 User
User.where('preferences @> ?', {editor: 'vim'}.to_json)

# ruby와 python을 모두 사용하는 User
User.where("preferences -> 'languages' ?& array[:keys]", keys: ['ruby', 'python'])

# ruby 또는 python을 사용하는 User
User.where("preferences -> 'languages' ?! array[:keys]", keys: ['ruby', 'python'])

# editor가 vim이고 ruby를 사용하는 User
User.where('preferences @> ?', {editor: 'vim', languages: [:ruby]}.to_json)