Here is my afternoon’s work exploring Rails 2.0.2 ActiveRecord Associations.
I went through all the possible ‘vanila’ versions, created samples, and documented what I learned.
These are a lot of combinations to test.
class Master < ActiveRecord::Base
has_one :bt1
end
class Bt1 < ActiveRecord::Base
belongs_to :master
end
master = Master.find(:first)
In this case, master.bt1 is a Bt1 object.
The text fixture hack of specifying associated records by yaml id works in this case.
The following fixtures files relate record masters record one to bt1s record one
from masters.yml
one:
name: Master Record 1
two:
name: Master Record 2
from bt1s.yml
one:
name: BT Record 1
master: one
belongs_to
record:
foo_id – which contains the id value for the associated recordfoo_type – which contains the type of the associated record
This allows the Polymorphic record to be associated with more than
one type.
class Master2 < ActiveRecord::Base
has_one :btp1, :as => :assoc
end
class Master3 < ActiveRecord::Base
has_one :btp1, :as => :assoc
end
class Btp1 < ActiveRecord::Base
belongs_to :assoc, :polymorphic => true
end
master = Master2.find(:first)
In this case master.btp1 is a Btp1 record who’s assoc_type field
has the string value ‘Master2’.
Here are the Migration files with the down method removed:
class CreateBtp1s < ActiveRecord::Migration
def self.up
create_table :btp1s do |t|
t.string :name
t.integer :assoc_id
t.string :assoc_type
end
end
end
class CreateMaster2s < ActiveRecord::Migration
def self.up
create_table :master2s do |t|
t.string :name
end
end
end
class CreateMaster3s < ActiveRecord::Migration
def self.up
create_table :master3s do |t|
t.string :name
end
end
end
I couldn’t think of any way to specify the assocated records
in the Yaml fixture files without specifying id and type
by hand.
Here they are:
# Master2
one:
name: Master2 Rec 1
id: 1
two:
name: Master2 Rec 2
id: 2
---------------------------
# Master3
one:
name: Master3 Rec 1
id: 1
two:
name: Master3 Rec 2
id: 2
---------------------------
# Btp1's
one:
name: Btp1 Rec 1
assoc_id: 1
assoc_type: Master2
two:
name: Btp1 Rec 2
assoc_id: 2
assoc_type: Master2
three:
name: Btp1 Rec 3
assoc_id: 1
assoc_type: Master3
four:
name: Btp1 Rec 4
assoc_id: 2
assoc_type: Master3
This variant does the obvious thing. Here’s the code:
class BtHm < ActiveRecord::Base
belongs_to :master_hm
end
class MasterHm < ActiveRecord::Base
has_many :bt_hm
end
master = MasterHm.find(:first)
This time master.bt_hm is a list of BtHm objects.
The table construction follows the doc and the neat hack
for associating BtHm records with MasterHm records
works, as in master_hm: one.
has_many in the records which are associated
with the record which declares the association polymorphichas_many records have a I didn’t bother to build this one.
This also works as advertized – if you follow all the conventions.
Here’s the code which creates the tables:
class CreateHmBt1s < ActiveRecord::Migration
def self.up
create_table :hm_bt1s do |t|
t.string :name
end
end
end
class CreateHmBt2s < ActiveRecord::Migration
def self.up
create_table :hm_bt2s do |t|
t.string :name
end
end
end
class HmBt1HmBt2 < ActiveRecord::Migration
def self.up
create_table :hm_bt1s_hm_bt2s, :id => false do |t|
t.integer :hm_bt1_id
t.integer :hm_bt2_id
end
end
end
r1 = HmBt1.find(:first)
r2 = HmBt2.find(:first)
Points to Note:
create_table :hm_bt1s_hm_bt2s. Make sure
the table names in the join table are pluralized., :id => false clause Both r1.hm_bt2 and r2.hm_bt1 are lists of objects.
Intestingly, r1.hm_bt2.hm_bt1 is empty – which makes sense because
otherwise we could have an infinite recursion.
Strangeness: when I first tried this test, I forgot to include the
:id => false clause in the join table migration. Then in the test
code, I tried getting the associated table via something like
HmBt1.find(hm_bt2.hm_bt1.id). Rails stuffed in the id field from
the join table, so the find failed. Removing the id field from the
join table fixed it.
Also, the hack in the Yaml files in test/fixtures works:
# HmBt1
one:
name: HmBt1 1 --> hm_bt2 1
hm_bt2: one
two:
name: HmBt1 2 --> hm_bt2 1
hm_bt2: one
three:
name: HmBt1 3 --> hm_bt2 3
hm_bt2: three
four:
name: HmBt1 4 --> hm_bt2 4
hm_bt2: four
-------------------------------
# HmBt2
one:
name: HmBt2 1 --> hm_bt1 1 and 2
hm_bt1: one, two
two:
name: HmBt2 2 --> (none)
three:
name: HmBt2 3 --> hm_bt1 3
hm_bt1: three
four:
name: HmBt2 4 --> hm_bt1 4
hm_bt1: four
This sets up associations as described in the name field of each record.
This is really a sub-set of many-to-many using a :through relationship.
I didn’t attempt to test this, but skipped to the many-to-many
using has_many with the :through clause.
:through clause, the referent of :through must
has_many
clause.Here is the code which makes this one work – in the small case:
class CreateHmtAs < ActiveRecord::Migration
def self.up
create_table :hmt_as do |t|
t.string :name
end
end
end
class HmtA < ActiveRecord::Base
has_many :hmt_joins
has_many :hmt_bs, :through => :hmt_joins
end
-------------------------
class CreateHmtBs < ActiveRecord::Migration
def self.up
create_table :hmt_bs do |t|
t.string :name
end
end
end
class HmtB < ActiveRecord::Base
has_many :hmt_joins
has_many :hmt_as, :through => :hmt_joins
end
-------------------------
class CreateHmtJoins < ActiveRecord::Migration
def self.up
create_table :hmt_joins do |t|
t.integer :hmt_a_id
t.integer :hmt_b_id
t.string :useless_data, :default => "Useless"
end
end
end
class HmtJoin < ActiveRecord::Base
belongs_to :hmt_a
belongs_to :hmt_b
end
The Yaml hack doesn’t work, so I had to explicitly set id’s and the join table.
HmtA.find(:first).hmt_bs is a list of hmtB objects.
Just for the heck of it, I tried relating three tables as follows:
A <--- A.id
B.id -----> B <---- B.id
C.id ----> C
where the A.id/B.id represents a join table, as well as B.id/C.id.
Rails would have to generate the following SQL for this to work:
select * from C where join2.C_id join2.B_id where B.id in (select B_id from join1 where A_id A.id)
I guess it’s not that smart (yet)
Again, this is an illusion in that it is the essentially the same as a many-to-many, polymorphic association.
Suppose I have a class that I want to join with several different classes which have some similarity in nature, then I will want to have something like:
A <--- A.id
J.id ----> B
J.type == B
<--- A.id
J.id ----> C
J.type == C
The code is fairly straight forward. The only problems I’ve had
is understanding the meaning of some of the has_many options:
belongs_to field the model masquerades as
in the join table. It only has meaning for polymorphic joins
and for the tables which ‘morph’ into the joinbelongs_to field in the join table that
the non-morphed table is looking to for the index of the morphing
table.Here’s the code which creates the tables:
class CreatePtmasters < ActiveRecord::Migration
def self.up
create_table :ptmasters do |t|
t.string :name
end
end
end
class Ptmaster < ActiveRecord::Base
has_many :ptjoins
has_many :bjoins, :through => :ptjoins,
:source_type => 'Bjoin', :source => :joiner
has_many :cjoins, :through => :ptjoins,
:source_type => 'Cjoin', :source => :joiner
end
-------------
class CreatePtjoins < ActiveRecord::Migration
def self.up
create_table :ptjoins do |t|
t.integer :ptmaster_id
t.integer :joiner_id
t.string :joiner_type
end
end
end
class Ptjoin < ActiveRecord::Base
belongs_to :ptmaster
belongs_to :joiner, :polymorphic => true
end
-------------
class CreateBjoins < ActiveRecord::Migration
def self.up
create_table :bjoins do |t|
t.string :name
end
end
end
class Bjoin < ActiveRecord::Base
has_many :ptjoins, :as => :joiner
has_many :ptmasters, :through => :ptjoins
end
-------------
class CreateCjoins < ActiveRecord::Migration
def self.up
create_table :cjoins do |t|
t.string :name
end
end
end
class Cjoin < ActiveRecord::Base
has_many :ptjoins, :as => :joiner
has_many :ptmasters, :through => :ptjoins
end
The thing actually makes sense.