I'm trying to make a simple movie database using Rails 4.0.0 as a learning project. I'm particularly interested in using scaffolding as much as possible, as this is one of the features that drew me to RoR in the first place.
Yes, I do realize the potential risks (Box 1.2.Scaffolding: Quicker, easier, more seductive), but I promise I won't have my project go public before I really understand what's going on beneath the hood. Right now I'm more in "evaluating technologies for my next super duper project"-mode.
Here's what I've got so far:
rails g scaffold Person name:string
rails g scaffold Movie name:string
Now, I could've done something like
rails g scaffold Movie name:string person_id:integer
instead, but I want a movie to be associated with both a director and an actor. (Next step is to make an association that relates multiple actors to a single movie, but I'm not quite there yet.)
So, I headed over to the blog post Creating Multiple Associations With the Same Table, describing pretty much what I need. It's a somewhat old post, so things might have changed now - I don't know. Anyway. This how I changed the models:
class Person < ActiveRecord::Base
has_many :movies
end
and
class Movie < ActiveRecord::Base
belongs_to :director_id, :class_name => 'Person', :foreign_key => 'person_id'
belongs_to :actor_id, :class_name => 'Person', :foreign_key => 'actor_id'
end
and finally the magical
rake db:migrate
Starting the WEBrick by running rails s
in the console, I open my browser and start registering people
The time has come to start registering movies. According to previous questions here and here I have to make a migration script in order to create the necessary database fields. So this is what I did:
rails g migration AddPersonIdsToMovies director_id:integer actor_id:integer
I also updated the app/views/movies/_form.html.erb
to
<%= form_for(@movie) do |f| %>
<% if @movie.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@movie.errors.count, "error") %> prohibited this movie from being saved:</h2>
<ul>
<% @movie.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :director_id %><br>
<%= f.select :director_id, Person.all.collect {|x| [x.name, x.id]}, {}, :multiple => false %>
</div>
<div class="field">
<%= f.label :actor_id %><br>
<%= f.select :actor_id, Person.all.collect {|x| [x.name, x.id]}, {}, :multiple => false %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
When I create a new movie, the view shows up fine and the select inputs works fine. However, the data in the director and actor field isn't persisted. I ran rails console
and looked at the newly created movie:
irb(main):004:0> mov = Movie.first
Movie Load (0.2ms) SELECT "movies".* FROM "movies" ORDER BY "movies"."id"
ASC LIMIT 1 => #<Movie id: 1, name: "No such movie", created_at:
"2013-08-02 17:02:12", updated_at: "2013-08-02 17:02:12",
director_id: nil, actor_id: nil>
which is kind'a disappointing with no director or actor info.
Update
Based on @Mattherick's suggesition, I edited the private part of the movies_controller.rb
to this:
# Never trust parameters from the scary internet, only allow the white list through.
def movie_params
params.require(:movie).permit(:name, :director_id, :actor_id)
end
Unfortunately, when I post a new movie I get
Person(#70319935588740) expected, got String(#70319918738480)
Extracted source:
# POST /movies.json def create @movie = Movie.new(movie_params) respond_to do |format| if @movie.save
and the request data goes as
{"utf8"=>"✓", "authenticity_token"=>"???", "movie"=>{"name"=>"Romantic Comedy", "director_id"=>"2", "actor_id"=>"1"}, "commit"=>"Create Movie"}
Update 2
I tried to create a new Movie in the rails console, like this:
irb(main):001:0> movie = Movie.new(name: "My fine movie", director_id: "1", actor_id: "2")
ActiveRecord::AssociationTypeMismatch: Person(#70311109773080) expected, got String(#70311102311480)
which is what you'd expect from the POST to the controller. This made me test what happened if I excluded the quotation marks for director_id
and actor_id
. So I did
irb(main):005:0> movie = Movie.new(name: "My fine movie", director_id: 1, actor_id: 2)
ActiveRecord::AssociationTypeMismatch: Person(#70282707507880) expected, got Fixnum(#70282677499540)
Still using the console, I decided to create an actor and a director
director = Person.new(name: "Woody Allen")
director.save
actor = Person.new(name: "Arnold Schwarzenegger")
actor.save
and then I did
movie = Movie.new(name: "I'd like to see that", director_id: director, actor_id: actor)
movie.save
which worked like a charm (output omitted for brevity). So the whole question boils down to "How can I pass a Person as the argument to director_id
and actor_id
through the web interface?"
If I had a single field in Movies called person_id: integer
, I believe that rails would've inferred that I'm not trying to pass a string containing the id of a person, but rather I'm trying to pass an entire person object.
Update 3
I tested my suspicion that rails understands how to deal with posts when the foreign key column is named after the pattern [table]_id. So I created a new project with a Continent
model and a Country
model, where rails g scaffold Country name:string continent_id:integer
. I changed my Country
view to include
<div class="field">
<%= f.label :continent_id %><br>
<%= f.select :continent_id, Continent.all.collect {|x| [x.name, x.id]} %>
</div>
instead of the default numeric field. The continent_id
is still posted a string:
Started POST "/countries" for 127.0.0.1 at 2013-08-03 10:40:40 +0200
Processing by CountriesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"???", "country"=>{"name"=>"Uganda", "continent_id"=>"4"}, "commit"=>"Create Country"}
yet rails understood that continent_id
was the identifier of an entry in the Continent
table.
Sadly, the inferrer mechanism doesn't work in my original case, as I have two associations from Movie
to the Person
table. I need to somehow make sure rails understand that there is a mapping from director_id
to a Person
.
Update 4
According to some sources, it seems as I need to refine the Person
model further. So I did
class Person < ActiveRecord::Base
has_many :directed_movies, :class_name => 'Movie', :foreign_key => 'director_id'
has_many :acted_movies, :class_name => 'Movie', :foreign_key => 'actor_id'
end
but still not fixing my problems.
I'm kind'a lost. Can anyone give me a few pointers on what I'm missing here? Can anyone help me map from director_id
to person_id
? Thanks!
Ok, so I finally got it. I don't think this is the correct way to go about this, but at least it solved the problem. As I mentioned in update 2, I was able to create a Movie
object in the irb, and so I asked the question "How can I pass a Person as the argument to director_id and actor_id through the web interface?"
According to the sources here and elsewhere, rails should've understood the has_many
and belongs_to
class methods. However, I just can't seem to get it to work.
So I hacked the create
method in movies_controller.rb
to read like this:
def create
director = Person.where(:id => movie_params[:director_id]).first
actor = Person.where(:id => movie_params[:actor_id]).first
@movie = Movie.new(name: movie_params[:name], director_id: director, actor_id: actor)
respond_to do |format|
if @movie.save
format.html { redirect_to @movie, notice: 'Movie was successfully created.' }
format.json { render action: 'show', status: :created, location: @movie }
else
format.html { render action: 'new' }
format.json { render json: @movie.errors, status: :unprocessable_entity }
end
end
end
This is certainly not as elegant as I'd like it to be, and I don't think it is the RoR way to do things. Unfortunately it is the only thing I got working so far, but if anyone else can make a multiple association to the same model using rails 4, please do give me a heads up! :)