【问题标题】:Converting SQL query into Custom Relations Query in Rails在 Rails 中将 SQL 查询转换为自定义关系查询
【发布时间】:2020-07-06 02:29:19
【问题描述】:

我正在尝试在 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?

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

【问题讨论】:

  • 您需要指明synonyms属于Word类。所以has_many :synonyms, class_name: Word, through: :synonym_pairs
  • @LesNightingill 仅当关联指向它指向的表上的一个关联时才有效。

标签: ruby-on-rails sqlite has-many-through self-join thesaurus


【解决方案1】:

您可以创建一个标准的 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.includes 并使用自定义查询加载记录并让 AR 使结果有意义并将关联视为已加载以避免 n+1 查询时,AR 并没有真正让您提供连接 sql问题可能非常棘手且耗时。有时您只需要围绕 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">]>

【讨论】:

  • SynonymPair 表已经是 M2M 连接表,为什么还要创建另一个?
  • @eikes 我更改了命名和内容,以使这里发生的事情更加明显。该表并没有真正作为成对表进行查询。而是每对有两行(每个方向一个)。因此,它是一个非常不同的解决方案。
  • 您需要将每对添加两次。不是最优的。这可能会导致一些非常严重的性能问题。作者具体说:“这个词库程序的一个关键方面是,一个词是在 word1 还是 word2 列中应该无关紧要;word1 是 word2 的同义词,反之亦然。”
  • @eikes 如答案中所写,是一种妥协。但是,替代方案缺乏即时加载将是一个更直接的性能问题。
【解决方案2】:

如果您真的想设置关联,其中记录可以位于连接表的任一列中,您需要一个 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 并将它们组合(通过转换为数组),但如果您需要对记录进行排序,这会带来问题。

【讨论】:

    【解决方案3】:

    您可能正在寻找scope 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
    

    【讨论】:

    • 作用域的问题是它们不是关系并且不能被预先加载。这可能会导致一些非常严重的性能问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多