• 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:

[code lang=bash]
sudo apt-get install postgres-contrib
[/code]

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:

[code lang=sql]
\c my_database
create schema extensions authorization my_user;
[/code]

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)

[code lang=sql]
create extension hstore schema extensions;
[/code]

Install the hstore extension into the extensions schema.

[code lang=sql]
create extension hstore schema extensions;
[/code]

Now, restart the database server.

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

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

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:

[code lang=ruby]
class AddSettingsColumnToCompany < ActiveRecord::Migration
def change
add_column :companies, :settings, :hstore
end
end

rake apartment:migrate
[/code]

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

[code lang=ruby]
class Company < ActiveRecord::Base
serialize :settings, ActiveRecord::Coders::Hstore
# ...
end
[/code]

Now, we can store our settings like this:

[code lang=ruby]
company = Company.new
company.settings = {my_custom_setting: true}
company.save

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

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:

[code lang=ruby]
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
[/code]

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:

[code lang=ruby]
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"
[/code]

Subscribe to future posts

No yukky spam • No more than one email a month