Monday, September 5, 2011

Difference between build_model and create_model in Rails

I keep a note pad with features I want to add, things I need to fix and other reminders to myself whenever I'm coding an application. I've got a few methods defined in my models that will calculate totals. Needless to say, these methods aren't going to do anything but complain if the data fields they use to calculate a total either don't exist or aren't integers. What to do?

Validation to the rescue! But I had a problem. A minor problem. The models I speak of all belong to a main Character model. Take my Initiative model for example. It belongs_to Character, and Character has_one Initiative. I have a few fields in Initiative: :dex, :misc, and :speed. These refer to a Character's Dexterity modifier, any miscellaneous modifiers and how many feet that Character can move in a round. So I have this method defined in Initiative.rb:

def total
  dex + misc
end


This way, I can simply call Initiative.total in a view and Rails will calculate the total for me. Cool, right? And this is where validation comes in handy. However, here's the issue I had. My validation worked, but it was showing an error BEFORE any data had been entered. Definitely better than not working, or allowing a user to enter a B instead of an 8, but I'd rather not show an error message if no error has been made. So what was the solution?

First, the validation I wrote, in my Initiative model.

validates_presence_of :dex, :misc, :speed
validates_numericality_of :dex, :misc, :speed

This all looks good. I suspected the error was in the way my new method was written in Intiatives controller, but I couldn't figure it out, so I asked for help on StackOverflow. User m_x was kind enough to point me in the right direction.

def new
  @character = Character.find(params[:character_id])
  @initiative = @character.create_initiative(params[:initiative])
end


This bit of code was the culprit. Easy enough to fix, though.

def new
  @character = Character.find(params[:character_id])
  @initiative = @character.build_initiative(params[:initiative])
end


It turns out the reason my validation was being called before a user had entered any data is because the create method creates a new object and attempts to save it. In this case, it wasn't saving because it failed the validation, because nothing had been entered yet. On the other hand, the build method creates a new object, but does NOT save it. Easy enough to remember! As an added bonus, this gives me some ideas on how to tackle another problem I've been thinking about.

This mistake feels like something I should have known before hand, considering how much time I've spent reading through the Rails API, but I have a hard time holding on to information until it becomes practical. But now that I've used this, and cemented exactly what the difference is between build_model and create_model, I won't make the same mistake again.

One less thing, right?

4 comments:

  1. I don't know that your validations will actually protect you from certain patterns of input. validates_presence_of ensures that a field isn't blank (nil or empty string), and validates_numericality_of either uses Kernel.float or a regex (depending on whether the option only_integer is true or false). The string '2' passes both of these. Say this is entered for dex and misc, you'll get '2' + '2' which does string concatenation and == '22'.

    If initiative modifiers are always integers, you might just use validates_numericality_of :dex, :misc, :speed and use dex.to_i + misc.to_i for your total method. Either that or cast them to ints before persisting.

    ReplyDelete
  2. @Matthew I like your solution of changing the total method to dex.to_i + misc.to_i but won't I still need to validate_presence_of to perform the total method? If nothing is entered, it reads it as nil and would throw an error when attempting to calculate, right?

    ReplyDelete
  3. Nayeth. Check out the regex for validates_numericality_of with :only_integer => true:

    /\A[+\-]?\d+\Z/

    So, optional sign and at least one digit. This will match '+1', '-1', or '1' but fail for nil or empty string (or any string containing non-numeric elements). So, I don't believe you get any additional value from a presence check (other than perhaps a more specific error message for total omission). You might additionally supply range requirements so that it makes sense in terms of DnD mechanics.

    As far as throwing an error when attempting to calculate the total, nil.to_i == 0. Similarly, nil.to_s == empty string, and nil.to_a == []. In general, typecasting is useful for ensuring nil values won't be problematic when performing calculations.

    ReplyDelete
  4. Just wanted to say I'm going back through my models and altering validation as you suggest. I agree that it doesn't really make sense to validate for presence_of, when validating for numericality_of does the trick just fine.

    It's also nice to know that nil.to_i == 0, I was actually wondering if there WAS a way to set a nil entry equal to zero a while back.

    As far as range requirements, I've thought about that, but I'm not entirely sure how to limit it yet. It makes sense to apply these to stats as well, but epic level characters are a consideration. Still, capping most individual numerical entries at 30 or so makes sense, and epic level characters aren't going to be the norm, if this program WERE to be used. But I'll put that on my checklist!

    ReplyDelete