Conversion d'un tableau d'objets en ActiveRecord :: Relation

104

J'ai un tableau d'objets, appelons-le un Indicator. Je veux exécuter des méthodes de classe Indicator (celles de la def self.subjectsvariété, des portées, etc.) sur ce tableau. La seule façon que je connais d'exécuter des méthodes de classe sur un groupe d'objets est de les avoir comme ActiveRecord :: Relation. Alors je to_indicatorsfinis par avoir recours à l'ajout d'une méthode à Array.

def to_indicators
  # TODO: Make this less terrible.
  Indicator.where id: self.pluck(:id)
end

Parfois, j'enchaine plusieurs de ces portées pour filtrer les résultats, dans les méthodes de classe. Donc, même si j'appelle une méthode sur un ActiveRecord :: Relation, je ne sais pas comment accéder à cet objet. Je ne peux en obtenir le contenu qu'à travers all. Mais allest un tableau. Alors je dois convertir ce tableau en un ActiveRecord :: Relation. Par exemple, cela fait partie de l'une des méthodes:

all.to_indicators.applicable_for_bank(id).each do |indicator|
  total += indicator.residual_risk_for(id)
  indicator_count += 1 if indicator.completed_by?(id)
end

Je suppose que cela se résume à deux questions.

  1. Comment puis-je convertir un tableau d'objets en ActiveRecord :: Relation? De préférence sans en faire à wherechaque fois.
  2. Lors de l'exécution d'une def self.subjectsméthode de type sur un ActiveRecord :: Relation, comment accéder à cet objet ActiveRecord :: Relation lui-même?

Merci. Si j'ai besoin de clarifier quelque chose, faites-le moi savoir.

Nathan
la source
3
Si votre seule raison d'essayer de reconvertir ce tableau en une relation est parce que vous l'avez obtenu via .all, utilisez simplement .scopedcomme l'indique la réponse d'Andrew Marshall (bien que dans les rails 4, cela fonctionne avec .all). Si vous avez besoin de transformer un tableau en une relation, vous vous êtes trompé quelque part ...
nzifnab

Réponses:

46

Comment puis-je convertir un tableau d'objets en ActiveRecord :: Relation? De préférence sans faire un où à chaque fois.

Vous ne pouvez pas convertir un Array en ActiveRecord :: Relation car une Relation est juste un générateur pour une requête SQL et ses méthodes ne fonctionnent pas sur des données réelles.

Cependant, si vous voulez une relation, alors:

  • pour ActiveRecord 3.x, n'appelez pas allet appelez à la place scoped, ce qui rendra une Relation qui représente les mêmes enregistrements que ceux allqui vous donneraient dans un tableau.

  • pour ActiveRecord 4.x, appelez simplement all, ce qui renvoie une relation.

Lors de l'exécution d'une def self.subjectsméthode de type sur un ActiveRecord :: Relation, comment accéder à cet objet ActiveRecord :: Relation lui-même?

Lorsque la méthode est appelée sur un objet Relation, selfest la relation (par opposition à la classe de modèle dans laquelle elle est définie).

Andrew Marshall
la source
1
Voir @Marco Prins ci-dessous à propos de la solution.
Justin
qu'en est-il de la méthode obsolète .push dans les rails 5
Jaswinder
@GstjiSaini Je ne suis pas sûr de la méthode exacte à laquelle vous faites référence, veuillez fournir un document ou un lien source. Cependant, si elle est obsolète, ce n'est pas une solution très viable car elle disparaîtra probablement bientôt.
Andrew Marshall
Et c'est pourquoi vous pouvez le faire class User; def self.somewhere; where(location: "somewhere"); end; end, alorsUser.limit(5).somewhere
Dorian
C'est la seule réponse qui éclaire vraiment le PO. La question révèle une connaissance erronée du fonctionnement d'ActiveRecord. @Justin ne fait que renforcer un manque de compréhension sur les raisons pour lesquelles il est mauvais de continuer à passer des tableaux d'objets juste pour les mapper et créer une autre requête inutile.
james2m
162

Vous pouvez convertir un tableau d'objets arren ActiveRecord :: Relation comme ceci (en supposant que vous sachiez de quelle classe sont les objets, ce que vous faites probablement)

MyModel.where(id: arr.map(&:id))

Vous devez cependant l'utiliser where, c'est un outil utile que vous ne devriez pas hésiter à utiliser. Et maintenant, vous avez une ligne unique convertissant un tableau en une relation.

map(&:id)transformera votre tableau d'objets en un tableau contenant uniquement leurs identifiants. Et passer un tableau à une clause where générera une instruction SQL avec INqui ressemble à quelque chose comme:

SELECT .... WHERE `my_models`.id IN (2, 3, 4, 6, ....

Gardez à l'esprit que l'ordre du tableau sera perdu - Mais comme votre objectif est uniquement d'exécuter une méthode de classe sur la collection de ces objets, je suppose que ce ne sera pas un problème.

Marco Prins
la source
3
Bravo, exactement ce dont j'avais besoin. Vous pouvez l'utiliser pour n'importe quel attribut du modèle. fonctionne parfaitement pour moi.
nfriend21
7
Pourquoi créer vous-même du SQL littéral lorsque vous pouvez le faire where(id: arr.map(&:id))? Et à proprement parler, cela ne convertit pas le tableau en une relation, mais obtient à la place de nouvelles instances des objets (une fois la relation réalisée) avec ces ID qui peuvent avoir des valeurs d'attribut différentes de celles des instances déjà en mémoire dans le tableau.
Andrew Marshall
7
Cela perd la commande, cependant.
Velizar Hristov
1
@VelizarHristov C'est parce que c'est maintenant une relation, qui ne peut être ordonnée que par une colonne, et pas comme vous le souhaitez. Les relations sont plus rapides pour gérer de grands ensembles de données, et il y aura des compromis.
Marco Prins
8
Très inefficace! Vous avez transformé une collection d'objets que vous avez déjà en mémoire en ceux auxquels vous allez effectuer une requête de base de données pour accéder. Je voudrais refactoriser les méthodes de classe que vous souhaitez itérer sur le tableau.
james2m
5

Eh bien, dans mon cas, j'ai besoin de convertir un tableau d'objets en ActiveRecord :: Relation ainsi que de les trier avec une colonne spécifique (id par exemple). Puisque j'utilise MySQL, la fonction de champ pourrait être utile.

MyModel.where('id in (?)',ids).order("field(id,#{ids.join(",")})") 

Le SQL ressemble à:

SELECT ... FROM ... WHERE (id in (11,5,6,7,8,9,10))  
ORDER BY field(id,11,5,6,7,8,9,10)

Fonction de champ MySQL

Xingcheng Xia
la source
Pour une version de ceci qui fonctionne avec PostgreSQL, ne cherchez pas plus loin que ce fil: stackoverflow.com/questions/1309624/…
armchairdj
0

ActiveRecord::Relation lie la requête de base de données qui récupère les données de la base de données.

Supposons que cela ait du sens, nous avons un tableau avec des objets de même classe, alors avec quelle requête nous supposons les lier?

Quand je cours,

users = User.where(id: [1,3,4,5])
  User Load (0.6ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 3, 4, 5)  ORDER BY created_at desc

Ici ci-dessus, usersretournez l' Relationobjet mais lie la requête de base de données derrière lui et vous pouvez l'afficher,

users.to_sql
 => "SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 3, 4, 5)  ORDER BY created_at desc"

Il n'est donc pas possible de retourner à ActiveRecord::Relationpartir d'un tableau d'objets indépendant de la requête SQL.

rayon
la source
0

Tout d'abord, ce n'est PAS une solution miracle. D'après mon expérience, j'ai trouvé que la conversion à la relation est parfois plus facile que les alternatives. J'essaie d'utiliser cette approche avec parcimonie et uniquement dans les cas où l'alternative serait plus complexe.

Cela étant dit voici ma solution, j'ai étendu la Arrayclasse

# lib/core_ext/array.rb

class Array

  def to_activerecord_relation
    return ApplicationRecord.none if self.empty?

    clazzes = self.collect(&:class).uniq
    raise 'Array cannot be converted to ActiveRecord::Relation since it does not have same elements' if clazzes.size > 1

    clazz = clazzes.first
    raise 'Element class is not ApplicationRecord and as such cannot be converted' unless clazz.ancestors.include? ApplicationRecord

    clazz.where(id: self.collect(&:id))
  end
end

Un exemple d'utilisation serait array.to_activerecord_relation.update_all(status: 'finished'). Maintenant où est-ce que je l'utilise?

Parfois, vous devez filtrer, ActiveRecord::Relationpar exemple retirer les éléments non terminés. Dans ces cas, le mieux est d'utiliser la portée elements.not_finishedet vous conserverez toujours ActiveRecord::Relation.

Mais parfois, cette condition est plus complexe. Retirez tous les éléments qui ne sont pas finis, qui ont été produits au cours des 4 dernières semaines et qui ont été inspectés. Pour éviter de créer de nouvelles étendues, vous pouvez filtrer en tableau, puis reconvertir. Gardez à l'esprit que vous faites toujours une requête à DB, rapide car il recherche par idmais toujours une requête.

Haris Krajina
la source