0
votes

Rails 5 ActiveRecord facultatif inclus où pour l'attribut d'association imbriquée

En supposant ce schéma simplifié:

users has_many discount_codes

discount_codes has_many orders

Je veux saisir tous les users , et s'ils ont des orders , n'incluez que les commandes qui ont été créées entre deux dates. Mais s'ils n'ont pas de commandes, ou s'ils ont des commandes uniquement en dehors de ces deux dates, renvoyez toujours les utilisateurs et n'excluez aucun utilisateur.

Ce que je fais maintenant:

users = User.all.includes(discount_codes: :orders)

users = users.where("orders.created_at BETWEEN ? AND ?", date1, date2).
or(users.where(orders: { id: nil })

Je pense que ma clause OR me permet de fidéliser les utilisateurs qui n'ont aucune commande, mais ce qui se passe, c'est que si j'ai un utilisateur qui n'a que des commandes en dehors de date1 et date2, ma requête exclura cet utilisateur.

Pour ce que ça vaut, je veux utiliser cette clause orders where ici spécifiquement afin que je puisse éviter n + 1 problèmes plus tard dans la détermination des commandes par utilisateur.

Merci d'avance!


6 commentaires

Qu'est-ce qui vous empêche de simplement définir une méthode orders_in_timespan dans votre modèle User et d'y faire orders.where(created_at: date1..date2) ?


@ClemensKofler rien du tout, mais peut-être qu'il me manque quelque chose, mais je ne vois pas comment cela résout le problème. J'ai besoin d'un moyen de m'assurer que les utilisateurs ne sont pas exclus; ajouter simplement une portée de niveau utilisateur comme celle-ci exclurait de la requête tous les utilisateurs dont les commandes ne remplissent pas la condition created_at


D'après ce que je comprends, vous voulez que tous les utilisateurs - et pour les utilisateurs ayant des commandes dans un laps de temps donné, vous voulez également que ces commandes => récupérer tous les utilisateurs, les parcourir et pour ceux qui ont des commandes dans le laps de temps, utilisez les commandes comme vous voir bon. Voici ce que je veux dire dans le code: gist.github.com/clemens/62c324f1b60adc5ce10094b56c1c58d9 . Vous effectuez essentiellement le rendu de tous les utilisateurs et s'ils ont des commandes qui correspondent à vos critères, vous faites quelque chose avec eux (dans mon exemple, "quelque chose" est simplement en train de les rendre).


Je peux voir pourquoi cela fonctionnerait, mais il a quelques problèmes qui le rendent intenable pour mon cas d'utilisation: 1. J'essaye de sérialiser les données en json et 2. Idéalement, faites-le en une requête. Ce que j'essaie d'accomplir n'est tout simplement pas possible avec ActiveRecord en une seule requête?


En réalité, vous rendez 2 types de ressources qui ont une relation un-à-plusieurs. La façon de faire cela avec une seule requête serait une OUTER JOIN mais cela inclut alors des données en double (car chaque utilisateur est répété pour chaque commande). C'est aussi pourquoi Rails a généralement changé pour avoir 2 requêtes dans de tels cas (1 pour les utilisateurs, puis 1 pour toutes les commandes pertinentes).


users = users.where ("orders.created_at BETWEEN? AND?", date1, date2) + users.group ("orders.id"). having ('COUNT ("orders.id") = 0')


3 Réponses :


0
votes

Cela n'a aucun sens d'essayer de contrôler les commandes chargées dans le cadre de la clause where pour les utilisateurs. Si vous deviez contrôler que cela devrait faire partie des includes (ce qui, je pense, signifie qu'il devrait faire partie de l'association).

Bien que techniquement, il puisse les combiner en une seule requête dans certains cas, activerecord va le faire en deux requêtes.
La première requête sera exécutée lorsque vous passerez à itérer sur les utilisateurs et utilisera cette clause where pour limiter les utilisateurs trouvés.
Il exécutera ensuite une deuxième requête dans les coulisses en fonction de cette instruction include. Ce sera simplement une requête pour obtenir toutes les commandes associées aux utilisateurs trouvés par la requête précédente. En tant que tel, le seul moyen de contrôler les commandes trouvées via la clause where de l'utilisateur est d'omettre les utilisateurs de l'ensemble de résultats.

Si j'étais vous, je créerais une méthode d'instance dans le modèle utilisateur pour ce que vous recherchez, mais au lieu d'utiliser où, utilisez un bloc de sélection:

def orders_in_timespan(start, end)
    orders.select{ |o| o.between?(start, end) }
end

En raison de la façon dont ActiveRecord mettra en cache les commandes trouvées à partir des inclusions par rapport à l'instance, si vous commencez avec une inclusion dans votre requête d'utilisateurs, je pense que cela ne donnera pas n requêtes.

Quelque chose comme: render json: User.includes(:orders), methods: :orders_in_timespan

Bien entendu, le moyen le plus simple de confirmer le nombre de requêtes est de consulter les journaux. Je pense que cette approche devrait avoir deux requêtes quel que soit le nombre d'utilisateurs rendus (comme le fait probablement votre code dans la question).

De plus, je ne sais pas à quel point vous êtes familier avec sql, mais vous pouvez appeler .to_sql à la fin de choses telles que la variable de vos utilisateurs afin de voir le sql qui serait généré, ce qui pourrait aider à faire la lumière sur les écarts entre ce qui vous obtenez et ce que vous recherchez.


2 commentaires

Je conviens que la clause where appartient probablement à la jointure à la place si je veux suivre cette voie. À quoi cela ressemblerait-il, la RA fournit-elle un moyen d'avoir une condition sur la jointure? Pour votre solution, je suis déjà en train de faire quelque chose comme ça, le problème avec le déplacement d'une requête au niveau de la base de données vers une requête basée sur ruby est qu'elle commence à consommer un peu de mémoire avec mon ensemble de données. Existe-t-il un moyen de tout garder en AR ou en SQL?


Eh bien, vous pouvez fournir des conditions sur une association afin que vous puissiez faire une association sur le modèle utilisateur pour les commandes à prix réduit. Ensuite, tant que vous ne faites pas quelque chose comme une limite dans la mesure où vous pourriez probablement simplement inclure cette association lors de la création de votre requête d'origine User.includes(:discounted_orders) . Vous pouvez également tirer parti de la méthode find_each car vous pouvez l'utiliser pour ne charger qu'une fraction des utilisateurs à la fois. Bien sûr, techniquement, cela entraînera plus de requêtes, mais si le nombre d'utilisateurs est grand, vous pouvez trouver un bon compromis mémoire.



0
votes

Vous devez utiliser des jointures au lieu des inclusions. Les joins Rails utilisent des joins internes et rejetteront tous les enregistrements qui n'ont pas d'associations.

User.joins(discount_codes: :orders).where(orders: {created_at: [10.days.ago..1.day.ago]}).distinct

Cela vous donnera tous les utilisateurs distincts qui ont passé des commandes dans une période de temps donnée.


1 commentaires

Nous souhaitons fidéliser les utilisateurs qui n'ont pas de commandes (c'est-à-dire ne jamais exclure aucun utilisateur)



0
votes

Option 1: écrivez une requête personnalisée en SQL (moche).

Option 2: Créez 2 requêtes distinctes comme ci-dessous ...

@users.each do |user|
  orders = @orders[user.id]
  puts user.name
  puts user.id
  puts orders.count
end

Maintenant, vous pouvez l'utiliser comme ça ...

@users = User.limit(10)
@orders = Order.joins(:discount_code)
  .where(created_at: [10.days.ago..1.day.ago], discount_codes: {user_id: users.select(:id)})
  .group_by{|order| order.discount_code.user_id}

J'espère que cela résoudra votre problème.


0 commentaires