Search code examples
ruby-on-railsassociationshas-manyself-reference

Rails self-referential has_many association


Using Ruby on Rails, I think I need to create a self-referential has_many association to model words in Chinese.

Background:

Each word is a composite of multiple component words.

For example, if I have three words, 'ni', 'hao', and 'nihao', I want to be able to do:

nihao.components = ['ni', 'hao']
and
'ni'.composites = ['nihao']
'hao'.composites =['nihao']

I don't believe this should be a hierarchical association (I've seen several gems... ) because a word doesn't have 1 or 2 "parents", it has 0, 1, or hundreds of "composites". Likewise a word has 0, 1, or several "components".

I've tried:

class Word < ActiveRecord::Base
  has_many :relationships
  has_many :components, through: :relationships, foreign_key: :component_id
  has_many :composites, through: :relationships, foreign_key: :composite_id
end

class Relationship < ActiveRecord::Base
  belongs_to :component, class_name: "Word"
  belongs_to :composite, class_name: "Word"
end

This isn't quite correct as I am unable to add components:

nihao.components << ni
   (0.2ms)  BEGIN
   (0.2ms)  ROLLBACK
ActiveModel::UnknownAttributeError: unknown attribute 'word_id' for Relationship.
        from (irb):5

Database schema:

create_table "relationships", force: :cascade do |t|
    t.integer "component_id"
    t.integer "composite_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "words", force: :cascade do |t|
    t.string "characters"
    t.string "pinyin"
    t.string "opinyin"
    t.string "tpinyin"
    t.string "english"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

Solution

  • Try this, you were not associating your models properly for this kind of use case.

    class Word < ActiveRecord::Base
      has_many :component_relationships, class_name: 'Relationship', foreign_key: :composite_id
      has_many :composite_relationships, class_name: 'Relationship', foreign_key: :component_id
    
      has_many :components, through: :component_relationships
      has_many :composites, through: :composite_relationships
    end 
    
    class Relationship < ActiveRecord::Base
      belongs_to :component, class_name: "Word", foreign_key: :component_id
      belongs_to :composite, class_name: "Word", foreign_key: :composite_id
    end
    

    I have not tried this, but this should work.