I worked out the design issue that I mentioned yesterday, and learned a few things in the process. The most helpful thing I encountered were the three most recent screencasts from Railscasts, which Jason Perkins pointed me toward.
As an aside, if you’re not using screencasts to augment your learning, you’re really missing out. I think they’re probably the most information-dense way to learn development concepts that I’ve found — far more useful than book reading for example. I think that screencasts work well for two reasons. The first is that watching someone else do something is a very natural way to learn, and watching them work in a screencast is especially good because it’s as though you’re looking at their computer as they work, rather than watching them talk from a podium. The second is that they provide the opportunity to absorb other good habits through observation. I noticed three or four TextMate features I wasn’t already using while watching the screencasts that will certainly save me time down the road.
So back to my problem. To recap, I have a data model that looks like this:
id home_address_id business_address_id
Both tables have other properties but they don’t really come into play here.
The problem is that I have one form that’s used to create a user, a home address, and a business address all at once, and the circular reference in the database makes it challenging to validate the user’s input and save everything.
After reading the comments on the previous entry and doing a lot of thinking, I considered changes to the data model and changes to the business logic, but would up fixing the whole thing by more cleverly using Ruby on Rails.
What’s validation for, anyway?
The first thing I figured out is that this line in
Address.rb was a bad idea:
You’d think it’s a good idea because an address that’s not associated with a user is an orphan, and you shouldn’t create orphaned addresses. However, the main purpose of validations in Ruby on Rails is not to prevent you from inserting bad data into the database but rather to validate user input and display errors in a clean and helpful way. The identity of the user associated with an address is not user input as that relationship is created behind the scenes. If that validation rule fails, there’s a bug in the code, not a user error, so you don’t need to use validation to handle it. I took that line out.
Handling the associations more intelligently
The next issue to tackle was validating the address before saving the user. Doing so was possible once I got rid of the unneeded validation rule, but the question was how best to do the validating. Fortunately, ActiveRecord provides the validates_associated method, which prevents a model from being saved if any of the associated objects specified are invalid.
User class looked something like this:
class User < ActiveRecord::Base belongs_to :home_address, :class_name => “Address”, :foreign_key => “home_address_id” belongs_to :business_address, :class_name => “Address”, :foreign_key => “business_address_id” end
I was creating the addresses and assigning them to the
business_address properties of the user, and using
validates_associated :home_address, :business_address
to make sure the addresses were valid before saving the user. That worked fine for validation, but
user.save blew up due to a circular reference. I couldn’t save the user because the addresses didn’t have IDs yet and I couldn’t save the addresses because the user wasn’t saved. Whoops.
Here’s the solution:
class User < ActiveRecord::Base
has_many :addresses belongs_to :home_address, :class_name => “Address”, :foreign_key => “home_address_id” belongs_to :business_address, :class_name => “Address”, :foreign_key => “business_address_id” end
has_many allows the user to be saved before the addresses, and I reference that relationship with
validates_associated so that validation works properly. So now the process is to create a user, create the addresses and associate them with the user. I can validate and save the user (automatically saving the associated addresses along the way) and then assign the newly created addresses to
business_address and save the user again.
That gives me everything I need. I can directly access a user’s current home and business address, and I can keep all of a user’s old addresses on file. I can also validate user input in a clean and Rails-ish way.
Thanks to everyone who offered advice and on to the next thing.