20090404

Giving up on Blogspot

I just don't like the software behind Blogspot, so despite my short stay I'm moving to blog.lyte.id.au.

20090317

Recursive many-to-many association in Rails

I'm fairly new to Ruby on Rails and I needed a way to create a recursive many-to-many association, but search as I may, the best help I could find was always on RailsRocket, and frankly their article, along with a lot of rails tutorials I've been reading, all leave a lot to be desired.

So here goes, a Recursive many-to-many association in Rails tutorial by a Rails newbie.

I'm using Ruby 1.8.7, RubyGems 1.2.0, Rails 2.1.0 and Linux.

App. Description
For this example I'm going to go with the what the RailsRocket article went over breifly. That is a Student database where we are mapping Tutors.

Why is this recursive? Well a Tutor is a Student, a Tutor teaches a Student and a Student is Tutored by a Student. Get it?

Getting started
Initialise the app:
$ rails students
$ cd students

Create Student Scaffold
$ rake db:migrate
$ script/generate scaffold Student name:string

Enter some studentsStarting up the server and navigating to the students url should produce an empty listing.
$ script/server
Now just put in some students so that we have something to play with. I created two: 'Dave' and 'Frank'.

Add the Tutorship model
$ script/generate model Tutorship tutor_id:integer pupil_id:integer
$ rake db:migrate

Add Rails associations
Add belongs_to associations in app/models/tutorship.rb:
  belongs_to :tutor, :class_name => "Student", :foreign_key => "tutor_id"
belongs_to :pupil, :class_name => "Student", :foreign_key => "pupil_id"
The only way I've been able to get this to work the way I want is to add an intermediate association on Student to it's Tutorships and then the actual association we want to use to map right through, via they has_many :through functionality.

For those who can make more sense of code, add the following associations to app/model/student.rb:
  has_many :tutorship_pupils,
:foreign_key => :tutor_id,
:class_name => 'Tutorship'
has_many :pupils,
:through => :tutorship_pupils,
:foreign_key => :pupil_id,
:class_name => 'Student'

has_many :tutorship_tutors,
:foreign_key => :pupil_id,
:class_name => 'Tutorship'
has_many :tutors,
:foreign_key => :tutor_id,
:through => :tutorship_tutors,
:class_name => 'Student'

Test the models
Fire up script/console and create a new Tutorship:
>> t = Tutorship.new
=> #<tutorship nil="">
>> t.tutor = Student.find_by_name('Dave')
=> #<student dave="" 17="" 19="">
>> t.pupil = Student.find_by_name('Frank')
=> #<student frank="" 26="" 17="" 33="">
>> t.save!
=> true
Now check that you can get pupils/tutors on a student:
>> s = Student.find_by_name('Dave')
=> #<student dave="" 19="" 17="" 14="">
>> s.pupils
=> [#<student frank="" 26="" 17="" 14="">]
>> s = Student.find_by_name('Frank')
=> #<student frank="" 26="" 17="" 14="">
>> s.tutors
=> [#<student dave="" 19="" 17="" 14="">]

Editing interface
To make this all editable in the front end I added some data gathering code in app/controllers/students_controller.rb:
  def edit
@student = Student.find(params[:id])
@possible_tutors = @possible_pupils = Student.all
@selected_tutors = @student.tutors
@selected_pupils = @student.pupils
end
I also add some view code in app/views/students/edit.html.erb:
  <b>:Tutors:</b>:
<p>:
<%=
select_tag(
'student[tutor_ids][]',
options_for_select(
@possible_tutors.collect { |s| [s.name, s.id] },
@selected_tutors.collect { |s| s.id }
),
:multiple =>: true
)
%>:
</p>:
<b>:Pupils:</b>:
<p>:
<%=
select_tag(
'student[pupil_ids][]',
options_for_select(
@possible_pupils.collect { |s| [s.name, s.id] },
@selected_pupils.collect { |s| s.id }
),
:multiple =>: true
)
%>:
</p>:
It should now be possible to edit tutor associations via the student edit action. The code for the student new action is fairly similar.

I was going to attach a zip file with all the code and full files for each snippet I mentioned but it seems this blog software doesn't really give me that functionality. If you'd like the code send me a message or leave a comment.