Attribute Values in Rails

Setting attribute values in Rails is harder than I'd expect it to be. With Core Data/Cocoa, I'd do something like this pseudocode-ish code:

- (void) scrubAttributes
{
   id attributes = [[self entityDescription] attributesByName];
   id key, value, e = [attributes keyEnumerator];
  
   while (key = [e nextObject]) {
      value = [NSMutableString stringWithString:[self valueForKey: key]];
      [value stripWhitespace];
      [value collapseWhitespace];
      [self setValue: value forKey: key];
   }
}


This is "the way" to do it in Cocoa. More verbose than Ruby, but it's clear what's going on. In a Rails app, accessing attribute values takes a few different forms. Let's assume there's an ActiveRecord subclass with "first_name" and "last_name" as attributes. I could get to the attribute values any of the following ways:

puts @first_name, @last_name

puts self.first_name, self.last_name

first = "first_name"
last  = "last_name
puts self.attributes[first], self.attributes[last]


As far as I can tell, self.key and self.attributes[key] do the same thing, but @key only grabs whatever is in memory -- it won't trigger any sort of faulting. (Don't take this as truth, it's just what I can gather from what I've read.) This is where the principle of least surprise seems to be absent a bit. These syntactical differences can cause a lot of confusion.

Now, what if I want to process a group of attributes at once? I thought since Ruby is OO, this might work:

def scrub_attributes
  self.attributes.each do |key,value|
    if value.is_a?(String)
      value.strip!
      value.squeeze!(" ")
    end
  end
end


Nope. No errors, but the changes don't get written to the parent. I guess these things that are passed into the block are copies? Next, I tried this:

def scrub_attributes
  self.attributes.each do |key,value|
    if value.is_a?(String)
      self.attributes[key].strip!
      self.attributes[key].squeeze!(" ")
    end
  end
end


Nope, same deal as last time. Then I though for sure this would work:

def scrub_attributes
  self.attributes.each do |key,value|
    if value.is_a?(String)
      value.strip!
      value.squeeze!(" ")
      self.attributes[key] = value
    end
  end
end


But once again, nothing happened. This was really surprising and I wasted a lot of time trying to figure it out. Finally, I got it to work like this:

def scrub_attributes
  values = {}
  self.attributes.each do |key,value|
    if value.is_a?(String)
      value.strip!
      value.squeeze!(" ")
      values[key] = value
    end
  end
  self.attributes = values
end


So technically it works, but it doesn't feel as elegant as other Ruby code. I found a slightly more compact solution:

def scrub_attributes
  self.attributes.each do |key,value|
    if value.is_a?(String)
      value.strip!
      value.squeeze!(" ")
      write_attribute key, value
    end
  end
end


So that's what I ended up with, but I'd still really like to know what I can't write to the self.attributes hash. In an effort to learn more about the language, is there an even-more-elegant-yet-still version of the above?

Update

I made this a bit easier by adding code to the String class. I created file called stringextensions.rb in the application's "lib" folder and added this:

class String
  def minimize_whitespace!
    self.strip!
    self.squeeze!(" ")
  end
end


Adding code to an existing class is just that easy. Now i can use it like this:

require "lib/stringextensions"

def scrub_attributes
  self.attributes.each do |key,value|
    if value.is_a?(String)      
      value.minimize_whitespace!
      write_attribute key, value
    end
  end
end


This is pretty good, but I still wonder if I can eliminate that if qualifier on the end by adding the method to the Ruby base class. I need to find out if that will work for nil objects, though.

Speaking of which, I was surprised that sending messages to nil generates exceptions in Ruby, at least in Rails development mode. I  think ignoring nil messages saves a lot of time in Objective-C.
Design Element
Attribute Values in Rails
Posted Oct 31, 2005 — 11 comments below




 

Mr eel — Oct 31, 05 484

"Speaking of which, I was surprised that sending messages to nil generates exceptions in Ruby, at least in Rails development mode. I think ignoring nil messages saves a lot of time in Objective-C."

Well, if you're calling an accessor of an ActiveRecord instance -- to say, write it's value into an input -- you might be a bit surprised if no errors get generated and yet the input doesn't have a value in it (if you were expecting one anyhow).

Having those error messages has helped me out a few times.

In what circumstances would you be sending messages to nil often enough to want errors supressed? I'm not familiar with Obj-C so that seems... intriguing :)

Steve — Oct 31, 05 486

We're supposed to not send messages to nil anymore though. Apparently Apple can optimize msgSend if there are "NO_NIL_RECEIVERS" or whatever. And on Intel, messages to nil are going to give different results. But is is a useful thing, I agree.

Scott Stevenson — Oct 31, 05 489 Scotty the Leopard

Mr eel says: you might be a bit surprised if no errors get generated and yet the input doesn't have a value in it

I think we're talking about different things. I agree setting a nil value on an attribute should raise a validation error. In Objective-C, though, you can send messages to nil without generating errors. In Ruby terms, that means you could do nil.strip!, and nil would just ignore it.

This is really helpful during development because you don't have to constantly check the nil-ness of every single object and you can stub out methods that don't actually have working objects in them.

Steve says: We're supposed to not send messages to nil anymore though. Apparently Apple can optimize msgSend if there are "NO_NIL_RECEIVERS" or whatever.

Hmmm, I vaguely remember this, but I'll have to look at it again. I'm not sure it's worth the loss in productivity and safety.

And on Intel, messages to nil are going to give different results.

True, though I think this is a secondary issue. My main interest is making sure the app doesn't just blow up when you talk to nil.

Jo — Nov 01, 05 492

You may first put "def scrub_attributes...end" within "class Hash; ...; end" (together with a modified "self.each do |key, value|..."). Then you can use "puts anykindofhash.scrub_attributes".

A way to make a hash an editable variable within a class is to use "def initialize":

class HashOfNames;
def hashofnames; @hashofnames; end;
def initialize; @hashofnames = {"firstname" => " lastname"}; end;
end;

edit = HashOfNames.new;
p a = edit.hashofnames;
p a.scrub_attributes;
p a

(You may also use @@hashofnames in class HashOfNames)

Jo — Nov 01, 05 495

You can modify the example above and add an input value to def initialize:

def initialize(value); @hashofnames = value; end
def putshash; puts @hashofnames; end

Now you can read in a new hash into class HashOfNames:

newhash = {...}
putin = HashOfNames.new(newhash.scrub_attributes)
puts putin.hashofnames
puts putin.putshash

Yet another option would be to use "...; value.scrub_attributes; ..." in def initialize(value) directly.

Blake Watters — Nov 07, 05 516

You are making this much too difficult on yourself.

First off, in Active Record models the attributes array is used to hold the values as fetched from the database. The instance variables, or attributes, are manipulated by the programmer and persisted back into the model via save or save!

You can inspect the validity of a model by calling model.valid? or doing:

begin
model.save!
rescue ActiveRecord::RecordInvalid
end

If you need to update an attribute individually or as a hash, ActiveRecord provides update_attribute and update_attributes that update the record and persist the model.

Iteration on the model.attributes array is failing to persist because attributes returns a hash of the attributes with _clones_ of the attribute value.

You might want to start coding with http://api.rubyonrails.com/ open in a tab -- these subtleties are often covered in the RDoc quite nicely.

You could also achieve the same effect by coding:

def scrub_attributes
self.attributes.each do |key,value|
if value.is_a?(String)
value.strip!
value.squeeze!(" ")
self.send("#{key}=", value)
OR
self.update_attribute(key, value)
end
end
end

Though grouping your changes and using update_attributes will result in attributes.size - 1 less DB queries to hydrate the model.

Scott Stevenson — Nov 07, 05 518 Scotty the Leopard

First off, in Active Record models the attributes array is used to hold the values as fetched from the database. The instance variables, or attributes, are manipulated by the programmer and persisted back into the model via save or save!

Sure -- this is the same as Core Data, DataCrux, WebObjects, etc.

You can inspect the validity of a model by calling model.valid? or doing:

Yup.

Iteration on the model.attributes array is failing to persist because attributes returns a hash of the attributes with _clones_ of the attribute value.

Ah. This is a little bit misleading because attributes = {:key = "value"} will actually change the value.

You might want to start coding with http://api.rubyonrails.com/ open in a tab

I do, but it's really only a reference. It's a lot more helpful if you already know what you're supposed to do.

these subtleties are often covered in the RDoc quite nicely.

Perhaps, but only if you can find the pertinent sections -- there's too much material to just read from start to finish.

You could also achieve the same effect by coding...

Why not just use write_attribute instead?

rg — Nov 13, 05 541

It's all about decoupling objects (aka Dependency Injection or Inversion of Control)!

Neal — Nov 14, 05 546

How does Obj-C do code injection? Via rentzsch.com/mach_inject/ ?

Duke Jones — Feb 04, 08 5447

Hello, I love the design of your site.

As for nil, you could make nil do nothing when a non-existent method is called on it:

class NilClass def method_missing(*args) end end

nil.do_something! # => nil

This is bad form for production, apparently, but could be quite helpful during development, as you pointed out.

Cheers!

Mauricio — May 27, 08 5931

Thanks man, that helped me for sure.




 

Comments Temporarily Disabled

I had to temporarily disable comments due to spam. I'll re-enable them soon.





Copyright © Scott Stevenson 2004-2015