Pat Shaughnessy

Ribadesella, Spain

Creating associations to existing data part 1: belongs_to scaffolding

January 24, 2010 · 9 comments

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...

Tags:·

9 responses so far ↓

  • 1 cuzic4n // Feb 22, 2010 at 11:41 PM

    just ran into situation where i needed has_and_belongs_to_many. thanks for helping me save time already with view_mapper.. just letting you know has_and_belongs_to_many would be a nice addition..
  • 2 pat // Feb 23, 2010 at 09:34 AM

    Great to hear view_mapper has been useful for you... and yes I'm already working on a view for has_many, through and has_and_belongs_to_many associations. I'll post an "Existing data part 3" article here as soon as it's ready.
  • 3 kyle // Mar 07, 2010 at 02:43 PM

    Pat, I've been perusing your blog for some time now and I wanted to finally post and let you know I appreciate very much the work you've done with auto_complete for nested/complex forms. I'm working on a site with some rather complex requirements and auto_complete is a must, but sadly, due to the ridiculous nature of the way various tables are tied together, I'm stuck with a few has_many, :through situations that, while this works, it doesn't work perfectly (I'm getting duplicate entries with the HMT models). I see though from your previous comment that you're working on a version of your repeated_auto_complete that works with HMT associations and I'm greatly looking forward to using it! You've got a very slick, elegant and easy-to-use solution for a problem that can be downright painful to deal with and your work does not go unnoticed. I eagerly await your future revisions; keep up the excellent work!
  • 4 pat // Mar 07, 2010 at 10:24 PM

    Hey Kyle, Thanks a lot for the kind words... I’m definitely working on an HMT solution, but I was planning to start with the simpler existing data scenario: that is, when you have a complex form that includes the “through” model and when the has_many model already exists. For example: if programmers have many projects through assignments, then view mapper will support a complex form for creating the programmer and assignment at the same time, for existing projects. However, from your comment about the duplicate entries it sounds like you want to create the programmer, project and assignment all at the same time, which is more difficult. I’ll see what I can do to solve this as well... or at least I’ll try to point you in the right direction.
  • 5 kyle // Mar 08, 2010 at 02:11 PM

    Great to hear! I'm actually doing a site databasing live performances. It's rather complex but I've got a number of models that are reused (bands, songs, venues, addresses (address can have multiple venues as names have changed over the years), tours, performers and finally, the crux of it all: performances. Each performance has a band, tour, venue, many performers, and many songs through PlayedSongs (gotta have a HMT so I can put the song order somewhere to correctly list the set), plus the performance's own data fields (data, notes, etc). The biggest thing I want autocomplete for is song names...punctuation, spelling and capitalization are all crucial to it working correctly (the worst being capitalization) and if a single letter is off, apostrophe missed or extra space placed...the entire form fails and since multiple songs are added via Ajax, going back results in a completely blank slate. :( I'm going to use your current version of working with existing data with autocomplete for hooking up Venues and Addresses and possibly Bands and Songs. If I were to have this all exactly as I want it in a perfect world, I've had nested forms going 3 or 4 levels deep which is just ridiculous so I'll do what I can to make everything I have control over as easy to use as possible and your plugins are definitely going to go a long way toward helping that. Thanks again and keep up the awesome work! :D
  • 6 kyle // Mar 08, 2010 at 03:05 PM

    FYI, it looks like view_mapper takes whatever model you tell it to do auto_complete for and just removes an s at the end if there is one. I just attempted to do the auto_complete version of this with Address and Venue by putting --view belongs_to_auto_complete:address and it kept throwing an error that "model 'addres' does not exist" and only by putting in --view belongs_to_auto_complete:addresses was I able to get it working.
  • 7 kyle // Mar 08, 2010 at 04:26 PM

    Sorry to keep posting but I cannot get part 1 or part 2 of this working now. I had a working example on my machine yesterday using performances and songs but now when I try to do addresses and venues nothing works. I've gone so far as to create a scaffold for Address so I can input some data...I've tried using a virtual name attribute but that won't work because every time I try to generate the auto_complete scaffold it whines that there's no name column on address...so I migrated one in. I can see that it's successfully saving the name based on my definition on the Address.rb file but nothing is working in terms of auto_complete functionality. This is incredibly frustrating and so far this is what I hate about Rails the most. It works amazingly for relatively simple things but trying to combine/compact things into more complex forms and nested models results in massive headaches...and implementing fancy functionality on top of it all is asking for a bomb to go off in your face. Oh well. Maybe when Rails 3 comes out this will get easier. For now, it's just going to have to suck to enter data I guess. Thanks for your work on this and I definitely look forward to see where you take your plugins in the future but I think my site's needs are just too complex for this (or any) auto_complete implementation. :(
  • 8 pat // Mar 08, 2010 at 10:48 PM

    Don’t give up on Rails yet Kyle! I’m sorry that you’re getting frustrated, but Rails is definitely way better than any other web development framework out there. I would try to keep things simple and take it one step at a time.

    So this evening I was able to reproduce your problem with the “Address” model. The way view mapper works is that is calls “singularize” (see ActiveSupport::CoreExtensions::String::Inflections) to insure that the model name you pass in is singular. The problem you’re running into is that the Rails code for singularize doesn’t seem to work for the word “address” – for example:

    $ ./script/console 
    Loading development environment (Rails 2.3.5)
    >> 'Address'.singularize
    => "Addres"

    Weird! And a real bummer for you today... it will not only break view_mapper but also lots of other Rails code using your Address model. Fortunately, there’s an easy workaround. Take a look at this file in your Rails project: config/initializers/inflections.rb… you’ll see this code:

    # Be sure to restart your server when you modify this file.
    # Add new inflection rules using the following format 
    # (all these examples are active by default):
    # ActiveSupport::Inflector.inflections do |inflect|
    #   inflect.plural /^(ox)$/i, '\1en'
    #   inflect.singular /^(ox)en/i, '\1'
    #   inflect.irregular 'person', 'people'
    #   inflect.uncountable %w( fish sheep )
    # end

    So here the Rails team has anticipated that some words might not be singularized or pluralized properly (just imagine if you’re a non-English speaker!) and allow for you to override the default behavior. So if you change the file to this:

    ActiveSupport::Inflector.inflections do |inflect|
      inflect.singular /^(address)/i, '\1'
    end

    … now the word ‘Address’ should be singularized properly… i.e. not changed at all:

    $ ./script/console 
    Loading development environment (Rails 2.3.5)
    >> 'Address'.singularize
    => "Address"
    

    Now view_mapper should work properly for you…. Hang in there and give Rails a second chance!

  • 9 kyle // Mar 24, 2010 at 12:04 AM

    Sorry I went MIA there. I took a break from Rails for a bit and just let things simmer. Thanks for the response and the in-depth help/look at my situation. You're a hell of a guy, Pat. :) I'll check this out more closely tomorrow but I wanted to know that your further help/work has not gone unappreciated!

Leave a Comment