1
votes

Éviter n + 1 has_many enfants devraient se souvenir de leurs parents

Lorsque vous avez une relation parent-enfant:

class Parent < ActiveRecord::Base
  has_many :children
end

class Child < ActiveRecord::Base
  belongs_to :parent
end

> parent = parent.find(2)
  Parent Load (0.6ms)  SELECT  `parents`.* FROM `parents` WHERE `parents`.`id` = 2 LIMIT 1
> children = parent.children
  Child Load (1.4ms)  SELECT  `children`.* FROM `children` WHERE `children`.`parent_id` = 2
> children.to_a
  Child Load (0.8ms)  SELECT `children`.* FROM `children` WHERE `children`.`parent_id` = 2
> children.loaded?
 => true
> children.first.parent
  Parent Load (0.7ms)  SELECT  `parents`.* FROM `parents` WHERE `parents`.`id` = 2 LIMIT 1

Cette dernière ligne est ce qui me tue. Pourquoi frappe-t-il la base de données du parent? On dirait qu'il devrait s'en souvenir puisque l'enfant a été chargé via le parent?


5 commentaires

Je pense que c'est ainsi que cela fonctionne. Vous pouvez essayer inverse_of et voir si cela vous aide.


@MarlinPierce Je pensais que je me souvenais d'un moyen de faire ça! Merci!


Etes-vous sûr que dans votre cas, c'est le parent pour lequel la dernière ligne atteint la base de données? Jetez un œil au SQL. Va-t-il chercher le parent ou récupère-t-il les enfants? Ce que vous décrivez ne ressemble pas au comportement par défaut de Rails. Le parent doit être mis en cache dans votre cas.


Il atteint définitivement la base de données sur children.first.parent. L'objet enfant ne connaît pas le parent sans ce paramètre inverse_of.


@TomRossi Dans votre cas, l'enfant doit connaître le parent sans aucun paramètre inverse_of . C'est le comportement par défaut. Je comprends que children.first.parent frappe la base de données. Ma question était: était-ce vraiment le parent pour lequel la DB a été frappée? Veuillez vérifier la requête SQL réelle


3 Réponses :


2
votes

La solution est l'utilisation de inverse_of :

class Parent < ActiveRecord::Base
  has_many :children, inverse_of: :parent
end

class Child < ActiveRecord::Base
  belongs_to :parent, inverse_of: :children
end

> parent = Parent.find(foo)
# Fetches the parent
> children = parent.children
# Fetches all children

> children.first.parent   
# No longer fetches the parent again


1 commentaires

Cela devrait fonctionner sans l'option inverse_of . Il s'agit du comportement par défaut de Rails 5.



1
votes

L'association appartient_to a une option appelée inverse_of , qui, si elle est utilisée, établit une association bidirectionnelle entre les modèles, explicitement, comme décrit dans le Documentation de l'API pour appartient_to . Le fonctionnement des associations bidirectionnelles peut être exploré plus avant dans ce document sur l'API .
Fondamentalement, si le modèle Child avait l’association écrite comme appartient_to: parent, inverse_of:: parent , la requête supplémentaire n’aurait pas été effectuée.
Consultez ce blog pour plus de détails sur le fonctionnement de inverse_of . Il fournit une très bonne explication avec des exemples.


1 commentaires

Cela devrait fonctionner sans l'option inverse_of . Il s'agit du comportement par défaut de Rails 5.



0
votes

À partir de la version 4.1, Rails détecte automatiquement l'inverse d'une association.

Voir les notes de version https : //guides.rubyonrails.org/4_1_release_notes.html

Dans votre cas, ce n'est pas le parent pour lequel la dernière ligne a atteint la base de données. La requête SQL réelle a récupéré les enfants car lorsque vous accédez à l'association dans la console Rails et que vous n'utilisez pas réellement le résultat, elle n'est pas mise en cache.

parent = Parent.find(foo)
# Fetches the parent

children = parent.children
# Fetches all children

children.first.parent   
# Fetches all children again, does not fetch parent as it is automatically inversed

child = children.first

# will not fetch the parent
child.parent 

Donc, dans votre cas, ce sont les enfants pour lesquels la requête DB a été effectuée.

parent = Parent.find(foo)
# Fetches the parent:
# Parent Load (0.3ms)  SELECT  "parents".* FROM "parents" WHERE ...

children = parent.children
# Fetches all children:
# Child Load (1.0ms)  SELECT  "children".* FROM "children" WHERE ...

# the association has not been cached
children.loaded? # => false

# Will fetch children again and again ...

children
# Fetches all children:
# Child Load (1.0ms)  SELECT  "children".* FROM "children" WHERE ...

children
# Fetches all children:
# Child Load (1.0ms)  SELECT  "children".* FROM "children" WHERE ...

# until you really use them:
children.to_a
children.loaded? # => true

children
# Does not hit the database now


1 commentaires

Merci pour cela! Malheureusement, je ne le vois pas dans ma console Rails 5.2. J'ai mis à jour ma question avec les résultats et je me suis assuré que la collection est chargée.