• ok, so I’ll actually be talking about Company settings, not User settings. You get the idea.

I needed to start storing some per-company configuration settings in a client app. I could obviously start adding columns to the Company table but these settings will: a) change over time; b) only apply to a few companies (the rest will use the default); c) don’t need to be queried. I could have just used a hash and the Rails serialize functionality but that makes the values quite opaque in the database. Given this, I decided to go with a hstore column in Postgres.

hstore

hstore is a proper key-value data type extension which you can add to Postgres. It allows you to store (and query!) key-values in a single column. To use hstore you’ll first need to install it. On Ubuntu:

    sudo apt-get install postgres-contrib

Rails 4 has support for this datatype built-in but for Rails 3.2 you’ll need the activerecord-postgres-hstore gem. This gem includes a migration (rails g hstore:setup) for installing the hstore extension into your database… but, wait, are you using Apartment to manage subdomains? I was…

Apartment

Apartment is a great gem for making use of Postgres schemas to support a subdomain-based multi-tenant application — but it makes life a little trickier at times. The Apartment README actually has a good explanation of how to use Hstore but I’ll reiterate it here because it’s not optional (I tried!).

  • The hstore extension can only be installed once per-database
  • Apartment will run each migration multiple times (once for each schema).
  • Can you spot the problem yet?
  • Also, the hstore extension will be installed to a particular schema but Apartment works by dynamically changing the current schema
  • Can you spot another problem?

Ok, so here’s the plan. We need to install the hstore extension into one schema, only once, and make that schema available at all times. We need to do some manual work instead of using the Rails migrations:

In psql:

c my_database
create schema extensions authorization my_user;

Create a new schema called extensions and make sure it’s owned by my_user (you can’t change ownership of a schema after it’s been created). We’ll use this schema to install the hstore extension (and all other future postgres extensions why might use)

create extension hstore schema extensions;

Install the hstore extension into the extensions schema.

create extension hstore schema extensions;

Now, restart the database server.

Finally, we need to make sure that the extensions schema is always available. So, in your apartment.rb file:

Apartment.configure do |config|
  # ...
  config.excluded_models = ["Company", "Delayed::Job"]
  config.persistent_schemas = ['extensions']
end

We also need to add the extensions schema to the database.yml config file so that we can access the hstore data type during migrations:

development:
  adapter: postgresql
  encoding: unicode
  database: development
  pool: 5
  username: <%= ENV['DB_USER'] %>
  password: <%= ENV['DB_PASSWORD'] %>
  schema_search_path: 'public,extensions'

Restart the server. Now you should have hstore, Rails and Apartment playing nicely together.

Using hstore (finally!)

After all that we can finally start adding the hstore settings column to our Company model. Generate the migration:

class AddSettingsColumnToCompany < ActiveRecord::Migration
  def change
    add_column :companies, :settings, :hstore
  end
end

rake apartment:migrate

Then add the settings column to our model. Note how this works with the serialize method but uses a custom serializer

class Company < ActiveRecord::Base
  serialize :settings, ActiveRecord::Coders::Hstore
  # ...
end

Now, we can store our settings like this:

company = Company.new
company.settings = {my_custom_setting: true}
company.save

company.settings['my_custom_setting']          #=> "true"

Note one oddity: hstore only stores strings so once persisted our true becomes "true".

Sprinkle Ruby for Effect

I don’t really want to have to remember the keys that I’ve used and I’d like some convenient — I’d much prefer if I could access the settings using standard ruby methods.

A little meta-programming:

require 'active_support/concern'

module Settings
  extend ActiveSupport::Concern

  module ClassMethods
    def settings_for *args, opts
      default_value = opts[:default]

      args.each do |setting|
        setting = setting.to_s
        getter = "#{setting}"
        setter = "#{setting}="
        check = "#{setting}?"

        define_method(getter) do
          get_setting setting, default_value
        end

        define_method(setter) do |value|
          settings[setting] = value
        end

        define_method(check) do
          get_setting(setting, default_value) ? true : false
        end
      end
    end
  end

  # hstore only stores strings so restore any boolean values
  def get_setting key, default=nil
    value = settings.fetch key.to_s, default
    value = (value == 'true') if %w(true false).include? value
    value
  end

end

Yeah, that code could be improved but you get the idea (and I’ve since discovered it’s a fairly popular approach seen in this blog post and this gem). So now we can do:

require 'settings'

class Company < ActiveRecord::Base
  include Settings

  serialize :settings, ActiveRecord::Coders::Hstore
  settings_for :hide_draft_review_scores, {default: false}
  # ...
end

company = Company.last
company.hide_draft_review_scores?     #=> false
company.hide_draft_review_scores = true
company.hide_draft_review_scores?     #=> true
company.hide_draft_review_scores      #=> "true"

Subscribe to future posts

No yukky spam • No more than one email a month