Creating associations to existing data part 1: belongs_to scaffolding

I decided it would be fun to look into various different types of Rails forms that allow you to create a new object that is associated with existing data. In my next few posts, I’ll explore different ways to select existing records, and also how to work with has_and_belongs_to_many and has_many, through relationships in a Rails form. It seems to me that these use cases are more common than the nested models form in the complex-form-examples sample app that creates new records but doesn't associate with existing ones.

To start with today, here’s the simplest such form I could think of – I call this a “belongs_to” form:

Here we can see a form for a new “shirt” record; along with the color and size the user can also select the person who owns the shirt. In this example, the shirt and person models have a has_many/belongs_to relationship. This form uses simple HTML <select> and <option> tags to display the list of people, and is generated by Rails ERB code that uses “collection_select” like this:

 1 <p>
 2   Person:<br />
 3   <%= f.collection_select(:person_id,
 4                           Person.all,
 5                           :id,
 6                           :name,
 7                           { :prompt => true })
 8   %>
 9 </p>

The parameters passed to collection_select here indicate that we want to display all of the people that exist, and set the value and label for each <option> tag to the id and name of each person respectively. When the form is submitted, the “person_id” field for this shirt is set to the “id” value of the selected person.

Belongs_to scaffolding with View Mapper

If you need a form like this in your app, you can use my View Mapper gem to generate it for your models as follows… first install it from gemcutter; you’ll need version 0.3.2 at least for this form:

$ gem sources -a http://gemcutter.org
http://gemcutter.org added to sources 
$ sudo gem install view_mapper
Successfully installed view_mapper-0.3.2

Then you can generate the belongs_to view scaffolding you see above like this:

$ ./script/generate view_for shirt --view belongs_to

View Mapper will open the specified model (“Shirt”), detect the associated model(s) that Shirt belongs to, and then generate the form using collection_select along with all of the other standard scaffolding files. You can also have View Mapper generate the new Shirt model and the view scaffolding code at the same time like this:

./script/generate scaffold_for_view shirt color:string size:integer
                                    --view belongs_to:person

Here you’ve specified the desired attributes for the new shirt model, along with the fact that you want it to belong to a person.

To make the generated view code simple and concise, View Mapper makes a couple of assumptions about your models:

  • It assumes the parent model (“Person” in this example) has an attribute or method called “name.” This is used to display the list of people.
  • It also assumes the child model (“Shirt”) has a method to display the name of the parent model it belongs to (“person_name” in this example).

I decided not to make the View Mapper command line more complex than it already is by providing a way to pass the “name” attribute as another parameter. Instead you can just edit the scaffolding code it produces as desired.

Detailed example and code review

Let’s create a sample app now together using View Mapper, and then review how this “belongs_to view” works. First, create a new Rails app:

$ rails belongs_to
      create  
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
      create  config/environments
      etc...

And now let’s create the Person model with first and last name attributes; I’ll also use the console to create a few Person records so we have some existing data to work with:

$ cd belongs_to
$ ./script/generate model person first_name:string last_name:string
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/person.rb
      create  test/unit/person_test.rb
      create  test/fixtures/people.yml
      create  db/migrate
      create  db/migrate/20100124120247_create_people.rb
$ rake db:migrate
(in /Users/pat/rails-apps/belongs_to)
==  CreatePeople: migrating ===================================================
-- create_table(:people)
   -> 0.0030s
==  CreatePeople: migrated (0.0032s) ==========================================
 $ ./script/console 
Loading development environment (Rails 2.3.5)
>> Person.create :first_name => 'Barack', :last_name => 'Obama'
>> Person.create :first_name => 'George', :last_name => 'Bush'
>> Person.create :first_name => 'Bill',   :last_name => 'Clinton'

Now let’s go ahead and run View Mapper to create the form…

$ ./script/generate scaffold_for_view shirt color:string size:integer
                                      --view belongs_to:person
     warning  Model Person does not contain a has_many association for Shirt.

Here View Mapper is warning us that we haven’t called “has_many :shirts” in the Person model yet; let’s edit person.rb and enter that code:

1 class Person < ActiveRecord::Base
2   has_many :shirts
3 end

Now we can try again:

$ ./script/generate scaffold_for_view shirt color:string size:integer
                                      --view belongs_to:person
     warning  Model Person does not have a name attribute.

Above I mentioned that View Mapper assumes the parent model has a “name” attribute or method. This is required to know how to display each person record in the <select> drop down box on the form. Remember in the call to collection_select we passed in the symbol “:name” – this tells Rails to call the name method for the label of each <option> tag. So let’s create a name method in the Person model that displays the first and last name attributes together:

1 class Person &lt; ActiveRecord::Base
2   has_many :shirts
3 def name 4 "#{first_name} #{last_name}" 5 end
6 end

The highlighted code returns the first and last names concatenated together as a single name. Now View Mapper won’t complain and we can go ahead and create our new form:

$ ./script/generate scaffold_for_view shirt color:string size:integer
                                      --view belongs_to:person
      exists  app/models/
      exists  app/controllers/
… etc …
create app/views/shirts/_form.html.erb
… etc … exists test/fixtures/ create app/models/shirt.rb create db/migrate/20100124121032_create_shirts.rb

Note the output here looks just like what you get from the standard Rails scaffold generator, except for the one additional line I highlighted above. If you run your app now, you’ll be able to see scaffolding for the Shirts model at http://localhost:3000/shirts, and you’ll see the person select box on the new and edit forms.

Let me take a few more minutes to point out a couple of interesting details about the belongs_to scaffolding… first I’ve moved the form fields into a partial shared among the new and edit views; this is the new _form.html.erb file highlighted above in the generator output. If you look at new.html.erb, you’ll see a call to render :partial:

 1 <h1>New shirt</h1>
 2 
 3 <% form_for(@shirt) do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <p> 6 <%= f.submit 'Create' %> 7 </p> 8 <% end %> 9 10 <%= link_to 'Back', shirts_path %>

The edit.html.erb file looks similar. The actual call to collection_select is in _form.html.erb; that way if you need to display the list of people differently, for example to use a method other than “name” for each person or possibly to use a filtered list of people instead of Person.all, then you just need to make your changes in one place.

And one more interesting detail: if you open up the new Shirts model that View Mapper generated you’ll see this:

1 class Shirt < ActiveRecord::Base
2   belongs_to :person
3 def person_name 4 person.name if person 5 end
6 end

The person_name method I highlighted above returns the person each shirt belongs to; it also checks if person is nil for that shirt. This simplifies the code in index.html.erb and show.html.erb; take a look at show.html.erb for example:

 1 <p>
 2   <b>Color:</b>
 3   <%=h @shirt.color %>
 4 </p>
 5 
 6 <p>
 7   <b>Size:</b>
 8   <%=h @shirt.size %>
 9 </p>
10 
11 <p> 12 <b>Person:</b> 13 <%=h @shirt.person_name %> 14 </p>
15 16 <%= link_to 'Edit', edit_shirt_path(@shirt) %> | 17 <%= link_to 'Back', shirts_path %>

Here the Person field looks just like any other field from the Shirt model. There’s no need to repeat the check for person == nil in the view, and if you ever need to use a Person attribute other than name or if you needed to find the associated person in some more complex way, you’ll only need to make the change to the model and not in each view file.

Again, if you run View Mapper on two has_many/belongs_to models that you’ve already written in your app, you will first need to provide:

  • A “name” method in the parent model, if it’s not already an attribute, and
  • A “[parent_model]_name” method in the child model

If these two methods don’t exist, View Mapper will display warning messages and not proceed; this avoids the confusion you would run into when the scaffolding view code didn’t work.

Next time, I’ll repeat this example but use type ahead/auto_complete to select the person instead...