如何实现单例模型

时间:2008-12-30 03:15:20

标签: ruby-on-rails singleton

我在rails中有一个站点,并希望进行站点范围的设置。如果发生特定事件,我的应用程序的一部分可以通过SMS通知管理员。这是我希望通过站点范围设置进行配置的功能示例。

所以我觉得我应该有一个环境模型或其他东西。它需要是一个模型,因为我希望能够使用has_many:SMS通知的联系人。

问题是数据库中只能有一个用于设置模型的帖子。所以我在考虑使用Singleton模型,但这只能阻止创建新对象吗?

我还需要为每个属性创建getter和setter方法,如下所示:

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

def self.attribute
  Model.first.attribute
end

直接使用Model.attribute可能不是最佳实践,但总是创建它的实例并使用它吗?

我该怎么办?

13 个答案:

答案 0 :(得分:55)

(我同意@ user43685并且不同意@Derek P - 有很多理由将网站范围内的数据保存在数据库而不是yaml文件中。例如:您的设置将在所有Web服务器上提供(如果您有多个Web服务器);对您的设置的更改将是ACID;您不必花时间实现YAML包装等。)

在rails中,这很容易实现,你只需记住你的模型应该是数据库术语中的“单例”,而不是红宝石对象术语。

实现这一目标的最简单方法是:

  1. 添加新模型,每个属性需要一列
  2. 添加一个名为“singleton_guard”的特殊列,并验证它始终等于“0”,并将其标记为唯一(这将强制该表中数据库中只有一行)
  3. 向模型类添加静态辅助方法以加载单行
  4. 所以迁移应该是这样的:

    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)
    

    模型类看起来像这样:

    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
    

    在Rails&gt; = 3.2.1中,您应该能够通过调用“first_or_create!”替换“实例”getter的主体,如下所示:

    def self.instance
      first_or_create!(singleton_guard: 0)
    end
    

答案 1 :(得分:24)

我不同意共同意见 - 从数据库中读取属性没有任何问题。如果您愿意,可以读取数据库值并冻结,但是可以有更灵活的替代方法来进行简单冻结。

YAML与数据库有何不同? ..相同的钻取 - 在应用程序代码持久性设置之外。

数据库方法的好处在于它可以以或多或少的安全方式即时更改(不直接打开和覆盖文件)。另一个好处是它可以在群集节点之间通过网络共享(如果正确实现)。

然而,问题仍然是使用ActiveRecord实现此类设置的正确方法。

答案 2 :(得分:11)

您还可以按如下方式强制执行最多一条记录:

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

这会覆盖ActiveRecord方法,以便在您已经存在的情况下尝试创建该类的新实例时它会爆炸。

然后,您可以继续仅定义作用于一条记录的类方法:

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

答案 3 :(得分:10)

我不确定是否会浪费数据库/ ActiveRecord / Model开销以满足这样的基本需求。这个数据是相对静态的(我假设)并且不需要动态计算(包括数据库查找)。

话虽如此,我建议您使用站点范围的设置定义YAML文件,并定义一个初始化文件,将文件加载到常量中。你不会有太多不必要的活动部件。

没有理由不能将数据放在内存中并为您节省大量复杂性。常量随处可用,无需初始化或实例化。如果你将一个类作为一个单独使用是绝对关键的,我建议你做这两件事:

  1. 取消初始化/新方法
  2. 仅定义self。*方法,使您无法维持状态

答案 4 :(得分:6)

我知道这是一个旧线程,但我只是需要相同的东西,并发现有一个宝石:acts_as_singleton

安装说明适用于Rails 2,但它也适用于Rails 3。

答案 5 :(得分:3)

赔率很高,你不需要单身人士。不幸的是,从模式热潮中走出来的最糟糕的设计习惯之一也是最常采用的习惯之一。我责怪简单的不幸外表,但我离题了。如果他们称之为“静态全球”模式,我相信人们对使用它会更加羞怯。

我建议使用一个包装类,该类包含要用于单例的类的私有静态实例。你不会像使用单身人士那样在你的代码中引入紧密的一对。

有些人称之为单一模式。我倾向于将其视为策略/代理概念的另一个转折因为您可以通过实现不同的接口来实现更多的灵活性来公开/隐藏功能。

答案 6 :(得分:2)

简单:

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

答案 7 :(得分:1)

使用has_many :contacts并不意味着你需要一个模型。 has_many做了一些魔术,但最后它只是添加了一些指定合同的方法。没有理由不能实现那些方法(或者你需要的某个子集)来使你的模型表现得像has_many :contacts,但实际上并没有使用ActiveRecord模型(或模型)来接触。

答案 8 :(得分:1)

您也可以查看Configatron:

http://configatron.mackframework.com/

Configatron可让您轻松轻松地配置应用程序和脚本。不再需要使用常量或全局变量。现在,您可以使用简单,无痛的系统来配置您的生活。而且,因为它都是Ruby,你可以做任何你想要的疯狂事情!

答案 9 :(得分:1)

class Constant < ActiveRecord::Base
  after_initialize :readonly!

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

常量:: FIELD_NAME

答案 10 :(得分:1)

您可以这样做:

 class Config < ApplicationRecord
  def self.instance
    Config.first || Config.create!
  end
 end

答案 11 :(得分:0)

我假设要使用具有唯一约束的继承列type

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

    t.timestamps
  end
end

父类中的几种方法:

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

之后,您可以将单个记录永久用于单例模型类。 在加载模型类时,将一次加载或创建您的实例。

答案 12 :(得分:0)

我将对先前的答案发表一些看法:

  • 不需要uniq索引的单独字段,我们可以将约束放在id字段上。由于无论如何我们都应该在Rails应用程序中有id字段,所以这是一个很好的权衡方法
  • 无需对模型和显式ID进行特殊验证,将默认值放在列中就足够了

如果我们应用这些修改,该解决方案将变得非常简单:

# 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