Rails 2.0.2 ActiveRecord Associations

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.

Kinds of Associations

  1. one-to-one – using has_one and belongs_to
  2. one-to-one polymorphic – using has_many and belongs_to
  3. one-to-many – using has_many and belongs_to
  4. one-to-many polymorphic – using has_many and belongs_to
  5. many-to-many – using has_many_and_belongs_to
  6. one-to-many – using has_many and :through
  7. many-to-many – using has_many and :through
  8. one-to-many polymorphic – using has_many and :through
  9. many-to-many polymorphic – using has_many and :through

These are a lot of combinations to test.

one-to-one – using has_one and belongs_to

    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

one-to-one polymorphic – using has_many and belongs_to

A Polymorphic one-to-one association uses two fields in the belongs_to record:
    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

one-to-many – using has_many and belongs_to

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.

one-to-many polymorphic – using has_many and belongs_to

Ok, this case is similar to the one-to-one polymorphic case, the only differences being:

I didn’t bother to build this one.

many-to-many – using has_many_and_belongs_to

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:

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.

one-to-many – using has_many and :through

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.

many-to-many – using has_many and :through

In order to use the :through clause, the referent of :through must
  1. exist as a separate and well defined model
  2. that model must be named as an association in the model having the 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)

one-to-many polymorphic – using has_many and :through

Again, this is an illusion in that it is the essentially the same as a many-to-many, polymorphic association.

many-to-many polymorphic – using has_many and :through

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:

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.