POSTS
[Rails]: Steven's Guide to: "Many to Many Associations" or "HABTM" (1/2)
BlogThe 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.
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:
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