206

I want to create a default value for an attribute by defining it in ActiveRecord. By default everytime the record is created, I want to have a default value for attribute :status. I tried to do this:

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

But upon creation I still retrieve this error from the database:

ActiveRecord::StatementInvalid: Mysql::Error: Column 'status' cannot be null

Therefore I presume the value was not applied to the attribute.

What would be the elegant way to do this in Rails?

Many thanks.

Promise Preston
  • 24,334
  • 12
  • 145
  • 143
Joshua Partogi
  • 16,167
  • 14
  • 53
  • 75
  • 1
    A more complete and up to date answer is available at http://stackoverflow.com/questions/328525/how-can-i-set-default-values-in-activerecord – Josh Coady Oct 19 '13 at 00:01

10 Answers10

307

You can set a default option for the column in the migration

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

OR

You can use a callback, 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
BenKoshy
  • 33,477
  • 14
  • 111
  • 80
Jim
  • 5,557
  • 1
  • 20
  • 18
  • Radar, you're right. I'll fix it. – Jim Oct 11 '09 at 22:31
  • 19
    Normally we'd write self.status ||= 'P'. Also, if the field is being validated, consider using the before_validation callback. – tokland Jul 15 '10 at 10:13
  • 45
    this won't help you if you're trying to set a default value for the view (i.e., when creating a new record). A better option is after_initialize. – insane.dreamer Aug 16 '10 at 22:39
  • 1
    Be careful with `before_save`. Use `before_validates` where appropriate. Read [more about it](http://siddharthdawara.blogspot.com/2008/09/rails-beforesave-and-validations.html). – m33lky Dec 26 '11 at 03:46
  • 1
    I would use a before_create if you just want a default value the first time the model is stored in your DB. – Goles Feb 16 '12 at 22:44
  • 14
    Note that if you use before_create and the last line of the function is something like `self.hates_unicorns ||= false`, false is returned and the model won't save. :) – James Mar 19 '12 at 21:46
  • 71
    Be careful about the `||=`, in case you're trying to set the default value of a Boolean field. `self.status = 'P' if self.status.nil?` is safer. – frontendbeauty Jun 08 '12 at 00:46
  • @frontendbeauty Why safer? What's the danger? – hrdwdmrbl Jun 07 '13 at 19:37
  • 7
    @jackquack: If you want `true` to be the default, `self.booleanfield ||= true` will *always* set it to `true`, not only when it is unset, but also if it is already set to `false` (because `false` and `nil` are both as falsy). I.e. `x ||= true` is equivalent to `x = true`. You can see how that might pose a problem. It only happens with booleans because no other datatype in Ruby has a falsy value. – Amadan Jun 10 '13 at 00:42
  • this approach is insufficient if you need to work on the model before its been saved and require the default value to be set on the reader method. see my approach below to handle all use cases. http://stackoverflow.com/a/20008629/1490262 – Peter P. Nov 15 '13 at 19:05
  • At migration level is way better, since the default value is handled at db level, and Rails just consumes it, w/out validation callbacks (assuming default value is **always** a valid value) – unmultimedio Jan 11 '17 at 22:59
  • hi folks thank you - what is to be preferred: using the database to default the values, or the model? im thinking that this thing could become extremely messy – BenKoshy Jan 20 '17 at 00:26
  • If you want to set default values for new records and still want to validate, `before_validation :set_defaults, if: :new_record?` runs the method `set_defaults` before the validations and only if it's a new record. `before_create` and `before_save` won't do this for you. `after_initialize` may work like this too. – Mateus Pires Dec 13 '19 at 14:05
197

Because I encountered this issue just a little while ago, and the options for Rails 3.0 are a bit different, I'll provide another answer to this question.

In Rails 3.0 you want to do something like this:

class MyModel < ActiveRecord::Base
  after_initialize :default_values

  private
    def default_values
      self.name ||= "default value"
    end
end
BeepDog
  • 5,016
  • 2
  • 24
  • 33
  • 124
    A word of caution; 'after_initialize' means after the Ruby initialize. Hence it is run every time a record is loaded from the database and used to create a new model object in memory, so don't use this callback if what you want is just to set default values the first time you add a new record. If you want to do that, use before_create and not before_save; before_create is run before creating the new db record and after the first initialize. before_save is called every time there is any type of update to a db record. – Mandrake Button Jan 11 '12 at 08:02
  • 8
    The problem with using before_create instead of before_save is that before_save executed first. So when you want to do something other than set defaults, say calculate a value from other attributes on both a creation and update it will cause issues because the defaults might not be set. It's best to use the ||= operator and use before_save – Altonymous Mar 09 '12 at 05:36
  • 2
    how about checking if it's `persisted?` and only setting it if not? – dleavitt Apr 20 '12 at 03:32
  • 3
    I have yet to see a case where code used in after_initialize couldn't easily be moved to before_validation, before_save, etc. Chances are that eventually one of the devs on your team will execute something like MyModel.all.each perhaps for bulk processing of some kind, and thus run this initialization logic MyModel.count num of times. – Luke W Nov 21 '12 at 17:55
  • 3
    @Altonymous: good point. I wonder if would also help to wrap the 'defaulting' block with a `new_record?` condition? ([link](http://apidock.com/rails/ActiveRecord/Base/new_record%3F)) – twelve17 Apr 30 '13 at 15:38
  • @lukewendling What about the case where you want to access these default values before saving the record for the first time? (E.g. In a form.) – Ajedi32 Nov 07 '14 at 17:11
  • @Ajedi32 for simply displaying default values in a form, perhaps you might consider a UI model (a.k.a. a presenter) that manages default values, instead of tying lookup values to a db model. Remembering that your model *should* only be dealing with serializing the data, not presenting default values, formatting, etc. Hope that helps...Since I wrote my initial reply 2 years ago, it seems the community has moved even more towards the presenter architecture... which as it turns out is a nice alternative to after_initialize. – Luke W Nov 07 '14 at 22:51
  • What happened if the user set the name as nil ? – Yana Agun Siswanto Sep 19 '16 at 06:20
103

When I need default values its usually for new records before the new action's view is rendered. The following method will set the default values for only new records so that they are available when rendering forms. before_save and before_create are too late and will not work if you want default values to show up in input fields.

after_initialize do
  if self.new_record?
    # values will be available for new record forms.
    self.status = 'P'
    self.featured = true
  end
end
Brad The App Guy
  • 16,255
  • 2
  • 41
  • 60
Tim Santeford
  • 27,385
  • 16
  • 74
  • 101
  • 1
    Thanks. That's the one I was looking for, where my input fields will be filled with the defaults. – wndxlori Dec 11 '12 at 22:11
  • 13
    Good work, this is what I generally do as well. Except you should only set the values if they are nil. Otherwise you will be overwriting the values when they are passed into the create method, such as in `a = Model.create(status:'A', featured:false)` – asgeo1 Oct 28 '13 at 04:20
  • 2
    asgeo1 is correct. It would be better to check if it's nil before setting. Use `self.status ||= 'P'` or `self.status = 'P' if self.status.nil?` – Tyler Nov 01 '18 at 22:43
78

You can do it without writing any code at all :) You just need to set the default value for the column in the database. You can do this in your migrations. For example:

create_table :projects do |t|
  t.string :status, :null => false, :default => 'P'
  ...
  t.timestamps
end
starball
  • 20,030
  • 7
  • 43
  • 238
Daniel Kristensen
  • 1,364
  • 8
  • 6
  • 5
    This solution requires a database dump to preserve information in it. – EmFi Oct 11 '09 at 20:44
  • 7
    Note, MySQL doesn't allow default values on TEXT/BLOB columns. Otherwise this is the ideal solution – Andrew Vit Mar 20 '13 at 18:35
  • Wow not once is `:default` mentioned in the guide! http://guides.rubyonrails.org/migrations.html Unfortunately, I already ran my migration so looking for a way to get a default in the model. – Chloe Dec 31 '13 at 08:29
  • 1
    This fails is you want the default to be a configuration value, possibly modified anytime after the migration. – Ciro Santilli OurBigBook.com Jan 14 '14 at 19:53
  • This is great if you need it as a default value after calling "new" method, before saving it or trying to save it. Also, you always can run a new migration that change the column to add the default value. – John Owen Chile Jan 31 '14 at 22:29
  • @Chloe create another migration and use `change_column` or `change_table` – PhilT Mar 07 '15 at 10:49
  • Even with this, when specifically set a column's value to `null`, the database will bork at you. It's stupid and doesn't happen often, but we've found that this sometimes is not enough. – Joshua Pinter Nov 30 '18 at 00:18
22

The solution depends on a few things.

Is the default value dependent on other information available at creation time? Can you wipe the database with minimal consequences?

If you answered the first question yes, then you want to use Jim's solution

If you answered the second question yes, then you want to use Daniel's solution

If you answered no to both questions, you're probably better off adding and running a new migration.

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

:string can be replaced with any type that ActiveRecord::Migration recognizes.

CPU is cheap so the redefinition of Task in Jim's solution isn't going to cause many problems. Especially in a production environment. This migration is proper way of doing it as it is loaded it and called much less often.

EmFi
  • 23,435
  • 3
  • 57
  • 68
  • I just used the migration technique and I was surprised to find the the default value had been back-applied to all my existing data. This Rails v3.0.1 sqlite in development mode. – SooDesuNe Jan 16 '11 at 23:45
  • Most DB engines don't do things that way. You can have a default value, and still have null values. I'm not sure if it's Rails or sqlite that assumes all null rows should have the default value when the not null constraint is applied. But I do know that other db engines will choke if you apply the not null constraint on a column containing null values. – EmFi Jan 17 '11 at 13:03
13

I would consider using the attr_defaults found here. Your wildest dreams will come true.

PeppyHeppy
  • 1,345
  • 12
  • 20
  • 1
    Though it's notable that this is essentially a wrapper around @BeepDog's answer: https://github.com/bsm/attribute-defaults/blob/master/lib/attribute_defaults.rb – user456584 Sep 20 '13 at 17:59
11

Just strengthening Jim's answer

Using presence one can do

class Task < ActiveRecord::Base
  before_save :default_values
  def default_values
    self.status = status.presence || 'P'
  end
end
Community
  • 1
  • 1
swapab
  • 2,402
  • 1
  • 27
  • 44
5

For column types Rails supports out of the box - like the string in this question - the best approach is to set the column default in the database itself as Daniel Kristensen indicates. Rails will introspect on the DB and initialize the object accordingly. Plus, that makes your DB safe from somebody adding a row outside of your Rails app and forgetting to initialize that column.

For column types Rails doesn't support out of the box - e.g. ENUM columns - Rails won't be able to introspect the column default. For these cases you do not want to use after_initialize (it is called every time an object is loaded from the DB as well as every time an object is created using .new), before_create (because it occurs after validation), or before_save (because it occurs upon update too, which is usually not what you want).

Rather, you want to set the attribute in a before_validation on: create, like so:

before_validation :set_status_because_rails_cannot, on: :create

def set_status_because_rails_cannot
  self.status ||= 'P'
end
aec
  • 833
  • 1
  • 9
  • 18
5

As I see it, there are two problems that need addressing when needing a default value.

  1. You need the value present when a new object is initialized. Using after_initialize is not suitable because, as stated, it will be called during calls to #find which will lead to a performance hit.
  2. You need to persist the default value when saved

Here is my solution:

# 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

I'd love to know why people think of this approach.

Peter P.
  • 3,221
  • 2
  • 25
  • 31
-3

I found a better way to do it now:

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

In Ruby a method call is allowed to have no parentheses, therefore I should name the local variable into something else, otherwise Ruby will recognize it as a method call.

Joshua Partogi
  • 16,167
  • 14
  • 53
  • 75
  • 1
    Your question should be changed to fit this accepted answer. In your question you wanted to default an initial value for an attribute. In the answer you wrote, you are really processing an inputted value for that attribute. – Jim Oct 13 '09 at 23:28
  • 3
    This does not set a default value, it sets a value to "P" if at any point the "status" value is set. Besides, you should really use "write_attribute :status, 'P'" instead – radiospiel Nov 28 '11 at 17:25