stevengharms.com

Sententiae viri ex temporibus duobus

[Rails]: Steven's Guide to: "Many to Many Associations" or "HABTM" (1/2)

The concept: “Many to Many” relationships

Many things in this life have a one to one relationship: a man has a wife ( Utah excepted ), a car has an owner, a dog it’s day. Many other things in life have a one to many relationship: a policy covers many people, a manager has direct reports, etc. But some things have a many to many relationship. People have surnames (“John Smith” is a Smith and so is “Jane Smith”;conversely, among the Smith clan there might be multiple Johns, 42 Janes, and 22 Larry’s ).

It is of this many to many relationship I will write and whose code I seek to share.

In this world there are many candies ( “Now and Laters”, “Sweet Tarts”, and “Kasugai’s Gummies” ) and candies have a flavor (chocolate, strawberry, cherry, and for my readers of an Oriental persuasion, lychee).

There are many candies which have a flavor, and a particular flavor has many candies which apply to it. This is a perfect, and simple example wherewith to demonstrate Rails' ActiveRecord’s Associations.

Build the Rails install directory

So, let’s start a rails project:

rails -d postgresql candy

And you will get a great deal of noise back:

create  
create  app/controllers
create  app/helpers
create  app/models
create  app/views/layouts
create  config/environments
create  config/initializers
create  db
create  doc
create  lib
create  lib/tasks
create  log
create  public/images
create  public/javascripts
create  public/stylesheets
create  script/performance
create  script/process
create  test/fixtures
create  test/functional
create  test/integration
create  test/mocks/development
create  test/mocks/test
create  test/unit
create  vendor
create  vendor/plugins
create  tmp/sessions
create  tmp/sockets
create  tmp/cache
create  tmp/pids
create  Rakefile
create  README
create  app/controllers/application.rb
create  app/helpers/application_helper.rb
create  test/test_helper.rb
create  config/database.yml
create  config/routes.rb
create  public/.htaccess
create  config/initializers/inflections.rb
create  config/initializers/mime_types.rb
create  config/boot.rb
create  config/environment.rb
create  config/environments/production.rb
create  config/environments/development.rb
create  config/environments/test.rb
create  script/about
create  script/console
create  script/destroy
create  script/generate
create  script/performance/benchmarker
create  script/performance/profiler
create  script/performance/request
create  script/process/reaper
create  script/process/spawner
create  script/process/inspector
create  script/runner
create  script/server
create  script/plugin
create  public/dispatch.rb
create  public/dispatch.cgi
create  public/dispatch.fcgi
create  public/404.html
create  public/422.html
create  public/500.html
create  public/index.html
create  public/favicon.ico
create  public/robots.txt
create  public/images/rails.png
create  public/javascripts/prototype.js
create  public/javascripts/effects.js
create  public/javascripts/dragdrop.js
create  public/javascripts/controls.js
create  public/javascripts/application.js
create  doc/README_FOR_APP
create  log/server.log
create  log/production.log
create  log/development.log
create  log/test.log

Now, let’s imagine a “Candy”, it has a “name”. Similarly a “Flavor” has a “name”. These two will be our models. We’ll use Rails' new scaffolding to create these models.

Build the Candy and Flavor models

bash-3.2$ cd candy/
bash-3.2$ script/generate scaffold Candy name:string; \
  script/generate scaffold Flavor name:string

Output not displayed

Create the database

bash-3.2$ psql -l |grep -i candy
bash-3.2$ rake db:create
(in /Users/sgharms/railsdev/candy)
bash-3.2$ psql -l |grep -i candy
 candy_development           | sgharms  | UTF8

OK, so there’s our database. Since I don’t want to have to create a user record, I’m going to edit my config/database.yml to make connections to the database anonyomous. I’m going to remove the user and password fields.

>development:
>adapter: postgresql
>encoding: unicode
>database: candy_development
>#  username: candy
>#  password:

Use db:migrate to bring the generated models into the db

bash-3.2$ rake db:migrate
(in /Users/sgharms/railsdev/candy)
== 1 CreateCandies: migrating =========================================
-- create_table(:candies)
NOTICE:  CREATE TABLE will create implicit sequence "candies_id_seq" 
for serial column "candies.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index 
"candies_pkey" for table "candies"     -> 0.0097s
== 1 CreateCandies: migrated (0.0099s) ================================

== 2 CreateFlavors: migrating =========================================
-- create_table(:flavors)
NOTICE:  CREATE TABLE will create implicit sequence "flavors_id_seq" 
for serial column "flavors.id"
    NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index 
    "flavors_pkey" for table "flavors"
== 2 CreateFlavors: migrated (0.0071s) ================================

So far so good! Let’s check out the tables.

Examine the tables with Psql, the Postgres CLI tool

Logically enough, a table for holding “Candy”-model based objects and “Flavor”-based objects were created. A simple SQL query shows the contents to match our expectation.

    bash-3.2$ psql candy_development
    Welcome to psql 8.2.5, the PostgreSQL interactive terminal.

    Type:  \copyright for distribution terms
           \h for help with SQL commands
           \? for help with psql commands
           \g or terminate with semicolon to execute query
           \q to quit

    candy_development=# \dt
               List of relations
     Schema |    Name     | Type  |  Owner  
    --------+-------------+-------+---------
     public | candies     | table | sgharms
     public | flavors     | table | sgharms
     public | schema_info | table | sgharms
    (3 rows)

    candy_development=# select * from CANDIES, flavors;
     id | name | created_at | updated_at | id | name 
    ----+------+------------+------------+----+------
    (0 rows)

Well that’s pretty nifty, no entries.

Create the HABTM relationship

Now, we need to make a change to the model. We need to make both of these models recognize the other as a HABTM partner. Simple enough, add these line to “candy.rb” and “flavor.rb” in app/models/, respectively:

has_and_belongs_to_many :flavors
has_and_belongs_to_many :candies

You may be wondering if the plural of “candy” was “candys” or “candies” in Rails' eyes. Let’s check that out.

bash-3.2$ script/console 
Loading development environment (Rails 2.0.2)
>> "candy".pluralize
=> "candies"

Since we scaffolded these views when we created the model, let’s try adding a Candy.

Start the server:

    >> bash-3.2$ script/server
    => Booting Mongrel (use 'script/server webrick' to force WEBrick)
    => Rails application starting on http://0.0.0.0:3000
    => Call with -d to detach
    => Ctrl-C to shutdown server
    ** Starting Mongrel listening at 0.0.0.0:3000
    ** Starting Rails with development environment...
    ** Rails loaded.
    ** Loading any Rails specific GemPlugins
    ** Signals ready.  TERM => stop.  USR2 => restart.  
    INT => stop (no restart).
    ** Rails signals registered.  HUP => reload (without restart). 
     It might not work well.
    ** Mongrel 1.1.2 available at 0.0.0.0:3000
    ** Use CTRL-C to stop.

And visit http//localhost:3000/candies

You should have a basic scaffold view that you can create candies with.

ss2.

I added two candies and back in the console I could see…

    >> puts Candy.find(:all).to_yaml
    --- 
    - !ruby/object:Candy 
      attributes: 
        name: PowerThirst Bar
        updated_at: 2008-01-10 18:26:22.882081
        id: "1"
        created_at: 2008-01-10 18:26:22.882081
      attributes_cache: {}

    - !ruby/object:Candy 
      attributes: 
        name: Sweet Tart
        updated_at: 2008-01-10 18:26:38.846037
        id: "2"
        created_at: 2008-01-10 18:26:38.846037
      attributes_cache: {}

The Web UI shows:

ss1

Similarly one can create flavors using the scaffold as well, but in this case I’ll use the console:

    >> Flavor
    => Flavor(id: integer, name: string, created_at: datetime, updated_at: datetime)
    >> Flavor.create :name=>"Shockolate"
    => #<Flavor id: 1, name: "Shockolate", created_at: "2008-01-10 18:30:13", updated_at: "2008-01-10 18:30:13">
    >> Flavor.create :name=>"Rawberry"
    => #<Flavor id: 2, name: "Rawberry", created_at: "2008-01-10 18:30:17", updated_at: "2008-01-10 18:30:17">
    >> Flavor.create :name=>"Lychee"
    => #<Flavor id: 3, name: "Lychee", created_at: "2008-01-10 18:30:34", updated_at: "2008-01-10 18:30:34">
    >> Flavor.create :name=>"Strawberry"
    => #<Flavor id: 4, name: "Strawberry", created_at: "2008-01-10 18:30:38", updated_at: "2008-01-10 18:30:38">
    >> Flavor.find_all  
    NoMethodError: undefined method `find_all' for #<Class:0x1b60c3c>
        from /opt/local/lib/ruby/gems/1.8/gems/activerecord-2.0.2-/lib/active_record/base.rb:1532:in `method_missing'
        from (irb):7
    >> Flavor.find(:all)
    => [#<Flavor id: 1, name: "Shockolate", created_at: "2008-01-10 18:30:13", updated_at: "2008-01-10 18:30:13">, #<Flavor id: 2, name: "Rawberry", created_at: "2008-01-10 18:30:17", updated_at: "2008-01-10 18:30:17">, #<Flavor id: 3, name: "Lychee", created_at: "2008-01-10 18:30:34", updated_at: "2008-01-10 18:30:34">, #<Flavor id: 4, name: "Strawberry", created_at: "2008-01-10 18:30:38", updated_at: "2008-01-10 18:30:38">]

You can take a look at your database schema in db/schema.rb:

ActiveRecord::Schema.define(:version => 2) do

  create_table "candies", :force => true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "flavors", :force => true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end
end

Create a join table

Now, a HABTM requires a join table, so that the two can look back and forth at one anotherthat’s fancy-talk for a two-way index between these two tables.

The convention that we use is to create a table, that has no primary key, that has the consituentModelNameOne_id as one column and consituentModelNameTwo_id as the other column. When I say ‘constituentModelName’ that should be in the singular. Also, the name of the table should list the plural of the two models, separated by an underscore “_” with the plurals in alphabetical order. In our case this would be “candies_flavors”

This sounds hairy, but I’ll show you. It’s really easy.

bash-3.2$ script/generate migration createCandyFlavorJoinTable
      exists  db/migrate
      create  db/migrate/003_create_candy_flavor_join_table.rb
bash-3.2$ 




Excellent!  So now let's create that table.  If you forget the syntax, you can look at your 001 or 002 migration in db/migrate to learn how to create a table.




    class CreateCandyFlavorJoinTable < ActiveRecord::Migration
      def self.up
        create_table (:candies_flavors, :id=>false) do |t|
          t.integer :candy_id
          t.integer :flavor_id

          t.timestamps
        end
      end

      def self.down
        drop_table :candies_flavors
      end
    end

Great! All we need to do is use another visit to rake.

bash-3.2$ rake db:migrate
(in /Users/sgharms/railsdev/candy)
== 3 CreateCandyFlavorJoinTable: migrating ====================================
-- create_table(:candies_flavors, {:id=>false})
   -> 0.0049s
== 3 CreateCandyFlavorJoinTable: migrated (0.0051s) ===========================

We can check it out in the Postgres viewer:

candy_development=# select * from candies_flavors ;
 candy_id | flavor_id | created_at | updated_at 
----------+-----------+------------+------------
(0 rows)

So now let’s associate a candy with a flavor.

See what we have

>> Candy.find(:all)
=> [#<Candy id: 1, name: "PowerThirst Bar", created_at: "2008-01-10 18:26:22", updated_at: "2008-01-10 18:26:22">, #<Candy id: 2, name: "Sweet Tart", created_at: "2008-01-10 18:26:38", updated_at: "2008-01-10 18:26:38">]
>> Flavor.find(:all)
=> [#<Flavor id: 1, name: "Shockolate", created_at: "2008-01-10 18:30:13", updated_at: "2008-01-10 18:30:13">, #<Flavor id: 2, name: "Rawberry", created_at: "2008-01-10 18:30:17", updated_at: "2008-01-10 18:30:17">, #<Flavor id: 3, name: "Lychee", created_at: "2008-01-10 18:30:34", updated_at: "2008-01-10 18:30:34">, #<Flavor id: 4, name: "Strawberry", created_at: "2008-01-10 18:30:38", updated_at: "2008-01-10 18:30:38">]

Note that you can now ask a Candy what it’s ‘flavors’ are - with tab-autocomplete:

>> Candy.find(1).fla(TAB)(TAB)vor(TAB)(TAB)
1).flavor_ids    1).flavor_path   1).flavors       1).flavors_path
1).flavor_ids=   1).flavor_url    1).flavors=      1).flavors_url
>> Candy.find(1).flavors
=> []

Similarly, a flavor can be asked its candies:

>> Flavor.find_by_name('Shockolate').candies
=> []

Let’s associate ‘Shockolate’ and ‘Rawberry’ with a PowerThirst Bar

>> example.flavors
=> []
>> example.flavors << Flavor.find_by_name('Shockolate')
=> [#<Flavor id: 1, name: "Shockolate", created_at: "2008-01-10 18:30:13", updated_at: "2008-01-10 18:30:13">]
    >> example.flavors << Flavor.find_by_name('Rawberry')
    => [#<Flavor id: 1, name: "Shockolate", created_at: "2008-01-10 18:30:13", updated_at: "2008-01-10 18:30:13">, #<Flavor id: 2, name: "Rawberry", created_at: "2008-01-10 18:30:17", updated_at: "2008-01-10 18:30:17">]
    >> example.flavors
    => [#<Flavor id: 1, name: "Shockolate", created_at: "2008-01-10 18:30:13", updated_at: "2008-01-10 18:30:13">, #<Flavor id: 2, name: "Rawberry", created_at: "2008-01-10 18:30:17", updated_at: "2008-01-10 18:30:17">]

Let’s see what’s happened in the DB.

candy_development=# SELECT * from candies_flavors ;
 candy_id | flavor_id |         created_at         |         updated_at         
----------+-----------+----------------------------+----------------------------
        1 |         1 | 2008-01-10 18:30:13.002001 | 2008-01-10 18:30:13.002001
        1 |         2 | 2008-01-10 18:30:17.905373 | 2008-01-10 18:30:17.905373
(2 rows)

Unsurprisingly candy 1 (Candy.find(1).name) is associated with flavor id 1 and 2 ( Shockolate and Rawberry, respectively).

Sweet (ur, Shockolate-y?). We have a 1 to many relationship now (“The name "PowerThirst Bar” corresponds two two flavors: Shockolate and Rawberry"). But we want those to also work in reverse. That we could get all the candies that are flavored, say with good old “Strawberry”. To the console!

Add another candy

>> Candy.create :name=>"Now and Later"
=> #<Candy id: 3, name: "Now and Later", created_at: "2008-01-10 19:02:35", updated_at: "2008-01-10 19:02:35">  

Now let’s associate a few candies with ‘Strawberry’ flavor. I’m going to include the PowerThirst bar because Rawberry is a subclass of Strawberry.

    >> strawberry=Flavor.find_by_name('Strawberry')
    => #<Flavor id: 4, name: "Strawberry", created_at: "2008-01-10 18:30:38", updated_at: "2008-01-10 18:30:38">
    >> strawberry.name
    => "Strawberry"
    >> strawberry.candies
    => []
    >> strawberry.candies << Candy.find(1,2,3)
    => [#<Candy id: 1, name: "PowerThirst Bar", created_at: "2008-01-10 18:26:22", updated_at: "2008-01-10 18:26:22">, #<Candy id: 2, name: "Sweet Tart", created_at: "2008-01-10 18:26:38", updated_at: "2008-01-10 18:26:38">, #<Candy id: 3, name: "Now and Later", created_at: "2008-01-10 19:02:35", updated_at: "2008-01-10 19:02:35">]
    >> strawberry.candies
    => [#<Candy id: 1, name: "PowerThirst Bar", created_at: "2008-01-10 18:26:22", updated_at: "2008-01-10 18:26:22">, #<Candy id: 2, name: "Sweet Tart", created_at: "2008-01-10 18:26:38", updated_at: "2008-01-10 18:26:38">, #<Candy id: 3, name: "Now and Later", created_at: "2008-01-10 19:02:35", updated_at: "2008-01-10 19:02:35">]   

So there you have it. You can see the structure beautifully with the .to_yaml method:

    >> puts strawberry.to_yaml
    --- !ruby/object:Flavor 
    attributes: 
      name: Strawberry
      updated_at: 2008-01-10 18:30:38.250826
      id: "4"
      created_at: 2008-01-10 18:30:38.250826
    attributes_cache: {}

    candies: 
    - !ruby/object:Candy 
      attributes: 
        name: PowerThirst Bar
        updated_at: 2008-01-10 18:26:22.882081
        id: "1"
        created_at: 2008-01-10 18:26:22.882081
      attributes_cache: {}

    - !ruby/object:Candy 
      attributes: 
        name: Sweet Tart
        updated_at: 2008-01-10 18:26:38.846037
        id: "2"
        created_at: 2008-01-10 18:26:38.846037
      attributes_cache: {}

    - !ruby/object:Candy 
      attributes: 
        name: Now and Later
        updated_at: 2008-01-10 19:02:35.926161
        id: "3"
        created_at: 2008-01-10 19:02:35.926161
      attributes_cache: {}

    => nil

Or….

    >> puts Candy.find(1).flavors.to_yaml
    --- 
    - !ruby/object:Flavor 
      attributes: 
        name: Shockolate
        updated_at: 2008-01-10 18:30:13.002001
        id: "1"
        flavor_id: "1"
        candy_id: "1"
        created_at: 2008-01-10 18:30:13.002001
      attributes_cache: {}

      readonly: true
    - !ruby/object:Flavor 
      attributes: 
        name: Rawberry
        updated_at: 2008-01-10 18:30:17.905373
        id: "2"
        flavor_id: "2"
        candy_id: "1"
        created_at: 2008-01-10 18:30:17.905373
      attributes_cache: {}

      readonly: true
    - !ruby/object:Flavor 
      attributes: 
        name: Strawberry
        updated_at: 2008-01-10 18:26:22.882081
        id: "4"
        flavor_id: "4"
        candy_id: "1"
        created_at: 2008-01-10 18:26:22.882081
      attributes_cache: {}

      readonly: true
    => nil

With this in shape, we are able to manipulate the tables and the model via the script/console. We next need to beef-up the views – but that’s for the next installment!

Comments