30

In plain java I'd use:

public User(String name, String email) {
  this.name = name;
  this.email = f(email);
  this.admin = false;
}

However, I couldn't find a simple standard way to do in rails (3.2.3), with ActiveRecords.

1. override initialize

def initialize(attributes = {}, options = {})
  @name  = attributes[:name]
  @email = f(attributes[:email])
  @admin = false
end

but it might be missed when creating a record from the DB

2. using the after_initialize callback

by overriding it:

def after_initialize(attributes = {}, options = {})
  ...
end

or with the macro:

after_initialize : my_own_little_init
def my_own_little_init(attributes = {}, options = {})
  ...
end

but there may be some deprecation issues.

There are some other links in SO, but they may be out-of-date.


So, what's the correct/standard method to use?

Community
  • 1
  • 1
Asaf
  • 2,480
  • 4
  • 25
  • 33
  • 4
    You can already do this without needing any custom code: `User.new(:name => 'Bon', :email => 'bob@example.com')`. Are you looking to use it in a different way? – Dylan Markow May 08 '12 at 19:33
  • you're correct. I guess I'm asking about default values, not init values which are passed on creation – Asaf May 08 '12 at 19:45
  • 1
    or doing some manipulation on the given input, while constructing – Asaf May 08 '12 at 19:47

5 Answers5

22

Your default values should be defined in your Schema when they will apply to ALL records. So

def change
  creates_table :posts do |t|
    t.boolean :published, default: false
    t.string :title
    t.text :content
    t.references :author
    t.timestamps
  end
end

Here, every new Post will have false for published. If you want default values at the object level, it's best to use Factory style implementations:

User.build_admin(params)

def self.build_admin(params)
  user = User.new(params)
  user.admin = true
  user
end
Jesse Wolgamott
  • 40,197
  • 4
  • 83
  • 109
  • thanks. So basically, if I want to have an e.g. normalized emails in all my user instances (upon creation), I should use a `def self.build_normalized(params)` factory method? that passes the work of finding the right creation method to the client of my code. And it doesn't cover all execution paths. Also, Is there a factory utility to use in rails? or a standard pattern? – Asaf May 08 '12 at 20:24
  • And... if we put default values in the schema definition and any other init logic in the object itself, isn't that spreading one concern across different places in the code (and maybe levels of abstractions)? – Asaf May 08 '12 at 20:30
  • @Asaf -- I would use either db init values or a factory pattern, not both. – Jesse Wolgamott May 08 '12 at 21:23
16

According to Rails Guides the best way to do this is with the after_initialize. Because with the initialize we have to declare the super, so it is best to use the callback.

Mauro George
  • 199
  • 1
  • 2
  • 6
  • 2
    Link is down, is `after_create` the new `after_intitialize`? – James McMahon Dec 11 '13 at 18:36
  • @JamesMcMahon no, we still have the `after_intitialize`. I updated the link of documentation. – Mauro George Jan 12 '14 at 11:31
  • 1
    In recent years I have avoided AR callbacks almost entirely. For me they make testing harder and my models more complex. I want my models to be really stupid. – bennick Dec 31 '15 at 15:19
  • `after_initialize` runs before creation and will also run when instantiating records, for example, after finding a record with `Thing.find( 1 )`. If you want it to only run for new records, you can do something like this: `after_initialize :do_something, if: :new_record?`. – Joshua Pinter Nov 08 '22 at 22:14
5

One solution that I like is via scopes:

class User ...
   scope :admins, where(admin: true)

Then you can do both: create new User in the admin status(i.e. with admin==true) via User.admins.new(...) and also fetch all your admins in the same way User.admins.

You can make few scopes and use few of them as templates for creating/searching. Also you can use default_scope with the same meaning, but without a name as it is applied by default.

jdoe
  • 15,665
  • 2
  • 46
  • 48
  • are there any issues with using `default_scope` for that? what about other param manipulation in the constructor? – Asaf May 08 '12 at 19:53
  • 1
    It's useful if you're dealing with, say, Comment model and want to save erased comments for FBI :) Just set default_scope like `where(deleted: false)` and you'll never encounter deleted answers unless you directly override your scope via, say, `Comment.where(deleted: true)` or via `unscoped` method: `Comment.unscoped`. There are other ways, but I like this one :) – jdoe May 08 '12 at 19:59
  • I'm a bit familiar with scope, but for finding, not creating. however, i'm looking for a way to properly initialize my objects, not "partition" them into scopes... – Asaf May 08 '12 at 20:10
  • default_scopes also extremely useful for specifying related records that have to be loaded alongside with current model. It's called eager loading if you aren't familiar. If you find yourself loading related models over and over and cycle through them than you can significantly reduce DB-hits this way: `default_scope include: :tags`. But that's another story. :) – jdoe May 08 '12 at 20:16
  • 10x... I've fought Hibernate's annotations long enough to look for the equivalent methods right away :) – Asaf May 08 '12 at 20:27
4

I was searching for something similar this morning. While setting a default value in the database will obviously work, it seems to break with Rails' convention of having data integrity (and therefore default values?) handled by the application.

I stumbled across this post. As you might not want to save the record to the database immediately, I think the best way is to overwrite the initialize method with a call to write_attribute().

def initialize
  super
  write_attribute(name, "John Doe")
  write_attribute(email,  f(email))
  write_attribute(admin, false)
end
clekstro
  • 593
  • 4
  • 10
  • 2
    for some reason this causes problems if you try to do something like Model.new(param1: arg1, param2: arg2), you'll get "ArgumentError: wrong number of arguments (1 for 0)" errors, looks like Mauro's answer is a better is a better one if you want to dynamically write attributes (randomized keys for example) – concept47 Apr 13 '14 at 22:58
  • I agree. You should probably be using after_initialize instead. – clekstro Apr 16 '14 at 11:43
  • FYI Actually, after_create worked better for what I needed, when I used after_initialize, every time I did a .find on the Model with the randomized attribute, the value would change, which I didn't want – concept47 Apr 16 '14 at 16:15
  • It doesn't work because the method signature is wrong, it just needs to declare an optional param. – Eduardo Jun 22 '16 at 07:19
  • I added an example of the optional params in the next answer suggestion. – Harry Fairbanks Aug 08 '16 at 00:43
4

This will work in rails 4.

def initialize(params)
    super
    params[:name] = params[:name] + "xyz" 
    write_attribute(:name, params[:name]) 
    write_attribute(:some_other_field, "stuff")
    write_attribute(:email, params[:email])
    write_attribute(:admin, false)
end
Harry Fairbanks
  • 408
  • 1
  • 4
  • 13