views:

1563

answers:

4

I am trying to create a model for a ruby on rails project that builds relationships between different words. Think of it as a dictionary where the "Links" between two words shows that they can be used synonymously. My DB looks something like this:

Words
----
id

Links
-----
id
word1_id
word2_id

How do I create a relationship between two words, using the link-table. I've tried to create the model but was not sure how to get the link-table into play:

class Word < ActiveRecord::Base
  has_many :synonyms, :class_name => 'Word', :foreign_key => 'word1_id'
end
+2  A: 

Hmm, this is a tricky one. That is because synonyms can be from either the word1 id or the word2 id or both.

Anyway, when using a Model for the link table, you must use the :through option on the Models that use the Link Table

class Word < ActiveRecord::Base
  has_many :links1, :class_name => 'Link', :foreign_key => 'word1_id'
  has_many :synonyms1, :through => :links1, :source => :word
  has_many :links2, :class_name => 'Link', :foreign_key => 'word2_id'
  has_many :synonyms2, :through => :links2, :source => :word
end

That should do it, but now you must check two places to get all the synonyms. I would add a method that joined these, inside class Word.

def synonyms
  return synonyms1 || synonyms2
end

||ing the results together will join the arrays and eliminate duplicates between them.

*This code is untested.

Tilendor
Thanks, this is really an elegant solution to the synonym-problem.
sdfx
You're Welcome :)
Tilendor
The elegance is lost on me... You're creating two links when you only need one. Check my answer.
Ryan Bigg
ok, maybe not "elegant", but it's easy to implement with existing data. But I agree that the solution of Sarah is the way to go.
sdfx
+1  A: 

I'd view it from a different angle; since all the words are synonymous, you shouldn't promote any one of them to be the "best". Try something like this:

class Concept < ActiveRecord::Base
  has_many :words
end

class Word < ActiveRecord::Base
  belongs_to :concept

  validates_presence_of :text
  validates_uniqueness_of :text, :scope => :concept_id

  # A sophisticated association would be better than this.
  def synonyms
    concept.words - [self]
  end
end

Now you can do

word = Word.find_by_text("epiphany")
word.synonyms
Daniel Schierbeck
sdfx
If `A == B`, `C == D`, and `A == D`, then `A == B == C == D`. Equality is transitive, so they should all share a single concept.This is an idealized example, since words can have more than one meaning, but that just means that you need a many-to-many relationship between words and concepts.
Daniel Schierbeck
I suspect the reciprocal links approach is better for most situations, but this is an interesting approach, thanks for posting it.
Jason Watkins
+3  A: 

In general, if your association has suffixes such as 1 and 2, it's not set up properly. Try this for the Word model:

class Word < ActiveRecord::Base
  has_many :links, :dependent => :destroy
  has_many :synonyms, :through => :links
end

Link model:

class Link < ActiveRecord::Base
  belongs_to :word
  belongs_to :synonym, :class_name => 'Word'

  # Creates the complementary link automatically - this means all synonymous
  # relationships are represented in @word.synonyms
  def after_save_on_create
    if find_complement.nil?
      Link.new(:word => synonym, :synonym => word).save
    end
  end

  # Deletes the complementary link automatically.
  def after_destroy
    if complement = find_complement
      complement.destroy
    end
  end

  protected

  def find_complement
    Link.find(:first, :conditions => 
      ["word_id = ? and synonym_id = ?", synonym.id, word.id])
  end
end

Tables:

Words
----
id

Links
-----
id
word_id
synonym_id
Sarah Mei
sdfx
You can keep it as word_1 and word_2 in the links table if you want - what I meant was if you have two *associations* with numeric suffixes, it usually means you should refactor. My code takes you from two sets of associations to one.
Sarah Mei
has_many :synonyms won't work because in your link model you're not defining any association called :synonym. Please also break different models into different code sections.
Ryan Bigg
This is a good way to do it, probably has some advantages over mine. +1
Tilendor
Nice catch on the association naming, Radar. Fixed, and reads better too.
Sarah Mei
+1  A: 

Word model:

class Word < ActiveRecord::Base
  has_many :links, :dependent => :destroy
  has_many :synonyms, :through => :links

  def link_to(word)
    synonyms << word
    word.synonyms << self
  end
end

Setting :dependent => :destroy on the has_many :links will remove all the links associated with that word before destroying the word record.

Link Model:

class Link < ActiveRecord::Base
  belongs_to :word
  belongs_to :synonym, :class_name => "Word"
end

Assuming you're using the latest Rails, you won't have to specify the foreign key for the belongs_to :synonym. If I recall correctly, this was introduced as a standard in Rails 2.

Word table:

name

Link table:

word_id
synonym_id

To link an existing word as a synonym to another word:

word = Word.find_by_name("feline")
word.link_to(Word.find_by_name("cat"))

To create a new word as a synonym to another word:

word = Word.find_by_name("canine")
word.link_to(Word.create(:name => "dog"))
Ryan Bigg
I think that your issue has one problem: word = Word.find_by_name("feline")word.synonyms << Word.find_by_name("cat")Makes cat a synonym to feline, but not vice versa. You either need to check both columns in the link table, like mine, or create a compliment like Sarah.
Tilendor
@Tilendor saw that too. If you add the creation of the compliment, this solution should be exactly like the corrected version of Sarah
sdfx
Now to link the two words together use the link_to method defined on the word model.
Ryan Bigg