How to implement a singleton model

后端 未结 13 1321
余生分开走
余生分开走 2020-12-24 12: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

相关标签:
13条回答
  • 2020-12-24 12:47

    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.

    0 讨论(0)
  • 2020-12-24 12:48

    (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
    
    0 讨论(0)
  • 2020-12-24 12:48

    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
    
    0 讨论(0)
  • 2020-12-24 12:49

    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.

    0 讨论(0)
  • 2020-12-24 12:50

    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
    
    0 讨论(0)
  • 2020-12-24 12:55

    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!

    0 讨论(0)
提交回复
热议问题