The idea is to use join models to leverage Active Record’s has_many :through() macro. This is taken from Chad Fowler, Rails Recipes, 2012. (Chapter One)
The has_and_belongs_to_many() (habtm) macro is used for join tables, but those join tables (for example a ‘subscriptions’ table joining Magazines and Readers) do not have any data in them other than the relationship. For example, you would name a relationship table magazines_readers, and include only the id’s for Magazine and Reader.
The idea here is to store information in the join table, and it is called a join model. The relationship item has dates.
We will use a very simple notion of a Car, a Driver, and a Lease. The Lease has a Start Date, an End Date, and a Rate.
This example is also serving to explore the minimum number of files which are necessary to run Active Record and Rails.
Fowler calls this join model a first-class concept - a Lease. It exists by itself.
Here are the relationships. A Car has many Leases; but it also has many Drivers. A Driver has many Leases; but it also has many Cars. Each Lease belongs to a Car, and each Lease belongs to a Driver. The relationship between Driver and Car (and vice versa, Car and Driver) is the one using the :through. The relationship between Car and Driver is then implicitly many-to-many.
The interesting things are as follows:
You can do this: car.leases << lease
. You have to do lease.save
at the end in order to persist the relationships which have been added.
Then, you can do car.drivers
or driver.cars
. The relation is implicit, and works through the :through parameter of the has_many declarations.
ActiveRecord generates a SQL query like this: SELECT “cars”.* from “cars” inner join “leases” on “cars”.id = “leases”.car_id where ((“leases”.reader_id = 1))
Note that there is an interesting accessor with a SQL condition in the Car model.
This is run by using rails runner app/myscript.rb
.
The steps to generating this app were:
-
At the command prompt, create a new Rails application:
rails new myapp -J --skip-bundle --skip-test-unit --skip-prototype
(wheremyapp
is the application name) -
Change directory to
myapp
and edit the Gemfile. Add therubyracer. Remove sqlite3 and replace it with mysql2; dobundle install
. -
Edit the automatically generated database.yml file to use MySQL2 instead of sqlite3.
-
Run
rails g model driver name
. -
Run
rails g model car make model_name serial
. -
Create the lease db/migrate file:
class CreateLeases < ActiveRecord::Migration
def change create_table :leases do |t| t.integer :driver_id t.integer :car_id t.date :start_date t.date :end_date t.string :rate t.timestamps end end end
-
Run
rake db:create
. -
Run
rake db:migrate
. -
Run
rake db:test:prepare
. -
Add this to the Car model:
class Car < ActiveRecord::Base attr_accessible :make, :model_name, :serial has_many :leases has_many :drivers, :through => :leases # named accessor has_many :cheap_drivers, :through => :leases, :source => :driver, :conditions => ['rate < 5.0'] end
-
Add this to the Driver model:
class Driver < ActiveRecord::Base
attr_accessible :name has_many :leases has_many :cars, :through => :leases
end
-
Add this to the Lease model:
class Lease < ActiveRecord::Base
attr_accessible :start_date, :end_date, :rate belongs_to :car belongs_to :driver
end
-
Run
rake db:seed
to create data. That looks like this:lease = Lease.create(:start_date => “2012-02-14”, :end_date => “2012-08-14”, :rate => “4.5”) car = Car.create(:make => “Mazda”, :model_name => “GLC”, :serial => “MG1”) driver = Driver.create(:name => “Babe”)
# Wild syntax car.leases << lease driver.leases << lease
lease.save
-
Then run
rails runner app/myscript.rb
.