Rails:如何在Rails activerecord的模型中为属性创建默认值?

206

我希望通过在ActiveRecord中定义默认值来创建属性的默认值。每次创建记录时,默认情况下,我希望为属性:status设置一个默认值。我尝试过这样做:

class Task < ActiveRecord::Base
  def status=(status)
    status = 'P'
    write_attribute(:status, status)
  end
end

但是在创建过程中,我仍然从数据库检索到这个错误:

ActiveRecord::StatementInvalid: Mysql::Error: Column 'status' cannot be null
因此,我认为该值未应用于属性。
在Rails中如何优雅地处理这个问题?
非常感谢。

1
更完整和最新的答案可在https://dev59.com/mHRC5IYBdhLWcg3wVvjL找到。 - Joshua Coady
10个回答

307

你可以在迁移中为列设置默认选项。

....
add_column :status, :string, :default => "P"
....

或者

你可以使用回调函数 before_save

class Task < ActiveRecord::Base
  before_save :default_values
  def default_values
    self.status ||= 'P' # note self.status = 'P' if self.status.nil? might better for boolean fields (per @frontendbeauty)
  end
end

19
通常我们会写成 self.status ||= 'P'。此外,如果该字段正在被验证,请考虑使用 before_validation 回调函数。 - tokland
45
如果您正在尝试为视图设置默认值(例如在创建新记录时),这并不能帮助您。更好的选择是使用 after_initialize。 - insane.dreamer
14
请注意,如果您使用before_create,并且函数的最后一行类似于“self.hates_unicorns ||= false”,则会返回false,模型将不会保存。 :) - James
71
如果你试图设置一个布尔字段的默认值,要小心使用||=运算符。更安全的方法是使用这个表达式:self.status = 'P' if self.status.nil? - frontendbeauty
7
如果你想让true成为默认值, self.booleanfield ||= true 将始终将其设置为true,不仅在它未设置时,而且如果它已经被设置为false(因为falsenil都是假值)。即x ||= true等同于 x = true。你可以看到这可能会导致问题。这只会发生在布尔值中,因为Ruby中的其他数据类型没有假值。 - Amadan
显示剩余8条评论

197

因为我刚刚遇到了这个问题,而且Rails 3.0的选项有点不同,所以我将提供另一个答案。

在Rails 3.0中,你需要这样做:

class MyModel < ActiveRecord::Base
  after_initialize :default_values

  private
    def default_values
      self.name ||= "default value"
    end
end

124
注意:'after_initialize' 的意思是在 Ruby 的 initialize 方法之后执行。因此,它会在每次从数据库中加载记录并用于创建一个新的模型对象时运行,所以如果你想要的只是在添加新记录时设置默认值,请不要使用此回调函数。如果您想这样做,请使用 before_create 而不是 before_save。before_create 在创建新的 db 记录之前和第一次初始化之后运行。before_save 每次更新 db 记录时都会被调用。 - Mandrake Button
8
使用 before_create 而不是 before_save 的问题在于 before_save 先执行。因此,如果你想要做一些不仅仅是设置默认值的事情,在创建和更新时从其他属性计算一个值,这可能会导致问题,因为默认值可能没有设置。最好使用 ||= 运算符并使用 before_save。 - Altonymous
2
检查一下它是否已经被“持久化”,如果没有,再进行设置怎么样? - dleavitt
3
我还没有看到过在after_initialize中使用的代码无法轻易地移动到before_validation、before_save等位置的情况。很有可能你团队中的某个开发人员会执行像MyModel.all.each这样的操作,也许是为了进行某种批量处理,从而运行此初始化逻辑MyModel.count次数。 - Luke W
3
@Altonymous: 很好的观点。我想知道将“defaulting”块套上new_record?条件是否也有帮助?(链接) - twelve17
显示剩余3条评论

103

通常我需要默认值是在新记录创建之前,当渲染新视图之前。以下方法将仅为新记录设置默认值,以便在呈现表单时可用。before_savebefore_create 太晚了,如果想要在输入字段中显示默认值,则无法使用它们。

after_initialize do
  if self.new_record?
    # values will be available for new record forms.
    self.status = 'P'
    self.featured = true
  end
end

1
谢谢。这就是我正在寻找的,我的输入字段将填充默认值。 - wndxlori
13
做得好,这通常也是我做的事情。不过你应该只在值为空时设置它们。否则当它们被传递到create方法中时,你将会覆盖这些值,比如 a = Model.create(status:'A', featured:false) - asgeo1
2
asgeo1 是正确的。在设置之前最好检查它是否为 nil。使用 self.status ||= 'P' 或者 self.status = 'P' if self.status.nil? - Tyler

78

您甚至无需编写任何代码 :) 您只需要在数据库中为该列设置默认值。您可以在迁移中完成此操作。例如:

create_table :projects do |t|
  t.string :status, :null => false, :default => 'P'
  ...
  t.timestamps
end

5
为了保留其中的信息,此解决方案需要进行数据库转储。 - EmFi
7
注意,MySQL 不允许在 TEXT/BLOB 列上设置默认值。否则,这就是理想的解决方案。 - Andrew Vit
哇,指南中竟然没有一次提到:default!http://guides.rubyonrails.org/migrations.html 不幸的是,我已经运行了我的迁移,所以正在寻找在模型中设置默认值的方法。 - Chloe
1
如果您希望默认值是配置值,并且可能在迁移后随时进行修改,则此方法会失败。 - Ciro Santilli OurBigBook.com
如果您需要在调用“new”方法之后将其作为默认值保存或尝试保存,则此方法非常有用。此外,您始终可以运行新的迁移来更改列以添加默认值。 - John Owen Chile
显示剩余2条评论

22

解决方案取决于几个因素。

默认值是否依赖于创建时可用的其他信息?您能否在最小后果下清除数据库?

如果您回答第一个问题是,那么您想使用Jim的解决方案。

如果您回答第二个问题是,那么您想使用Daniel的解决方案。

如果您对两个问题的回答都是否定的,您可能最好添加并运行新的迁移。

class AddDefaultMigration < ActiveRecord::Migration
  def self.up
     change_column :tasks, :status, :string, :default => default_value, :null => false
  end
end

:string 可以替换为 ActiveRecord::Migration 所识别的任何类型。

CPU是廉价的,所以Jim方案中对Task的重新定义不会引起太多问题,特别是在生产环境中。这个迁移是正确的方式,因为它被加载和调用的频率要少得多。


我刚刚使用了迁移技术,惊讶地发现默认值已经被应用到了所有现有数据中。这是在Rails v3.0.1 sqlite开发模式下实现的。 - SooDesuNe
大多数数据库引擎不会这样做。您可以设置默认值,但仍然可以有空值。我不确定是Rails还是sqlite假定所有空行都应具有默认值,当应用非空约束时。但我知道其他数据库引擎将在包含空值的列上应用非空约束时出现问题。 - EmFi

13

我建议使用在这里找到的attr_defaults。你的梦想将成真。


1
尽管这本质上是@BeepDog答案的包装器:https://github.com/bsm/attribute-defaults/blob/master/lib/attribute_defaults.rb,但这值得注意。 - user456584

11

加强 Jim 的回答

使用presence,可以这样做

class Task < ActiveRecord::Base
  before_save :default_values
  def default_values
    self.status = status.presence || 'P'
  end
end

5
对于 Rails 提供的列类型 - 像这个问题中的字符串 - 最好的方法是根据 Daniel Kristensen 的建议在数据库本身中设置列默认值。Rails 将检查数据库并相应地初始化对象。此外,这使得您的数据库免受某人在您的 Rails 应用程序之外添加行并忘记初始化该列的影响。
对于 Rails 不支持的列类型 - 例如 ENUM 列 - Rails 将无法自省列默认值。对于这些情况,您不想使用 after_initialize(它每次从 DB 中加载对象时都会被调用,以及每次使用 .new 创建对象时都会被调用),before_create(因为它出现在验证之后)或 before_save(因为它也会在更新时发生,这通常不是您想要的)。
相反,您需要在 before_validation on: create 中设置属性,如下所示:
before_validation :set_status_because_rails_cannot, on: :create

def set_status_because_rails_cannot
  self.status ||= 'P'
end

5

在我看来,需要解决两个问题才能设置默认值:

  1. 当一个新对象被初始化时,需要存在这个值。使用after_initialize方法不太合适,因为它会在调用#find方法时被调用,从而导致性能下降。
  2. 需要在保存时保留默认值。

以下是我的解决方案:

# the reader providers a default if nil
# but this wont work when saved
def status
  read_attribute(:status) || "P"
end

# so, define a before_validation callback
before_validation :set_defaults
protected
def set_defaults
  # if a non-default status has been assigned, it will remain
  # if no value has been assigned, the reader will return the default and assign it
  # this keeps the default logic DRY
  status = status
end

我很想知道人们为什么会想到这种方法。


如果设置为跳过验证,它不会被执行吗? - Andre Figueiredo

-3

我现在找到了更好的方法:

def status=(value) 
  self[:status] = 'P' 
end 

在 Ruby 中,方法调用允许不使用括号,因此我应该将本地变量命名为其他名称,否则 Ruby 将把它识别为方法调用。

1
您的问题应该更改以适应这个被接受的答案。在您的问题中,您想为属性设置默认初始值。而在答案中,您所写的是,您实际上正在处理该属性的输入值。 - Jim
3
如果“status”值被设置,这不会设置默认值,而是将值设置为“P”。此外,你应该真正使用“write_attribute :status,'P'”代替。 - radiospiel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接