12

In my rails app I have a model with a start_date and end_date. If the user selects Jan 1, 2010 as the start_date and Jan 5, 2010 as the end_date, I want there to be 5 instances of my model created (one for each day selected). So it'll look something like

Jan 1, 2010
Jan 2, 2010
Jan 3, 2010
Jan 4, 2010
Jan 5, 2010

I know one way to handle this is to do a loop in the controller. Something like...

# ...inside controller
start_date.upto(end_date) { my_model.new(params[:my_model]) }

However, I want to keep my controller skinny, plus I want to keep the model logic outside of it. I'm guessing I need to override the "new" method in the model. What's the best way to do this?

Lan
  • 6,039
  • 7
  • 22
  • 24
  • I have a form the user fills out to create a model. But that form is just a skeleton to fill in the details of my model. The form has details like "start" and "end" points. To create a complete model it needs the start and end points filled in. I can do this in the controller but I figure this type of logic should go in the model. – Lan Dec 07 '10 at 13:54

7 Answers7

20

As @brad says, you definitely do not want to override initialize. Though you could override after_initialize, that doesn't really look like what you want here. Instead, you probably want to add a factory method to the class like @Pasta suggests. So add this to your model:

def self.build_for_range(start_date, end_date, attributes={})
  start_date.upto(end_date).map { new(attributes) }
end

And then add this to your controller:

models = MyModel.build_for_range(start_date, end_date, params[:my_model])
if models.all?(:valid?)
  models.each(&:save)
  # redirect the user somewhere ...
end
Trotter
  • 1,270
  • 11
  • 13
11

Don't override initialize It could possibly break a lot of stuff in your models. IF we knew why you needed to we could help better ( don't fully understand your explanation of the form being a skeleton, you want form attributes to create other attributes?? see below). I often use a hook as Marcel suggested. But if you want it to happen all the time, not just before you create or save an object, use the after_initialize hook.

def after_initialize
  # Gets called right after Model.new
  # Do some stuff here
end

Also if you're just looking for some default values you can provide default accessors, something like: (where some_attribute corresponds with the column name of your model attribute)

def some_attribute
  attributes[:some_attribute] || "Some Default Value"
end

or a writer

def some_attribute=(something)
  attributes[:some_attribute] = something.with_some_changes
end

If I understand your comment correctly, it looks like you expose a form that would make your model incomplete, with the other attributes based on parts of this form? In this case you can use any of the above methods after_initialize or some_attribute= to then create other attributes on your model.

brad
  • 31,987
  • 28
  • 102
  • 155
  • 1
    Check out the ActiveRecord::Base source (https://github.com/rails/rails/blob/v3.0.3/activerecord/lib/active_record/base.rb) Look at the initialize method and see what's going on, it's fetching columns from the db, assigning instance variables etc, if you override this but don't set these attributes, there's no guarantee active_record will behave properly. – brad Dec 07 '10 at 15:31
  • Note that in Rails 3.2.x, the keys in `attributes` are strings as opposed to symbols. So you would need `attributes["some_attribute"]` in the above. – steakchaser Feb 13 '14 at 01:59
  • 2
    There is no reason not to override initialize. Contrary, if you look into rails source code, you'll understand that initialize was meant to be overridden. All you need to do is to call super in your overridden initialize, but it's a rule that is expected to be followed by all ruby developers and rails developers silently expects you to do it. – Victor Nazarov Jul 17 '14 at 10:42
  • If you only want to do something for new records, you can do something like: `after_initialize :do_something, if: :new_record?` and it will only get run on new records, not when finding an existing record, etc. – Joshua Pinter Nov 08 '22 at 22:08
4

Strictly, although late, the proper way to override new in a model is

def initialize(args)
    #
    # do whatever, args are passed to super
    #
    super
end
s1mpl3
  • 1,456
  • 1
  • 10
  • 14
  • 1
    Perhaps `**args` ans `super(args)`. I wanted different model default in Rails as opposed to default in DB for legacy apps. – mlt Oct 04 '19 at 01:25
2

why don't you just create a method into your model like this

 def self.create_dates(params) 
   [...] 
  end

containing this logic (basically your loop?)

Pasta
  • 1,750
  • 2
  • 14
  • 25
  • I want it in the model because that's where the logic for creating itself should be, no? My model just needs a start and end date and then it'll be able to create "an instance" of itself. (the instance just happens to be multiple rows in the database table) – Lan Dec 07 '10 at 13:30
  • so i'd do what I said in my answer. a create_dates class method with your loop calling new inside. – Pasta Dec 07 '10 at 13:37
  • Sorry, I need to word my question better. But basically, I just want to know how to override MyModel.new(attributes) – Lan Dec 07 '10 at 13:42
  • 1
    I wouldn't do that if I were you, but maybe I'm wrong. Interested in the answer anyway! – Pasta Dec 07 '10 at 13:44
  • +1 You shouldn't have to override `new`, regardless of how you're wanting your model to behave. If it's absolutely necessary to do this in the model, I would use a combination of the above suggestion along with a `before_create` callback. The callback will require some extra failsafes however, to prevent the spanwed instances from spawning their own. – theTRON Dec 07 '10 at 14:19
  • Calling `new` should return a single un-saved instance of a model, not create and save a whole bunch of them. The best plan is to create a new method that does what you want. – tadman Dec 07 '10 at 16:25
2

I guess you want to set default values for your model attribute ?

There's another solution than overriding ; you can set callbacks :

class Model

before_create :default_values
def default_values
  ...
end
Marcel Falliere
  • 1,884
  • 21
  • 39
2

You can use:

def initialize(attributes = nil)
  # do your stuff...
end

Although somewhere I read it wasn't recommendable...

jordinl
  • 5,219
  • 1
  • 24
  • 20
  • 3
    ActiveRecord does a lot of crazy stuff and this method isn't guaranteed to be run as you'd expect. – tadman Dec 07 '10 at 16:24
2

This reeks of the factory method patttern...seek it out.

If you're reluctant for some reason to go with create_date per @Pasta, then possibly create just a simple ruby object (not ActiveRecord backed), named YourModelFactory/Template/Whatever with two instance vars - you can use your standard params[:foo] to assign these - then define and call a method on that class that returns your real objects.

Your controller logic now looks something like this:

mmf  = MyModelFactory.new(params[:foo])
objs = mmf.create_real_deal_models

Good luck.

Cory
  • 2,538
  • 2
  • 18
  • 19
  • I agree with this, now that he's explained that he actually wants to create multiple models, i think a Factory is in order. – brad Dec 07 '10 at 17:07
  • 3
    I find factories on models are better done as class methods than separate classes. So def.make_me_things over MakeMeThings.new. – Trotter Dec 07 '10 at 17:12
  • 2
    I'm totally +1 on def.make_me_things - just trying to make it feel for modelish for original poster. – Cory Dec 07 '10 at 17:57