Krzysztof Zalewski

zlw@github:pages

Rails: Attr_accessor for HStore

HStore is great feature in PostgreSQL. It allows us to use “mini-noSQL” db in out pg table. Nice!

Rails4 will have this baked-in, but now we have to use great gem activerecord-postgres-hstore.

The only problem is, that there’s no accessor methods. Let’s assume that our model (Product) need price and weight. We will have to write something like this:

product.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Product < ActiveRecord::Base
  def price
    data && data['price']
  end

  def price=(val)
    self.data = (data || {}).merge(price: val)
  end

  def weight
    data && data['weight']
  end

  def weight=(val)
    self.data = (data || {}).merge(weight: val)
  end
end

A lot of repetition. Definetly not DRY. We could use some metaprogramming and create this:

product.rb
1
2
3
4
5
6
7
8
9
10
11
class Product < ActiveRecord::Base
  [:price, :weight].each do |e|
    define_method e do
      data && data[e.to_s]
    end

    define_method :"#{e}=" do |val|
      self.data = (data || {}).merge(e => val)
    end
  end
end

Nice, a lot cleaner. But maybe extract this so we could use it anywhere?

hstore_accessor.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module HstoreAccessor
  extend ActiveSupport::Concern

  module ClassMethods
    # Define accessors method for HStore field
    #
    # @param [Symbol] hstore name of HStore field
    # @param [Array] fields list of accessors methods to define
    def hstore_accessor(hstore, *fields)
      fields.each do |field|
        define_hstore_reader hstore, field
        define_hstore_writer hstore, field
      end
    end


    private

    def define_hstore_reader(hstore, field)
      define_method field do
        send(hstore) && send(hstore)[field]
      end
    end

    def define_hstore_writer(hstore, field)
      define_method :"#{field}=" do |val|
        val = (send(hstore) || {}).merge(field => val)
        send(:"#{hstore}=", val)
      end
    end
  end
end

Now we can do this:

product.rb
1
2
3
4
class Product < ActiveRecord::Base
  include HstoreAccessor
  hstore_accessor :data, :price, :weight
end
Usage
1
2
3
4
5
6
7
p = Product.new

p.price = '$45'
p.price #=> $45

p.weight = '100kg'
p.weight #=> 100kg

Comments