Comment retourner une relation ActiveRecord vide?

246

Si j'ai une portée avec un lambda et qu'il faut un argument, selon la valeur de l'argument, je sais peut-être qu'il n'y aura pas de correspondance, mais je veux toujours renvoyer une relation, pas un tableau vide:

scope :for_users, lambda { |users| users.any? ? where("user_id IN (?)", users.map(&:id).join(',')) : [] }

Ce que je veux vraiment, c'est une méthode "none", l'opposé de "all", qui renvoie une relation qui peut encore être chaînée, mais qui entraîne un court-circuit de la requête.

dzajic
la source
Si vous laissez simplement la requête s'exécuter, elle renverra une relation: User.where ('id in (?)', []). Class => ActiveRecord :: Relation. Essayez-vous d'éviter complètement la requête?
Brian Deterling
1
Correct. Si je sais qu'il ne peut pas y avoir de correspondance, l'idéal serait que la requête soit complètement évitée. J'ai simplement ajouté ceci à ActiveRecord :: Base: "def self.none; where (: id => 0); end" Semble fonctionner très bien pour ce dont j'ai besoin.
dzajic
1
> Essayez-vous d'éviter complètement la requête? serait totalement logique, un peu boiteux, nous devons frapper DB pour cela
dolzenko

Réponses:

478

Il existe désormais un mécanisme "correct" dans Rails 4:

>> Model.none 
=> #<ActiveRecord::Relation []>
steveh7
la source
14
Jusqu'à présent, cela n'a pas été porté sur 3.2 ou antérieur. Seul avantage (4.0)
Chris Bloom
2
Je viens d'essayer avec Rails 4.0.5 et ça fonctionne. Cette fonctionnalité est arrivée dans la version Rails 4.0.
Evolve le
3
@AugustinRiedinger Model.scopedfait ce que vous cherchez dans les rails 3.
Tim Diggins
9
Depuis Rails 4.0.5, Model.nonene fonctionne pas. Vous devez utiliser l'un de vos vrais noms de modèle, par exemple User.noneou ce que vous avez.
Grant Birchmeier
77

Une solution plus portable qui ne nécessite pas de colonne "id" et ne suppose pas qu'il n'y aura pas de ligne avec un id de 0:

scope :none, where("1 = 0")

Je cherche toujours une manière plus "correcte".

steveh7
la source
3
Oui, je suis vraiment surpris que ces réponses soient les meilleures que nous ayons. Je pense qu'ActiveRecord / Arel doit encore être assez immature. Si je devais passer par des perambulations pour créer un tableau vide dans Ruby, je serais vraiment ennuyé. La même chose ici, fondamentalement.
Purplejacket
Bien que quelque peu hack, c'est la bonne façon pour Rails 3.2. Pour Rails 4, voir l'autre réponse de @ steveh7 ici: stackoverflow.com/a/10001043/307308
scarver2
scope :none, -> { where("false") }
nroose
43

Venir dans Rails 4

Dans Rails 4, une chaîne ActiveRecord::NullRelationsera renvoyée à partir d'appels comme Post.none.

Ni elle, ni les méthodes chaînées, ne généreront de requêtes dans la base de données.

Selon les commentaires:

L'ActiveRecord :: NullRelation retourné hérite de Relation et implémente le modèle Null Object. Il s'agit d'un objet avec un comportement nul défini et renvoie toujours un tableau d'enregistrements vide sans interroger la base de données.

Voir le code source .

Nathan Long
la source
42

Vous pouvez ajouter une étendue appelée "aucune":

scope :none, where(:id => nil).where("id IS NOT ?", nil)

Cela vous donnera un ActiveRecord :: Relation vide

Vous pouvez également l'ajouter à ActiveRecord :: Base dans un initialiseur (si vous le souhaitez):

class ActiveRecord::Base
 def self.none
   where(arel_table[:id].eq(nil).and(arel_table[:id].not_eq(nil)))
 end
end

Beaucoup de façons d'obtenir quelque chose comme ça, mais certainement pas la meilleure chose à garder dans une base de code. J'ai utilisé la portée: aucune pour refactoriser et trouver que je dois garantir un ActiveRecord :: Relation vide pendant une courte période.

Brandon
la source
14
where('1=2')pourrait être un peu plus concis
Marcin Raczkowski
10
Si vous ne faites pas défiler la page jusqu'à la nouvelle réponse «correcte»: Model.nonecomment procéder?
Joe Essey
26
scope :none, limit(0)

Est une solution dangereuse car votre portée peut être enchaînée.

User.none.first

renverra le premier utilisateur. Il est plus sûr d'utiliser

scope :none, where('1 = 0')
bbrinck
la source
2
Celui-ci est le bon 'portée: aucune, où (' 1 = 0 ')'. l'autre échouera si vous avez la pagination
Federico
14

Je pense que je préfère la façon dont cela ressemble aux autres options:

scope :none, limit(0)

Menant à quelque chose comme ça:

scope :users, lambda { |ids| ids.present? ? where("user_id IN (?)", ids) : limit(0) }
Alex
la source
Je préfère celui-ci. Je ne sais pas pourquoi where(false)ne ferait pas le travail - cela laisse la portée inchangée.
aceofspades
4
Attention, cela limit(0)sera remplacé si vous appelez .firstou .lastplus tard dans la chaîne, car Rails s'ajoutera LIMIT 1à cette requête.
2012
2
@aceofspades où (false) ne fonctionne pas (Rails 3.0) mais où ('false') fonctionne. Pas que vous vous en souciez probablement maintenant, c'est 2013 :)
Ritchie
Merci @Ritchie, depuis, je pense que nous avons également la nonerelation mentionnée ci-dessous.
aceofspades
3

Utilisez la portée:

portée: for_users, lambda {| utilisateurs | users.any? ? où ("user_id IN (?)", users.map (&: id) .join (',')): portée}

Mais, vous pouvez également simplifier votre code avec:

portée: for_users, lambda {| utilisateurs | où (: user_id => users.map (&: id)) si users.any? }

Si vous voulez un résultat vide, utilisez ceci (supprimez la condition if):

portée: for_users, lambda {| utilisateurs | où (: id_utilisateur => utilisateurs.map (&: id))}
Pan Thomakos
la source
Renvoyer "scoped" ou nil n'atteint pas ce que je veux, qui est de limiter les résultats à zéro. Renvoyer "scoped" ou nil n'a aucun effet sur la portée (ce qui est utile dans certains cas, mais pas dans le mien). J'ai trouvé ma propre réponse (voir les commentaires ci-dessus).
dzajic
J'ai ajouté une solution simple pour que vous
renvoyiez également
1

Il existe également des variantes, mais toutes demandent à db

where('false')
where('null')
fmnoise
la source
2
BTW Notez que ce doivent être des chaînes. where(false)ou where(nil)est simplement ignoré.
mahemoff