1

我正在尝试在 Rails 中构建一个简单的同义词库应用程序,其中单词表中的单词将通过同义词对的连接表与表中的其他单词具有多重、自连接关系。

我的 SynonymPair 类构建如下:

class SynonymPair < ActiveRecord::Base
    belongs_to :word1, class_name: :Word
    belongs_to :word2, class_name: :Word
end

这个词库程序的一个关键方面是,一个词是在 word1 还是 word2 列中都无关紧要。word1 是 word2 的同义词,反之亦然。

为了让我的 Words 类返回给定单词的 SynonymPairs 和 Synonyms,我编写了一个 SQL 查询:

class Word < ActiveRecord::Base

def synonym_pairs

    #joins :synonym_pairs and :words where either word1_id OR word2_id matches word.id.
    sql = <<-SQL 
    SELECT synonym_pairs.id, synonym_pairs.word1_id, synonym_pairs.word2_id, words.word FROM synonym_pairs 
    JOIN words ON synonym_pairs.word1_id = words.id WHERE words.word = ? 
    UNION SELECT synonym_pairs.id, synonym_pairs.word1_id, synonym_pairs.word2_id, words.word FROM synonym_pairs 
    JOIN words ON synonym_pairs.word2_id = words.id WHERE words.word = ?
    SQL

    #returns synonym_pair objects from the result of sql query
    DB[:conn].execute(sql,self.word,self.word).map do |element|
        SynonymPair.find(element[0])
    end
end

    def synonyms
        self.synonym_pairs.map do |element|
            if element.word1 == self
                element.word2
            else
                element.word1
            end
        end
    end
end

此代码按预期工作。但是,它没有利用 ActiveRecord 中的关联模型。所以,我想知道是否可以在 Words 类中编写一个 has_many :synonyms_pairs/has_many :synonyms through: :synonym-pairs 自定义关系查询,而不是像我上面那样写出整个 SQL 查询。换句话说,我很好奇是否可以将我的 SQL 查询转换为 Rails 自定义关系查询。

注意,我尝试了以下自定义关系查询:

class Word < ActiveRecord::Base

has_many :synonym_pairs, ->(word) { where("word1_id = ? OR word2_id = ?", word.id, word.id) }
has_many :synonyms, through: :synonym_pairs

end

但是,在传递了一些 Word/SynonymPair 种子后,当我尝试调用 word#synonym_pairs 时,它返回了一个“ActiveRecord:Associations:CollectionProxy”,当我调用 word#synonyms 时出现以下错误:

[17] pry(main)> w2 = Word.create(word: "w2")
=> #<Word:0x00007ffd522190b0 id: 7, word: "w2">
[18] pry(main)> sp1 = SynonymPair.create(word1:w1, word2:w2)
=> #<SynonymPair:0x00007ffd4fea2230 id: 6, word1_id: 6, word2_id: 7>
[19] pry(main)> w1.synonym_pairs
=> #<SynonymPair::ActiveRecord_Associations_CollectionProxy:0x3ffea7f783e4>
[20] pry(main)> w1.synonyms
ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) "synonym" or :synonyms in model SynonymPair. Try 'has_many :synonyms, :through => :synonym_pairs, :source => <name>'. Is it one of word1 or word2?

获取自定义关系查询或任何类型的自联接模型在这里工作的任何其他想法?

4

3 回答 3

0

您可能正在寻找范围ActiveRecord 类方法:

class SynonymPair < ActiveRecord::Base
    belongs_to :word1, class_name: :Word
    belongs_to :word2, class_name: :Word

    scope :with_word, -> (word) { where(word1: word).or(where(word2: word)) }
end

class Word < ActiveRecord::Base
  scope :synonyms_for, -> (word) do
    pairs = SynonymPair.with_word(word)
    where(id: pairs.select(:word1_id)).where.not(id: word.id).or(
    where(id: pairs.select(:word2_id)).where.not(id: word.id))
  end
   
  def synonyms
    Word.synonyms_for(self)
  end
end
于 2020-07-06T09:10:58.140 回答
0

如果您真的想设置关联,其中记录可以位于连接表的任一列中,您需要has_many为每个潜在外键建立一个关联和一个间接关联。

在这里忍受我,因为这真的很疯狂:

class Word < ActiveRecord::Base
  has_many :synonym_pairs_as_word_1, 
   class_name: 'SynonymPair',
   foreign_key: 'word_1'

  has_many :synonym_pairs_as_word_2, 
   class_name: 'SynonymPair',
   foreign_key: 'word_2'

  has_many :word_1_synonyms, 
   through: :synonym_pairs_as_word_1,
   class_name: 'Word', 
   source: :word_2

  has_many :word_2_synonyms, 
   through: :synonym_pairs_as_word_2,
   class_name: 'Word',
   source: :word_1

  def synonyms
    self.class.where(id: word_1_synonyms).or(id: word_2_synonyms)    
  end
end

由于这里的同义词仍然不是真正的关联,因此如果您正在加载单词列表及其同义词,您仍然存在潜在的 n+1 查询问题。

虽然您可以急切地加载 word_1_synonyms 和 word_2_synonyms 并将它们组合(通过转换为数组),但如果您需要对记录进行排序,则会出现问题。

于 2020-07-06T09:56:14.203 回答
0

您可以创建一个标准的 M2M 连接表,而不是同义词对表:

class Word
  has_many :synonymities
  has_many :synonyms, though: :synonymities
end
class Synonymity 
  belongs_to :word
  belongs_to :synonym, class_name: 'Word'
end
class CreateSynonymities < ActiveRecord::Migration[6.0]
  def change
    create_table :synonymities do |t|
      t.belongs_to :word, null: false, foreign_key: true
      t.belongs_to :synonym, null: false, foreign_key: { to_table: :words }
    end
  end
end

虽然此解决方案需要连接表中两倍的行数,但它可能非常值得权衡,因为处理外键不固定的关系是 ActiveRecord 中的噩梦。这行得通。

.eager_load在使用自定义查询和加载记录时,AR 并没有真正让您提供连接 sql,.includes如果结果和处理关联为已加载以避免 n+1 查询问题,则让 AR 变得有意义,这可能非常麻烦且耗时。有时你只需要围绕 AR 构建你的模式,而不是试图打败它以提交。

您将设置两个单词之间的同义词关系:

happy = Word.create!(text: 'Happy')
jolly = Word.create!(text: 'Jolly')
# wrapping this in a single transaction is slightly faster then two transactions
Synonymity.transaction do
  happy.synonyms << jolly
  jolly.synonyms << happy
end
irb(main):019:0> happy.synonyms
  Word Load (0.3ms)  SELECT "words".* FROM "words" INNER JOIN "synonymities" ON "words"."id" = "synonymities"."synomym_id" WHERE "synonymities"."word_id" = $1 LIMIT $2  [["word_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Word id: 2, text: "Jolly", created_at: "2020-07-06 09:00:43", updated_at: "2020-07-06 09:00:43">]>
irb(main):020:0> jolly.synonyms
  Word Load (0.3ms)  SELECT "words".* FROM "words" INNER JOIN "synonymities" ON "words"."id" = "synonymities"."synomym_id" WHERE "synonymities"."word_id" = $1 LIMIT $2  [["word_id", 2], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Word id: 1, text: "Happy", created_at: "2020-07-06 09:00:32", updated_at: "2020-07-06 09:00:32">]>
于 2020-07-06T09:11:52.760 回答