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.
Attribute Values in Rails
Posted Oct 31, 2005 — 11 comments below
Posted Oct 31, 2005 — 11 comments below
Mr eel — Oct 31, 05 484
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
Scott Stevenson — Oct 31, 05 489
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
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
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
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
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
Neal — Nov 14, 05 546
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