Deeper Into ActiveRecord
Things have been progressing nicely for me. As I said before, creating a new controller, action, model, or association has gone from scary black magic to “type it in without checking the syntax first”. Well, mostly anyway.
The new big challenge I’m having now is getting the data out from non-trivial (though not hugely complex) data model associations. I have to find valid users’ games. Clubs have Fields, Fields have Games, and games have an Agegroup (ie: peewee, junior, etc). Users belong to a Club and have an Agegroup. My challenge is to connect those two sets of associations so that basically I can say:
Give me all games with the same agegroup as a given user has, in the same club as the given user is in.
First I had to get all the associations setup, just simple “has_one”, “has_many”, and “belongs_to” in the models. I got it to the point that I could do things like:
Club.first.fields.first.games
This lets me know that a) my models are set up right (and honestly I thought I had them set up until I came up with this example while typing this up and found it didn’t work, and had to add a missing “has_many” to my Field model).
The next step was IRB, in fact, living in IRB for a night. Well, a bit more than a night actually. This lets you do the code/test/results/try-again cycle way faster than editing a controller and reloading a webpage. Man I wish we had this thing for my Perl programming with the Class::DBI ORM.
The next thing I found that’s been a big help was from this James Buck post on ActiveRecord logging. It’s important to run this as the first command when you start up IRB, and it’ll display the SQL being executed, a huge help for seeing how things like :joins and :includes affect your queries.
Now don’t get me wrong, I can get the data out easily. I can either hardcode the SQL and it’s ugly joins into the Model, or I can do it all in “pure” ActiveRecord with multiple calls and arrays (ie: get a list of fields, get a list of games from that, iterate through the list of games and grab the ones matching the age condition), but my real goal here (other than getting deeper into ActiveRecord of course) is to see if I can do this in “pure” ActiveRecord, in one line (must be the Perl programmer in me).
The secret it turns out, is reversing your thinking. Instead of trying to figure out how the queries and relationships work “up” the chain, from the games, finding the fields they belong to, and if they belong to the club, I wondered why I couldn’t just get a list of all the games in a club. This was the key.
I couldn’t relate a club to it’s games directly, because they belonged to an intermediate model, fields. I had to relate a club to it’s games through another model. So after adding this to the Club model:
has_many :games, :through => :fields
I could run
Club.first.games
and get a result, and from there it was an easy step to:
User.first.club.games.find_by_age_id(1)
Which is ugly, but when it’s put into “real” code it’ll look somewhat nicer, something along the lines of:
@user = User.find_by_id(params[:user])
@games = @user.club.games.find( :all, :conditions => { :age_id => @user.age_id } )
Oddly enough, “find.all( … )” doesn’t work, but that’s a battle for another day
March 11th, 2010 at 2:28 pm
has_many :through is awesome sauce.
What you’ll ideally want to do is set up named scopes or accessor methods so you aren’t doing these awkward chains in your code, and instead end up with something more readable.
@user.games should really just give you the games that are scoped within the user’s club and age group, without that somewhat awkward (you called it “somewhat nicer”) statement at the end of your post.
The find(:all, options) method is equivalent to all(options), so you would never chain .find.all.
Also, I’m wondering if Agegroup is warranted as a standalone model, or if this should just be a simple attribute. Does your Agegroup have any of its own attributes, or is it really just a lookup table?
March 11th, 2010 at 2:34 pm
Thanks Andrew, definitely have a lot to learn
I was going to put something like this into the model, so I could do something cleaner (maybe Game.getvalid(@user.age_id) ?). The agegroups is really just a lookup table, as are a couple of the other attributes in the models (I have a ‘group level’ as well which is similar). I assume that making it an attribute in the model instead of another table means if it’s needing to be checked or joined against other models, it can’t be? IE: If I need to match @user.age to @game.age ?
March 11th, 2010 at 4:23 pm
Good point about keeping Agegroup as a model if you’re joining across models, so you can take advantage of the all-powerful :through association:
class User :agegroup, :source => :games
end
@user.eligible_games # => All games in the user’s agegroup.
You can also do something like this, if you need to scope games to their age groups in other situations, for example:
class Game { :agegroup => user.agegroup } }
end
end
@club.games.eligible_for(@user)
March 11th, 2010 at 4:28 pm
WordPress ate my “<” characters. Here’s what I meant:
class User < ActiveRecord::Base
belongs_to :agegroup
has_many :eligible_games, :through => :agegroup, :source => :games
end
@user.eligible_games # => All games in the user’s agegroup.
class Game < ActiveRecord::Base
named_scope :eligible_for, lambda do |user|
{ :conditions => { :agegroup => user.agegroup } }
end
end
@club.games.eligible_for(@user)
March 11th, 2010 at 4:54 pm
Got it working…. woo! Forgot I had a couple of slightly different model names here in the blog to try to make the meaning a bit more expressive (in reality my agegroup mode is actually “Age”), and for some reason on my system I have to use { } in the lambda, not do / end. However, now I can run that, and you’re right, it’s *way* more clean