Ruby on Rails
Beginner Howto on has_many :through

has_many :through and drop downs

The other day, I wanted to implement something with Ruby on Rails that – as far as I could tell – was not very well documented. As I’m a beginner with Ruby and with Rails, I’m not sure in how far this is the best way to go, but it’s a working way.

What I want to do is have three tables: movies, dancers and their join table that I called dancer_movies. The main goal of this exercise is to be able to add dancers directly via a drop down that lists all dancers from the database (and the number of dances they appeared in in that movie) when creating a new movie entry.
*

New Project

First of all I’ll create a new rails directory called movies:

$shell: rails movies

Database

Then I’ll prepare the database by creating the tables I need.


CREATE DATABASE `movies` ;

CREATE TABLE `dancers` (
`id` INT( 11 ) NOT NULL AUTO_INCREMENT ,
`firstname` VARCHAR( 100 ) NOT NULL ,
`lastname` VARCHAR( 100 ) NOT NULL ,
PRIMARY KEY ( `id` )
);


CREATE TABLE `movies` (
`id` INT( 11 ) NOT NULL AUTO_INCREMENT ,
`title` VARCHAR( 200 ) NOT NULL ,
PRIMARY KEY ( `id` )
);

CREATE TABLE `dancer_movies` (
`id` INT( 11 ) NOT NULL AUTO_INCREMENT ,
`dancer_id` INT( 11 ) NOT NULL ,
`movie_id` INT( 11 ) NOT NULL ,
`numofdances` INT( 11 ) NOT NULL ,
PRIMARY KEY ( `id` )
);

I will have to adapt my database.yml in the section “development”, the name of the development database is just plain “movies”.

Scaffold

I’ll have rails do most of the work by letting it create a scaffold for movies and my other two models:


ruby script\generate scaffold movie
ruby script\generate model dancer
ruby script\generate model dancer_movie


h2. Models

Now I have to let Rails know about the associations:

app/models/movie.rb:


class Movie < ActiveRecord::Base
  has_many :dancer_movies, :dependent => true
  has_many :dancers, :through => :dancer_movies
end

app/models/dancer.rb:


class Dancer < ActiveRecord::Base
  has_many :dancer_movies, :dependent => true
  has_many :movies, :through => :dancer_movies
end

app/models/dancer_movies.rb:


class DancerMovie < ActiveRecord::Base
  belongs_to :dancer
  belongs_to :movie
end

Views

The view needs to be adapted, so it will list dancers and their number of dances per movie.

list.rhtml

app/views/movies/list.rhtml:


<h1>Listing movies

<% Movie.content_columns.each do |column| %> <% end %>


<% @movies.each do |movie| >


< Movie.content_columns.each do |column| >
<% end %>



<% end %>

<%= column.human_name %>Dancers

Action
<=h movie.send(column.name) %>



<% movie.dancer_movies.each do |appearance| >

<% end %>

<= appearance“numofdances” >   <=

appearance.dancer.firstname > <= appearance.dancer.lastname %>

 
<%= link_to ‘Show’, :action => ‘show’, :id => movie %>

<%= link_to ‘Edit’, :action => ‘edit’, :id => movie %> <%= link_to ‘Destroy’, { :action => ‘destroy’, :id => movie }, :confirm =>
‘Are you sure?’, :post => true %>

<%= link_to ‘Previous page’, { :page => @movie_pages.current.previous } if

@movie_pages.current.previous >
<
= link_to ‘Next page’, { :page => @movie_pages.current.next } if

@movie_pages.current.next %>


<%= link_to ‘New movie’, :action => ‘new’ %>

_form.rhtml

I also need to extend the _form.rhtml a bit, so when editing or creating a movie I get a number of drop down boxes with the dancers to chose from, and a text field to provide the number of dances they’re in.

app/views/movies/_form.rhtml:


<!--[form:movie]-->
<p><label for="movie_title">Title



<%= text_field ‘movie’, ‘title’ %>




<% dancer_movies.each do |appearance| %> <% end %>
<%= text_field_tag (“appearance[numofdances][]”,
@appearance.numofdances, “size” => "5") %>


Controller

new

I’ll need to define @dancer_movies in the “new”-function in the controller: The part with the “15.times do” is merely there so there are 15 empty dancer_movies when creating a new one – that way I get 15 text_fields and drop downs.

app/controller/movies_controller.rb:


  def new
    @movie = Movie.new
    @dancer_movies = Array.new
    15.times do
      @dancer_movies << DancerMovie.new
    end
  end


h3. create

When saving a movie, I also want to save its dancers and their number of dances, so I change the “create”-function a bit. First I’ll delete all the entries from the parameters “appearance” that are empty strings (no choice or entry was made there, so there’s nothing to save either). Then I’ll iterate over the number of dancers that were added and create new DancerMovie-objects that I then iterate over to create every appearance (save it in the database).


  def create
    @movie = Movie.new(params[:movie])
    if @movie.save

  1. NEW ##
    @dancer_movies = Array.new
    params[:appearance]‘numofdances’.delete(“")
    params[:appearance]‘dancer_id’.delete(”")
for i in 0…params[:appearance]‘dancer_id’.length

@dancer = Dancer.find(params[:appearance]‘dancer_id’[i])
@dancer_movies << DancerMovie.new(:movie => @movie,
:dancer => @dancer,
:numofdances =>

params[:appearance]‘numofdances’[i])
end
@movie.dancers(true)
for appearance in @dancer_movies
appearance.create
end

  1. END of new part ##
    flash[:notice] = ‘Movie was successfully created.’
    redirect_to :action => ‘list’
    else
    render :action => ‘new’
    end
    end

edit

Same thing as in the “new”-function: When editing a movie, the user might very well want to add a dancer, so I append a few extra dancer_movies to the array of existing ones.


  def edit
    @movie = Movie.find(params[:id])
    @dancer_movies = @movie.dancer_movies
    10.times do
	    @dancer_movies << DancerMovie.new
    end
  end
 

update

Same thing as in the “create”-function: When a movie gets updated, I want to update its dancers and their number of appearances as well. What I do here is first find all DancerMovies that belong to the particular movie and delete them and afterwards save every remaining appearance in the database.


  def update
    @movie = Movie.find(params[:id])
    if @movie.update_attributes(params[:movie])
      @dancer_movies = DancerMovie.find(:all, :conditions => ["movie_id = ?",

@movie.id])
for old_appearance in @dancer_movies
DancerMovie.delete(old_appearance.id)
end
@new_appearances = Array.new
params[:appearance]‘numofdances’.delete(“")
params[:appearance]‘dancer_id’.delete(”")

for i in 0…params[:appearance]‘dancer_id’.length

@dancer = Dancer.find(params[:appearance]‘dancer_id’[i])
@new_appearances << DancerMovie.new(:movie => @movie,
:dancer => @dancer,
:numofdances =>

params[:appearance]‘numofdances’[i])
end
@movie.dancers(true)

for appearance in @new_appearances

appearance.create
end

flash[:notice] = ‘Movie was successfully updated.’ redirect_to :action => ‘show’, :id => @movie else render :action => ‘edit’ end end

Quintessence:

Many-to-many relationships with attributes are quite difficult to handle for beginners – especially with a lack of documentation.

I’m looking forward to getting feedback and to find out whether there is a more direct / more object oriented way to implement this (especially the create and update functions).

—-

Position Question

Your example works great for me, except every time I update an item, all the existing appearances get new position numbers and are sent to the back because they are treated as new entries. Is there a better way to update the appearances without removing then reentering?

-Lance

—-

Back Button Error

Thank you for the great introduction. I noticed that if you enter edit mode for movies, and lets say change your mind and do not enter any data, when you click the back button you get a nil object error. The only way back is via the edit button.

-Chris

—-
No List Action Code

So where’s the list action controller? There are back / fwd buttons in the rhtml, but @movie_pages is not hooked up to anything, and wouldn’t be given this implementation. That would be the really interesting part of this!

-James

—-
What version of Rails is this example compatible with?

I’m running into tons of errors implementing this, eg:
“The :dependent option expects either :destroy, :delete_all, or :nullify (true)”

I have a feeling this is because I’m using Rails 2. It would be nice if it could be specified what version(s) of Rails examples are compatible with.

-Jack
—-

We should have opted for migrations than pure sql, for creating the DB structure. This would be a good signal for the beginners.

-s