Dans mon application Rails, j'ai rencontré plusieurs fois un problème que j'aimerais savoir comment les autres résolvent:
J'ai certains enregistrements où une valeur est facultative, donc certains enregistrements ont une valeur et certains sont nuls pour cette colonne.
Si je commande par cette colonne sur certaines bases de données, les nulls trient en premier et sur certaines bases de données, les nulls trient en dernier.
Par exemple, j'ai des photos qui peuvent appartenir ou non à une collection, c'est-à-dire qu'il y a des photos où collection_id=nil
Et d'autres où collection_id=1
Etc.
Si je fais Photo.order('collection_id desc)
alors sur SQLite j'obtiens les nuls en dernier mais sur PostgreSQL j'obtiens les nuls en premier.
Existe-t-il un moyen standard Rails pour gérer cela et obtenir des performances cohérentes dans n'importe quelle base de données?
Ajouter des tableaux ensemble préservera l'ordre:
@nonull = Photo.where("collection_id is not null").order("collection_id desc")
@yesnull = Photo.where("collection_id is null")
@wanted = @nonull+@yesnull
Je ne suis pas un expert en SQL, mais pourquoi ne pas simplement trier par si quelque chose est nul d'abord, puis trier par la façon dont vous vouliez le trier.
Photo.order('collection_id IS NULL, collection_id DESC') # Null's last
Photo.order('collection_id IS NOT NULL, collection_id DESC') # Null's first
Si vous utilisez uniquement PostgreSQL, vous pouvez également le faire
Photo.order('collection_id DESC NULLS LAST') #Null's Last
Photo.order('collection_id DESC NULLS FIRST') #Null's First
Si vous voulez quelque chose d'universel (comme si vous utilisez la même requête sur plusieurs bases de données, vous pouvez l'utiliser (gracieuseté de @philT)
Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
Même si c'est maintenant 2017, il n'y a toujours pas de consensus sur la question de savoir si NULL
s doit avoir la priorité. Sans que vous soyez explicite à ce sujet, vos résultats vont varier en fonction du SGBD.
La norme ne spécifie pas comment les NULL doivent être ordonnés par rapport aux valeurs non NULL, sauf que deux NULL doivent être considérés de manière égale et que les NULL doivent trier au-dessus ou au-dessous de toutes les valeurs non NULL.
Pour illustrer le problème, j'ai compilé une liste de quelques cas les plus populaires en matière de développement Rails:
NULL
s ont la valeur la plus élevée.
Par défaut, les valeurs nulles sont triées comme si elles étaient supérieures à toute valeur non nulle.
NULL
s ont la valeur la plus basse.
Lorsque vous effectuez un ORDER BY, les valeurs NULL sont présentées en premier si vous effectuez ORDER BY ... ASC et en dernier si vous effectuez ORDER BY ... DESC.
NULL
s ont la valeur la plus basse.
Une ligne avec une valeur NULL est supérieure aux lignes avec des valeurs régulières dans l'ordre croissant et elle est inversée pour l'ordre décroissant.
Malheureusement, Rails lui-même ne fournit pas encore de solution pour cela.
Pour PostgreSQL, vous pouvez utiliser de manière assez intuitive:
Photo.order('collection_id DESC NULLS LAST') # NULLs come last
Pour MySQL, vous pouvez mettre le signe moins à l'avance, mais cette fonctionnalité semble être non documentée. Semble fonctionner non seulement avec des valeurs numériques, mais aussi avec des dates.
Photo.order('-collection_id DESC') # NULLs come last
Pour couvrir les deux, cela semble fonctionner:
Photo.order('collection_id IS NULL, collection_id DESC') # NULLs come last
Pourtant, celui-ci ne fonctionne pas dans SQLite.
Pour fournir une prise en charge croisée de tous les SGBD, vous devez écrire une requête à l'aide de CASE
, déjà suggérée par @PhilIT:
Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
ce qui se traduit par un premier tri de chacun des enregistrements d'abord par CASE
résultats (par ordre croissant par défaut, ce qui signifie que NULL
les valeurs seront les dernières), ensuite par calculation_id
.
Mettez signe moins devant nom_colonne et inversez le sens de l'ordre. Cela fonctionne sur mysql. Plus de détails
Product.order('something_date ASC') # NULLS came first
Product.order('-something_date DESC') # NULLS came last
Photo.order('collection_id DESC NULLS LAST')
Je sais que c'est un ancien mais je viens de trouver cet extrait et cela fonctionne pour moi.
Un peu tard pour le spectacle, mais il existe un moyen générique SQL de le faire. Comme d'habitude, CASE
à la rescousse.
Photo.order('CASE WHEN collection_id IS NULL THEN 1 ELSE 0 END, collection_id')
Pour la postérité, je voulais souligner une erreur ActiveRecord relative à NULLS FIRST
.
Si vous essayez d'appeler:
Model.scope_with_nulls_first.last
Rails tentera d'appeler reverse_order.first
, et reverse_order
n'est pas compatible avec NULLS LAST
, car il essaie de générer le SQL non valide:
PG::SyntaxError: ERROR: syntax error at or near "DESC"
LINE 1: ...dents" ORDER BY table_column DESC NULLS LAST DESC LIMIT...
Cela a été référencé il y a quelques années dans certains problèmes encore ouverts Rails ( one , two , three J'ai pu contourner ce problème en procédant comme suit:
scope :nulls_first, -> { order("table_column IS NOT NULL") }
scope :meaningfully_ordered, -> { nulls_first.order("table_column ASC") }
Il semble qu'en chaînant les deux commandes ensemble, un SQL valide soit généré:
Model Load (12.0ms) SELECT "models".* FROM "models" ORDER BY table_column IS NULL DESC, table_column ASC LIMIT 1
Le seul inconvénient est que ce chaînage doit être fait pour chaque portée.
La façon la plus simple est d'utiliser:
.order('name nulls first')
Dans mon cas, j'avais besoin de trier les lignes par date de début et de fin par ASC, mais dans quelques cas, end_date était nul et que les lignes devraient être au-dessus, j'ai utilisé
@invoice.invoice_lines.order('start_date ASC, end_date ASC NULLS FIRST')