سؤال

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

enter image description here

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! :)

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top