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?

3
  • 4
    You can already do this without needing any custom code: User.new(:name => 'Bon', :email => '[email protected]'). Are you looking to use it in a different way? Commented May 8, 2012 at 19:33
  • you're correct. I guess I'm asking about default values, not init values which are passed on creation Commented May 8, 2012 at 19:45
  • 1
    or doing some manipulation on the given input, while constructing Commented May 8, 2012 at 19:47

5 Answers 5

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
Sign up to request clarification or add additional context in comments.

3 Comments

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?
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 -- I would use either db init values or a factory pattern, not both.
17

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.

4 Comments

Link is down, is after_create the new after_intitialize?
@JamesMcMahon no, we still have the after_intitialize. I updated the link of documentation.
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.
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?.
4

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.

5 Comments

are there any issues with using default_scope for that? what about other param manipulation in the constructor?
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 :)
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...
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. :)
10x... I've fought Hibernate's annotations long enough to look for the equivalent methods right away :)
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

5 Comments

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)
I agree. You should probably be using after_initialize instead.
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
It doesn't work because the method signature is wrong, it just needs to declare an optional param.
I added an example of the optional params in the next answer suggestion.
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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.