36

I have a site in rails and want to have site-wide settings. One part of my app can notify the admin by SMS if a specific event happens. This is an example of a feature that I want configurable via the site-wide settings.

So I was thinking I should have a Setting model or something. It needs to be a model because I want to be able to has_many :contacts for the SMS notification.

The problem is that there can only be one post in the database for the settings model. So I was thinking of using a Singleton model but that only prevents new object to be created right?

Would I still need to create getter and setter methods for each attribute like so:

def self.attribute=(param)
  Model.first.attribute = param
end

def self.attribute
  Model.first.attribute
end

Is it perhaps not best-practice to use Model.attribute directly but always create an instance of it and use that?

What should I do here?

14 Answers 14

71

(I agree with @user43685 and disagree with @Derek P -- there are lots of good reasons to keep site-wide data in the database instead of a yaml file. For example: your settings will be available on all web servers (if you have multiple web servers); changes to your settings will be ACID; you don't have to spend time implementing a YAML wrapper etc. etc.)

In rails, this is easy enough to implement, you just have to remember that your model should be a "singleton" in database terms, not in ruby object terms.

The easiest way to implement this is:

  1. Add a new model, with one column for each property you need
  2. Add a special column called "singleton_guard", and validate that it is always equal to "0", and mark it as unique (this will enforce that there is only one row in the database for this table)
  3. Add a static helper method to the model class to load the singleton row

So the migration should look something like this:

create_table :app_settings do |t|
  t.integer  :singleton_guard
  t.datetime :config_property1
  t.datetime :config_property2
  ...

  t.timestamps
end
add_index(:app_settings, :singleton_guard, :unique => true)

And the model class should look something like this:

class AppSettings < ActiveRecord::Base
  # The "singleton_guard" column is a unique column which must always be set to '0'
  # This ensures that only one AppSettings row is created
  validates_inclusion_of :singleton_guard, :in => [0]

  def self.instance
    # there will be only one row, and its ID must be '1'
    begin
      find(1)
    rescue ActiveRecord::RecordNotFound
      # slight race condition here, but it will only happen once
      row = AppSettings.new
      row.singleton_guard = 0
      row.save!
      row
    end
  end
end

In Rails >= 3.2.1 you should be able to replace the body of the "instance" getter with a call to "first_or_create!" like so:

def self.instance
  first_or_create!(singleton_guard: 0)
end
7
  • 5
    In case it saves anyone a couple minutes, I'd recommend against a model called "Config": oldwiki.rubyonrails.org/rails/pages/ReservedWords
    – user657199
    Commented Nov 3, 2012 at 2:00
  • Thanks, @Ricky, I've renamed the model
    – Rich
    Commented Nov 4, 2012 at 16:59
  • 3
    Those reserved words'll get you every time!
    – user657199
    Commented Nov 7, 2012 at 16:59
  • Why not just: class AppSettings < ActiveRecord::Base def self.instance AppSettings.first_or_create!(...) end end Commented May 11, 2017 at 14:10
  • Thanks, @Lichtamberg, I've added that to the answer. I think that didn't exist at the time I wrote this; it was added in Rails 3.2.1, Jan 2012.
    – Rich
    Commented May 11, 2017 at 14:33
25

I disagree with common opinion - there is nothing wrong with reading a property out of the database. You can read the database value and freeze if you'd like, however there could be more flexible alternatives to simple freezing.

How is YAML different from database? .. same drill - external to application code persistent setting.

Nice thing about the database approach is that it can be changed on the fly in more or less secure way (not opening and overwriting files directly). Another nice thing is it can be shared across the network between cluster nodes (if properly implemented).

The question however remains what would be the proper way to implement such a setting using ActiveRecord.

1
  • 1
    I'm reading because I have a similar challenge -- but agree that a YAML file is a poor choice. My site will have multiple users with admin rights that can edit the site setting, and I can't expect them to edit the config files.
    – Jeff
    Commented Dec 23, 2011 at 4:21
14

You could also enforce a maximum of one record as follows:

class AppConfig < ActiveRecord::Base

  before_create :confirm_singularity

  private

  def confirm_singularity
    raise Exception.new("There can be only one.") if AppConfig.count > 0
  end

end

This overrides the ActiveRecord method so that it will blow up if you try to create a new instance of the class when one already exists.

You could then go on to define only class methods that act on the one record:

class AppConfig < ActiveRecord::Base

  attr_accessible :some_boolean
  before_create :confirm_singularity

  def self.some_boolean?
    settings.some_boolean
  end

  private

  def confirm_singularity
    raise Exception.new("There can be only one.") if AppConfig.count > 0
  end

  def self.settings
    first
  end

end
2
  • Is the reference to ShopGlobalSetting class intentional? Would AppConfig count actually show you this class has one instance? Commented Apr 21, 2015 at 12:40
  • Nope, should be AppConfig. Good catch; I'll change it. And yes, he count call should work.
    – hoffm
    Commented Apr 23, 2015 at 2:44
8

I am not sure I'd waste the database/ActiveRecord/Model overhead for such a basic need. This data is relatively static (I am assuming) and on the fly calculations aren't necessary (including database lookups).

Having said that, I'd recommend you define a YAML file with your site-wide settings and define an initializer file that loads the settings into a constant. You won't have nearly as many of the unnecessary moving parts.

There is no reason that data couldn't just sit in memory and save you a ton of complexity. Constants are available everywhere, and they don't need to be initialized or instantiated. If its absolutely critical that you utilize a class as a singleton, I'd recommend doing these two things:

  1. undef the initialize/new method
  2. define only self.* methods that way it is not possible for you to maintain a state
2
  • 12
    Using a .yml file is inflexible and doesn't lend itself well to a user (rather than site author) being able to update the settings. Commented Jun 27, 2014 at 8:43
  • Do you know the dynamic roles and permissions in Rails? it cannot use the YAML static file. Very many attribute settings change usually.
    – Johan
    Commented Feb 6, 2022 at 9:17
6

I know this is an old thread, but I just needed the same thing and found out that there's a gem for this: acts_as_singleton.

Installation instructions are for Rails 2, but it works great with Rails 3 too.

3

Odds are good you don't need a singleton. It's unfortunate that one of the worst design habits to come out of the patterns craze is also one of the most commonly adopted. I blame the unfortunate appearance of simplicity, but I digress. If they had called it the "Static Global" pattern I'm sure people would have been more sheepish about using it.

I suggest using a wrapper class with a private static instance of the class you want to use for the singleton. You won't introduce a tight couple throughout your code like you will with the singleton.

Some people call this a monostate pattern. I tend to think of it as just another twist on the strategy/agent concept since you can allow for more flexibility by implementing different interfaces to expose/hide functionality.

2

Simple:

class AppSettings < ActiveRecord::Base 
  before_create do
    self.errors.add(:base, "already one setting object existing") and return false if AppSettings.exists?      
  end

  def self.instance
    AppSettings.first_or_create!(...) 
  end 
end
2

I will put a few remarks to the previous answers:

  • no need for a separate field for uniq index, we can put the constraint on the id field. Since we should have id field in a Rails app anyway, it's a good tradeoff
  • no need special validations on the model and explicit ID assignment, it's enough to put default value on the column

If we apply these modifications, the solution becomes very easy:

# migration
create_table :settings, id: false do |t|
  t.integer :id, null: false, primary_key: true, default: 1, index: {unique: true}
  t.integer :setting1
  t.integer :setting2
  ...
end

# model
class Settings < ApplicationRecord
  def self.instance
    first_or_create!(...)
  end  
end
4
  • Why not def self.find(*args); first_or_create!; end instead of self.instance? Commented Dec 10, 2020 at 10:54
  • 1
    @JoshuaMuheim it's up to you. instance is just a common name for a singleton method, that's it.
    – ka8725
    Commented Dec 11, 2020 at 11:20
  • that default value guarantees that create method will be executed with id: 1, but actually you table can contain as many values as possible (until you are out of range), so 2 consequent calls of create(id: 1), create(id: 2) will lead to 2 records
    – Nick Roz
    Commented Apr 13, 2022 at 19:31
  • unique index on the primary key as well as not null is redundant
    – Nick Roz
    Commented Apr 13, 2022 at 19:50
1

Using has_many :contacts doesn't mean you need a model. has_many does some magic, but in the end it's just adds some method with a specified contract. There's no reason why you can't implement those methods (or some subset that you need) to make your model behave like it has_many :contacts yet not actually use an ActiveRecord model (or model at all) for Contact.

1

You might also check out Configatron:

http://configatron.mackframework.com/

Configatron makes configuring your applications and scripts incredibly easy. No longer is a there a need to use constants or global variables. Now you can use a simple and painless system to configure your life. And, because it‘s all Ruby, you can do any crazy thing you would like to!

1
class Constant < ActiveRecord::Base
  after_initialize :readonly!

  def self.const_missing(name)
    first[name.to_s.downcase]
  end
end

Constant::FIELD_NAME

1
1

You can do it like this:

 class Config < ApplicationRecord
  def self.instance
    Config.first || Config.create!
  end
 end
1
  • That is actually the same as first_or_create! and does not protect from race conditions
    – Nick Roz
    Commented Apr 13, 2022 at 19:08
1

I assume to use inheriting column type with uniq constraint.

# miragtion
class CreateSingletonRecords < ActiveRecord::Migration[5.2]
  create_table :balance_holders do |t|
    t.string :type
    t.index :type, unique: true

    t.timestamps
  end
end

several methods in you parent class:

class SingletonRecord < ApplicationRecord
  class << self
    def instance
      @singleton__instance__
    end

    def load_record(params = {})
      @singleton__instance__ = find_or_create_by!(params)
    end
  end

  load_record

  validates :type, uniqueness: true
end

After it you can forever use single record for singleton model class. Your instance will be loaded or created one time durring model class loaded.

1

I'd like to sum up the things, because none of the answers seem to cover all the aspects.

In case you truly want to make model a Singleton you need:

  1. Track uniqueness
  2. Use the only method to access config
  3. Avoid race conditions

Track uniqueness

= Force your database to keep only one row.

Do NOT rely on before actions in Rails, as well as on uniqueness validator.

You must create a table with a column that is resposible for uniqueness.

This column must be:

  • either a primary key
  • or a not null column with unique index.

Is also should allow only one value possible, so wee need an additional constraint in both cases.

Choose one of the following.

  1. Use a separate column

    Very simple, does not ovverride Rails standart behaviour. NOT NULL constaint and unique index are obligatory.

    class Config < ActiveRecord::Migration[6.1]
      def change
        create_table :configs do |t|
          t.integer :code, null: false, default: 1, index: { unique: true }
          t.string :setting
          t.timestamps # if needed
        end
       add_check_constraint :configs, 'code = 1', name: 'check_config_code_equals_1'
      end
    end
    
  2. Redefine primary key

    Technically you can create a self-contradictory (null primary key), but this is not the case. Rails is clever enough to do everything right.

    NOT NULL as well as unique index are totally redundant.

    class Config < ActiveRecord::Migration[6.1]
      def change
        create_table :configs, id: false do |t|
          t.integer :id, primary_key: true, default: 1
          t.string :setting
          t.timestamps # if needed
        end
       add_check_constraint :configs, 'id = 1', name: 'check_config_id_equals_1'
      end
    end
    
  3. [pick me] Use boolean column (even better) as primary key.

    Integer id column does not make any sence. Check constraint is extremely simple, no room for error

    class Config < ActiveRecord::Migration[6.1]
      def change
        create_table :configs, id: false do |t|
          t.boolean :id, primary_key: true, default: true
          t.string :setting
          t.timestamps # if needed
        end
       add_check_constraint :configs, 'id', name: 'check_config_id'
      end
    end
    

Use only one access point

+ Avoid race conditions

Define instance class method.

Keep in mind that first_or_create as well as many other Rails helpers are not thread-safe. So make retry:

class Config < ApplicationRecord
  def self.instance
    attempts = 1
    first_or_create!
  rescue ActiveRecord::RecordNotUnique => e
    if attempts += 1 <= 3
      sleep 0.005 # optional
      retry
    else
      raise e
    end
  end
end
2

Not the answer you're looking for? Browse other questions tagged or ask your own question.