33

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

所以我想我应该有一个设置模型或其他东西。它需要是一个模型,因为我希望能够 has_many :contacts 用于 SMS 通知。

问题是设置模型的数据库中只能有一篇文章。所以我正在考虑使用单例模型,但这只会阻止创建新对象吗?

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

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

def self.attribute
  Model.first.attribute
end

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

我应该在这里做什么?

4

13 回答 13

68

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

在 Rails 中,这很容易实现,您只需要记住您的模型应该是数据库术语中的“单例”,而不是 ruby​​ 对象术语。

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

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

所以迁移应该是这样的:

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 >= 3.2.1 中,您应该能够通过调用“ first_or_create! ”来替换“实例”getter 的主体,如下所示:

def self.instance
  first_or_create!(singleton_guard: 0)
end
于 2012-09-17T16:13:03.137 回答
25

我不同意普遍的看法——从数据库中读取属性并没有错。如果您愿意,您可以读取数据库值并冻结,但是可以有更灵活的替代简单冻结。

YAML 与数据库有何不同?.. 相同的练习 - 应用程序代码持久设置的外部。

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

然而,问题仍然是使用 ActiveRecord 实现这种设置的正确方法是什么。

于 2009-01-13T21:40:38.197 回答
14

您还可以强制执行最多一条记录,如下所示:

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
于 2013-06-28T20:57:55.220 回答
8

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

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

没有理由数据不能只存在于内存中并为您节省大量复杂性。常量无处不在,不需要初始化或实例化。如果将类用作单例绝对至关重要,我建议您做以下两件事:

  1. 取消定义初始化/新方法
  2. 只定义 self.* 方法,这样你就不可能维持一个状态
于 2008-12-30T05:36:02.070 回答
6

我知道这是一个旧线程,但我只需要同样的东西,发现有一个宝石可以解决这个问题:acts_as_singleton

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

于 2013-02-26T17:43:19.963 回答
3

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

我建议使用包装类和您要用于单例的类的私有静态实例。您不会像在单例中那样在整个代码中引入紧密的一对。

有些人称之为单态模式。我倾向于认为它只是策略/代理概念的另一个转折,因为您可以通过实现不同的接口来公开/隐藏功能来提供更大的灵活性。

于 2008-12-30T04:58:18.403 回答
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
于 2017-05-11T14:11:15.137 回答
2

我将对之前的答案发表一些评论:

  • uniq 索引不需要单独的字段,我们可以将约束放在id字段上。因为id无论如何我们都应该在 Rails 应用程序中有字段,所以这是一个很好的权衡
  • 不需要对模型进行特殊验证和显式 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
于 2019-12-10T13:24:50.743 回答
1

使用has_many :contacts并不意味着您需要模型。 has_many做了一些魔术,但最后它只是添加了一些具有指定合同的方法。您没有理由不能实现这些方法(或您需要的某些子集)以使您的模型表现得像它,has_many :contacts但实际上没有为 Contact 使用 ActiveRecord 模型(或根本模型)。

于 2008-12-30T06:32:14.173 回答
1

您还可以查看 Configatron:

http://configatron.mackframework.com/

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

于 2009-01-02T01:46:32.890 回答
1
class Constant < ActiveRecord::Base
  after_initialize :readonly!

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

常量::FIELD_NAME

于 2016-11-22T14:43:24.763 回答
1

你可以这样做:

 class Config < ApplicationRecord
  def self.instance
    Config.first || Config.create!
  end
 end
于 2019-02-13T16:20:00.810 回答
0

type我假设使用具有 uniq 约束的继承列。

# 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

父类中的几种方法:

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

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

于 2019-02-14T09:46:56.720 回答