I recently worked on an app where an admin user needed to be able to tweak an app-wide configuration settings. For example the default title for HTML pages, or the default commission for newly hired sales people. Some settings were text values, some dates, some numbers, and all had different validations.
In this post I’ll explain how you can easily solve this problem using unique STI (Single-Table-Inheritance).
Create the base model / migration
class ApplicationSetting
end
class CreateApplicationSettings < ActiveRecord::Migration
def self.up
create_table :application_settings do |t|
t.string :type, :null => false
t.string :value
end
add_index :application_settings, :type, :unique => true
end
end
Notice that the type column, which indicates that the ActiveRecord model should use Single-Table-Inheritance, has a
unique constraint. That means that you can only have 1 row per type, which means that there will only ever be a single
instance of a given settings class.
The models
Let’s say the first application setting is the default page title, which is a String.
# app/models/application_settings/default_page_title.rb
class ApplicationSettings::DefaultPageTitle < ApplicationSetting
validates :title, :presence => true, :length => {:maximum => 50}
def self.get
first || create!(:title => "Welcome to acme.com")
end
def title() value end
def title=(value) self.value = value end
end
There are a few noteworthy concepts in this model. The first is that it aliases the value column to be something more
descriptive (title). The second is that there
is a validation on the aliased method, which means that you’ll get a more friendly error message like ‘Title can’t be blank’,
as opposed to ‘Value can’t be blank’.
Finally, there is a get method, that ensures that if no record exists in the database, one is created with a
sort of meta-default value. This comes in very handy when creating forms and working with the controllers.
Next let’s store an integer value, representing the default commission percentage for newly-hired salespeople:
class ApplicationSettings::DefaultCommission < ApplicationSetting
DEFAULT_PERCENTAGE = 10
validates :percentage, :numericality => {
:less_than_or_equal_to => 100,
:greater_than_or_equal_to => 0
}
def self.get
first || create!(:percentage => DEFAULT_PERCENTAGE)
end
def percentage() value.to_i end
def percentage=(value) self.value = value.to_i.to_s end
end
Note how by defining a setting-specific getter (percentage) and setter (percent=) it’s easy to store all values as
strings and then coerce the value into
something that can be more easily handled by rails view helpers and validations.
You may want to store each value in a different strongly-typed column in the database (like string_value, int_value, date_value etc…) and let rails handle the type casting,
and if you did, your code would be virtually identical:
class CreateApplicationSettings < ActiveRecord::Migration
def self.up
create_table :application_settings do |t|
t.string :type, :null => false
t.string :string_value
t.integer :int_value
t.date :date_value
# etc...
end
add_index :application_settings, :type, :unique => true
end
end
class ApplicationSettings::DefaultCommission < ApplicationSetting
DEFAULT_PERCENTAGE = 10
validates :percentage, :numericality => {
:less_than_or_equal_to => 100,
:greater_than_or_equal_to => 0
}
def self.get
first || create!(:percentage => DEFAULT_PERCENTAGE)
end
def percentage() int_value end
def percentage=(value) self.int_value = value end
end
The routes
As far as the routes go, you could map to a different controller for each settings class, or map to a single controller with custom actions – depends on your preference.
Here’s an example of mapping everything to one controller:
# config/routes.rb
namespace :admin do
resources :application_settings do
collection do
put :default_page_title # => PUT /admin/application_settings/default_page_title
put :default_commission
end
end
end
The view
# app/views/admin/application_settings/index.html.erb
<%= form_for ApplicationSettings::DefaultPageTitle.get, :url => default_page_title_admin_application_settings_path(@default_page_title), :as => :setting do |f| %>
<%= f.error_messages %>
<%= f.label :title %>
<%= f.text_field :title, :size => 50, :maxlength => 50 %>
<%= f.submit "Save" %>
<% end %>
<%= form_for ApplicationSettings::DefaultCommission.get, :url => default_commission_admin_application_settings_path(@default_commission), :as => :setting do |f| %>
<%= f.error_messages %>
<%= f.label :percentage %>
<%= f.text_field :percentage, :size => 3, :maxlength => 3 %>
<%= f.submit "Save" %>
<% end %>
From the form’s point of view, each of these objects is completely separate. Notice the :as => :setting option in form_for.
This ensures that when the params get to the controller, they can be accessed with params[:setting], as opposed to
params[:application_setting_default_page_title] – that’s an important step to keeping the controller DRY.
The controller
# app/controllers/admin/application_settings_controller.rb
class Admin::ApplicationSettingsController < ApplicationController
def default_page_title
update_setting ApplicationSettings::DefaultPageTitle
end
def default_commission
update_setting ApplicationSettings::DefaultCommission
end
private
def update_setting(klass)
setting = klass.get
setting.update_attributes(params[:setting])
redirect_to admin_application_settings_path
end
end
Since each settings class conforms to the same interface (get), and the view has ensured that the params get sent up
as params[:setting] the controller becomes pretty trivial.
Usage
Anywhere you need access to the value of a setting, you just call get on the appropriate settings object. Obviously if this were
production code you could cache those settings for performance.
References
I originally learned about this modeling pattern from Dan Chak’s book Enterprise Rails.
Sounds like a whole lot of overhead to me, have you tried using something simpler like https://github.com/ledermann/rails-settings ?
Just use it with a generic controller that list/updates all keys/values.
May 23, 2011 at 4:16 am
Thanks for the link. The point of this post was to show how you can add validations with friendly error messages for each setting. But if I need a generic key/value store in I’ll give it a try.
May 23, 2011 at 7:41 am
That’s pretty sweet! You might be able to avoid `:as => :setting` by overriding `.model_name`. Then you wouldn’t have to think about it when you make each form.
May 25, 2011 at 4:42 am
Strange approach from maintenance stand point.
You need work hard to support this structure.
Getting custom validation for each attribute is not a big deal.
Try serialization for all attributes and you won’t need migrations at all.
“`
class Settings < AR:B
serialize :options, Hash
delegate :opt1, :opt2, :to => Hash
validate :opt1 ….
validate :opt2
end
“`
No types, no conventions. All data types allowed.
May 30, 2011 at 5:53 am
@bogdan – while some variation of your idea might work, the code you provided doesn’t work at all, and it took me a while to write up something along those lines that does work. Here’s what I came up with:
class Setting < ActiveRecord::Base
serialize :options, Hash
attr_accessor :should_validate_default_page_title, :should_validate_default_commission_percentage
validates :default_commission_percentage, :numericality => {
:less_than_or_equal_to => 100,
:greater_than_or_equal_to => 0
}, :if => :should_validate_default_commission_percentage
validates :default_page_title,
:presence => true,
:length => {:maximum => 50},
:if => :should_validate_default_page_title
def self.get_default_commission_percentage
order(:created_at).first || create!(:default_commission_percentage => DEFAULT_PERCENTAGE)
end
def self.get_default_page_title
order(:created_at).first || create!(:title => “Welcome to acme.com”)
end
def default_commission_percentage
self.options ||= {}
options[:default_commission_percentage]
end
def default_commission_percentage=(value)
self.options ||= {}
options[:default_commission_percentage]=value
end
def default_page_title
self.options ||= {}
options[:page_title]
end
def default_page_title=(value)
self.options ||= {}
options[:page_title]=value
end
end
I see a number of issues with this approach, namely:
* you have to conditionally validate each field, or force a user to fill out all values at once, because all validations will run by default when the object is saved. This seems like a major potential source for error, since it violates the principle of least surprise for developers who have to maintain the code
* it violates the open/closed principle, because any time you add a new setting, you have to modify existing code. The example I mentioned above allows for easy extension, but doesn’t require modifying existing code.
* there’s no guarantee that there will be a single record in the database, so you have to rely on order(:created_at).first
* since it’s serialized, it limits your ability to do anything with useful with database queries, since you need a yaml parser) to make those values useful.
I appreciate the comments and feedback, but I’m a bit surprised at the pushback. It seemed to me like a dead simple, OO solution. I’d be interested to know why it seems like there’s a lot of “overhead” and why “convention” would be construed as a bad thing.
In practice I’ve found this to be super simple to write from scratch, adding new settings to the app takes me all of 1-2 minutes and I haven’t encountered any problems with it.
June 4, 2011 at 11:41 pm
It does seem like a lot of code for something where the first instinct would be something like:
setting :default_commission_percentage, :decimal
setting :default_page_title, :string, :default => “welcome to (your company here)”
but given the requirements as stated (e.g. setting-specific validation), perhaps it isn’t so bad.
June 6, 2011 at 7:48 am