1
votes

Conversion d'une requête SQL en requête de relations personnalisées dans Rails

J'essaie de créer une application de thésaurus simple dans Rails, dans laquelle un mot dans une table de mots serait dans une relation auto-jointe à plusieurs mots dans la table, via une table de synonyme de jointure.

Ma classe SynonymPair est construite comme suit:

[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?

Un aspect crucial de ce programme de thésaurus est que peu importe si un mot est dans la colonne mot1 ou mot2; word1 est un synonyme de word2, et vice versa.

Pour que ma classe Words renvoie les SynonymPairs et les Synonymes d'un mot donné, j'ai écrit une requête SQL:

p>

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

Ce code fonctionne comme prévu. Cependant, il ne tire pas parti des modèles d'association dans ActiveRecord. Donc, je me demandais qu'il serait possible d'écrire une requête de relation personnalisée has_many: synonymes_pairs / has_many: synonym via:: synonym-pairs dans la classe Words, plutôt que d'écrire une requête SQL entière, comme je l'ai fait ci-dessus. En d'autres termes, je suis curieux de savoir s'il est possible de convertir ma requête SQL en une requête de relations personnalisées Rails.

Remarque, j'ai essayé la requête de relations personnalisées suivante:

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

Mais, après avoir passé quelques graines Word / SynonymPair, il a renvoyé un 'ActiveRecord: Associations: CollectionProxy' lorsque j'ai essayé d'obtenir que j'appelle mot # synonym_pairs et l'erreur suivante lorsque j'ai appelé mot # synonymes: p>

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

Avez-vous d'autres idées pour obtenir une requête de relation personnalisée, ou toute sorte de modèle d'auto-jointure fonctionnant ici?


2 commentaires

Vous devez indiquer que les synonymes appartiennent à la classe Word. Donc has_many: synonymes, class_name: Word, through:: synonym_pairs


@LesNightingill qui ne fonctionnerait que si l'association pointe vers une seule association sur la table vers laquelle elle pointe.


3 Réponses :


0
votes

Vous recherchez probablement la scope Méthode de classe 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


1 commentaires

Le problème avec les étendues est qu'elles ne sont pas des relations et qu'elles ne peuvent pas être chargées rapidement. Cela peut entraîner des problèmes de performances assez graves sur toute la ligne.



0
votes

Au lieu d'une table de paires de synonymes, vous pouvez simplement créer une table de jointure M2M standard:

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">]>
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
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

Pendant cette solution nécessiterait deux fois plus de lignes dans la table de jointure, cela pourrait valoir la peine de faire un compromis car traiter des relations où les clés étrangères ne sont pas fixées est un cauchemar dans ActiveRecord. Cela fonctionne.

AR ne vous permet pas vraiment de fournir la jointure sql lorsque vous utilisez .eager_load et .includes et que vous chargez des enregistrements avec une requête personnalisée et faire en sorte que l'AR ait un sens si les résultats et traiter les associations comme chargées pour éviter les problèmes de requête n + 1 peut être extrêmement piraté et prendre du temps. Parfois, vous devez simplement construire votre schéma autour de la RA plutôt que d'essayer de le battre en soumission.

Vous définiriez une relation synonyme entre deux mots avec:

class Synonymity 
  belongs_to :word
  belongs_to :synonym, class_name: 'Word'
end

XXX


4 commentaires

La table SynonymPair est déjà la table de jointure M2M, pourquoi en créer une autre?


@eikes J'ai changé la dénomination et les choses pour rendre plus clair ce qui se passe ici. La table n'est pas vraiment interrogée comme une table de paires. Il a plutôt deux lignes pour chaque paire (une pour chaque direction). C'est donc une solution très différente.


Vous devrez alors ajouter chaque paire deux fois. Pas optimal. Cela peut entraîner des problèmes de performances assez graves sur toute la ligne. L'auteur a spécifiquement déclaré: "Un aspect crucial de ce programme de thésaurus est qu'il ne devrait pas être important qu'un mot soit dans la colonne mot1 ou mot2; mot1 est un synonyme de mot2, et vice versa."


@eikes comme écrit dans la réponse, c'est un compromis. Mais le manque de chargement avec les alternatives sera un problème de performance beaucoup plus immédiat.



0
votes

Si vous voulez vraiment configurer des associations où l'enregistrement peut être dans l'une ou l'autre des colonnes de la table de jointure, vous avez besoin d'une association has_many et d'une association indirecte pour chaque clé étrangère potentielle.

Continuez avec moi ici car cela devient vraiment fou:

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

Puisque les synonymes ici ne sont toujours pas vraiment une association, vous avez toujours un problème potentiel de requête n + 1 si vous chargez une liste de mots et leurs synonymes.

Bien que vous puissiez charger avec impatience word_1_synonymes et word_2_synonymes et les combiner (en les transformant en tableaux), cela pose un problème si vous devez commander les enregistrements.


0 commentaires