Lyte's Blog

Bad code, bad humour and bad hair.

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:

1
2
$ rails students
$ cd students

Create Student Scaffold

1
2
$ rake db:migrate
$ script/generate scaffold Student name:string

Enter some students

Starting up the server and navigating to the students url should produce an empty listing.

1
$ 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

1
2
$ 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:

1
2
3
4
5
6
  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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 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:

1
2
3
4
5
6
7
8
9
>> t = Tutorship.new
>> t = Tutorship.new
=> #<Tutorship id: nil, tutor_id: nil, pupil_id: nil, created_at: nil, updated_at: nil>
>> t.tutor = Student.find_by_name('Dave')
=> #<Student id: 1, name: 'Dave', created_at: '2009-03-17 08:10:19', updated_at: '2009-03-17 08:48:14'>
>> t.pupil = Student.find_by_name('Frank')
=> #<Student id: 2, name: 'Frank', created_at: '2009-03-17 08:10:26', updated_at: '2009-03-17 08:48:14'>
>> t.save!
=> true

Now check that you can get pupils/tutors on a student:

1
2
3
4
5
6
7
8
>> s = Student.find_by_name('Dave')
=> #<Student id: 6, name: 'Dave', created_at: '2009-04-03 02:29:00', updated_at: '2009-04-03 02:29:00'>
>> s.pupils
=> [#<Student id: 7, name: 'Frank', created_at: '2009-04-03 02:29:19', updated_at: '2009-04-03 02:29:19'>]
>> s = Student.find_by_name('Frank')
=> #<Student id: 7, name: 'Frank', created_at: '2009-04-03 02:29:19', updated_at: '2009-04-03 02:29:19'>
>> s.tutors
=> [#<Student id: 6, name: 'Dave', created_at: '2009-04-03 02:29:00', updated_at: '2009-04-03 02:29:00'>]

Editing interface

To make this all editable in the front end I added some data gathering code in app/controllers/students_controller.rb:

1
2
3
4
5
6
 def edit
 @student = Student.find(params[:id])
 @possible_tutors = @possible_pupils = Student.find(:all)
 @selected_tutors = @student.tutors
 @selected_pupils = @student.pupils
 end

I also added some view code in app/views/students/edit.html.erb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 <b>Tutors:</b>

 <%=
 select_tag(
 'student[tutor_ids][]',
 options_for_select(
 @possible_tutors.collect { |s| [s.name, s.id] },
 @selected_tutors.collect { |s| s.id }
 ),
 :multiple => true
 )
 %>

 <b>Pupils:</b>

 <%=
 select_tag(
 'student[pupil_ids][]',
 options_for_select(
 @possible_pupils.collect { |s| [s.name, s.id] },
 @selected_pupils.collect { |s| s.id }
 ),
 :multiple => true
 )
 %>

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’ve created a tar file with the Rails app in it.

Comments