web-dev-qa-db-fra.com

Max ou par défaut?

Quel est le meilleur moyen d'obtenir la valeur Max d'une requête LINQ qui peut ne renvoyer aucune ligne? Si je fais juste

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).Max

Je reçois une erreur lorsque la requête ne renvoie aucune ligne. je pourrais faire

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter _
         Order By MyCounter Descending).FirstOrDefault

mais cela semble un peu obtus pour une demande aussi simple. Est-ce que je manque une meilleure façon de le faire?

UPDATE: Voici l'historique: J'essaie de récupérer le prochain compteur d'éligibilité à partir d'une table enfant (système hérité, ne me démarrez pas ...). La première ligne d'éligibilité pour chaque patient est toujours 1, la seconde 2, etc. (évidemment, il ne s'agit pas de la clé primaire de la table enfant). Je sélectionne donc la valeur de compteur maximale existante pour un patient, puis lui ajoute 1 pour créer une nouvelle ligne. Quand il n'y a aucune valeur enfant existante, j'ai besoin que la requête renvoie 0 (donc ajouter 1 me donnera une valeur de compteur de 1). Notez que je ne veux pas compter sur le nombre brut de lignes enfants, au cas où l'application héritée introduit des espaces dans les valeurs de compteur (possible). Mon mauvais pour essayer de rendre la question trop générique.

168
gfrizzle

Puisque DefaultIfEmpty n'est pas implémenté dans LINQ to SQL, j'ai effectué une recherche sur l'erreur renvoyée et trouvé un article fascinant qui traite des ensembles nuls dans les fonctions d'agrégat. Pour résumer ce que j'ai trouvé, vous pouvez contourner cette limitation en définissant un nullable dans votre sélection. Mon VB est un peu rouillé, mais je pense que cela ressemblerait à ceci:

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select CType(y.MyCounter, Integer?)).Max

Ou en C #:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();
203
Jacob Proffitt

Je viens d'avoir un problème similaire, mais j'utilisais des méthodes d'extension LINQ sur une liste plutôt que la syntaxe de requête. Le casting à un tour Nullable fonctionne aussi dans ce cas:

int max = list.Max(i => (int?)i.MyCounter) ?? 0;
95
Eddie Deyo

Cela ressemble à un cas pour DefaultIfEmpty (un code non testé suit):

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max
47
Jacob Proffitt

Pensez à ce que vous demandez!

Le maximum de {1, 2, 3, -1, -2, -3} est évidemment 3. Le maximum de {2} est évidemment 2. Mais quel est le maximum de l'ensemble vide {}? De toute évidence, cette question n'a pas de sens. Le maximum de l'ensemble vide n'est simplement pas défini. Essayer d'obtenir une réponse est une erreur mathématique. Le maximum de tout ensemble doit lui-même être un élément de cet ensemble. L'ensemble vide n'a pas d'éléments, donc prétendre qu'un certain nombre est le maximum de cet ensemble sans être dans cet ensemble est une contradiction mathématique.

Tout comme le comportement correct pour l'ordinateur de déclencher une exception lorsque le programmeur lui demande de diviser par zéro, le comportement correct pour l'ordinateur de déclencher une exception lorsque le programmeur lui demande de prendre le maximum de l'ensemble vide. Division par zéro, en prenant le maximum de l'ensemble vide, en agitant le spacklerorke et en pilotant la Licorne volante vers Neverland ne veulent rien dire, sont impossibles, indéfinis.

Maintenant, qu'est-ce que vous réellement voulez faire?

35
yfeldblum

Vous pouvez toujours ajouter Double.MinValue À la séquence. Cela garantirait qu’il existe au moins un élément et que Max ne le renvoie que s’il s’agit du minimum. Pour déterminer quelle option est la plus efficace (Concat, FirstOrDefault ou Take(1)), vous devez effectuer une analyse comparative adéquate.

double x = context.MyTable
    .Where(y => y.MyField == value)
    .Select(y => y.MyCounter)
    .Concat(new double[]{Double.MinValue})
    .Max();
24
David Schmitt

Depuis .Net 3.5, vous pouvez utiliser DefaultIfEmpty () en transmettant la valeur par défaut en tant qu’argument. Quelque chose comme l'une des manières suivantes:

int max = (from e in context.Table where e.Year == year select e.RecordNumber).DefaultIfEmpty(0).Max();
DateTime maxDate = (from e in context.Table where e.Year == year select e.StartDate ?? DateTime.MinValue).DefaultIfEmpty(DateTime.MinValue).Max();

Le premier est autorisé lorsque vous interrogez une colonne NOT NULL et le second est la façon dont un l'a utilisée pour interroger une colonne NULLABLE. Si vous utilisez DefaultIfEmpty () sans arguments, la valeur par défaut sera celle définie pour le type de votre sortie, comme vous pouvez le voir dans le Tableau des valeurs par défaut .

Le résultat obtenu ne sera pas aussi élégant mais acceptable.

J'espère que ça aide.

10
Fernando Brustolin
int max = list.Any() ? list.Max(i => i.MyCounter) : 0;

Si la liste contient des éléments (c'est-à-dire non vides), le maximum du champ MyCounter sera pris, sinon 0 sera renvoyé.

9
beastieboy

Je pense que le problème est de savoir ce que vous voulez faire lorsque la requête n'a aucun résultat. S'il s'agit d'un cas exceptionnel, je placerais la requête dans un bloc try/catch et gérer l'exception générée par la requête standard. Si vous ne voulez pas que la requête ne renvoie aucun résultat, vous devez déterminer le résultat souhaité. Il se peut que la réponse de @ David (ou quelque chose de similaire fonctionne). Autrement dit, si le MAX sera toujours positif, il suffira peut-être d'insérer une "mauvaise" valeur connue dans la liste qui ne sera sélectionnée que s'il n'y a aucun résultat. Généralement, je m'attendrais à ce qu'une requête qui récupère un maximum ait des données sur lesquelles travailler et j'irais par la route try/catch, sinon vous êtes toujours obligé de vérifier si la valeur que vous avez obtenue est correcte ou non. Je préférerais que le cas non exceptionnel soit juste capable d'utiliser la valeur obtenue.

Try
   Dim x = (From y In context.MyTable _
            Where y.MyField = value _
            Select y.MyCounter).Max
   ... continue working with x ...
Catch ex As SqlException
       ... do error processing ...
End Try
7
tvanfosson

Une autre possibilité serait le regroupement, similaire à la façon dont vous pourriez l’approcher en SQL brut:

from y in context.MyTable
group y.MyCounter by y.MyField into GrpByMyField
where GrpByMyField.Key == value
select GrpByMyField.Max()

La seule chose est (teste à nouveau dans LINQPad) de basculer vers la version VB) La syntaxe LINQ donne des erreurs de syntaxe sur la clause de regroupement. Je suis sûr que l’équivalent conceptuel est assez facile à trouver, je ne Je ne sais pas comment le refléter en VB.

Le SQL généré ressemblerait à quelque chose comme:

SELECT [t1].[MaxValue]
FROM (
    SELECT MAX([t0].[MyCounter) AS [MaxValue], [t0].[MyField]
    FROM [MyTable] AS [t0]
    GROUP BY [t0].[MyField]
    ) AS [t1]
WHERE [t1].[MyField] = @p0

Le SELECT imbriqué a l'air bizarre, comme si la requête récupérait toutes les lignes, puis sélectionnait celle correspondante dans le jeu extrait ... la question est de savoir si SQL Server optimise la requête en quelque chose de comparable à l'application de la clause where au SELECT interne. Je regarde ça maintenant ...

Je ne suis pas habitué à interpréter les plans d'exécution dans SQL Server, mais il semble que lorsque la clause WHERE figure sur le SELECT externe, le nombre de lignes qui en résultent correspond à toutes les lignes de la table, par opposition aux lignes correspondantes. lorsque la clause WHERE est sur le SELECT interne. Cela dit, il semble que seulement 1% des coûts soient transférés à l'étape suivante lorsque toutes les lignes sont prises en compte. De toute façon, une seule ligne revient jamais de SQL Server. Ce n'est donc peut-être pas une si grande différence dans le grand schéma des choses. .

6
Rex Miller

litt tard, mais j'avais le même souci ...

En reformulant votre code à partir de la publication d’origine, vous souhaitez que le maximum de l’ensemble S défini par

(From y In context.MyTable _
 Where y.MyField = value _
 Select y.MyCounter)

Prise en compte de votre dernier commentaire

Autant dire que je sais que je veux 0 lorsqu'il n'y a pas d'enregistrements à sélectionner, ce qui a certainement un impact sur la solution éventuelle

Je peux reformuler votre problème en disant: Vous voulez un maximum de {0 + S}. Et il semble que la solution proposée avec concat soit sémantiquement la bonne :-)

var max = new[]{0}
          .Concat((From y In context.MyTable _
                   Where y.MyField = value _
                   Select y.MyCounter))
          .Max();
6
Dom Ribaut

Pourquoi pas quelque chose de plus direct comme:

Dim x = context.MyTable.Max(Function(DataItem) DataItem.MyField = Value)
3
legal

Pour que tout le monde sache que pour utiliser Linq to Entities, les méthodes ci-dessus ne fonctionneront pas ...

Si vous essayez de faire quelque chose comme

var max = new[]{0}
      .Concat((From y In context.MyTable _
               Where y.MyField = value _
               Select y.MyCounter))
      .Max();

Il va lancer une exception:

System.NotSupportedException: le type de noeud d'expression LINQ 'NewArrayInit' n'est pas pris en charge dans LINQ to Entities.

Je suggérerais juste de faire

(From y In context.MyTable _
                   Where y.MyField = value _
                   Select y.MyCounter))
          .OrderByDescending(x=>x).FirstOrDefault());

Et le FirstOrDefault retournera 0 si votre liste est vide.

2
Nix
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
1
jong su.

Une différence intéressante qui semble intéressante est que FirstOrDefault et Take (1) génèrent le même code SQL (selon LINQPad, de toute façon), FirstOrDefault renvoie une valeur - la valeur par défaut - lorsqu'il n'y a pas de lignes correspondantes et que Take (1) renvoie aucun résultat ... au moins dans LINQPad.

1
Rex Miller

J'ai trouvé une méthode d'extension MaxOrDefault. Il n’ya pas grand-chose à faire, mais sa présence dans Intellisense est un rappel utile que Max sur une séquence vide provoquera une exception. De plus, la méthode permet de spécifier la valeur par défaut si nécessaire.

    public static TResult MaxOrDefault<TSource, TResult>(this 
    IQueryable<TSource> source, Expression<Func<TSource, TResult?>> selector,
    TResult defaultValue = default (TResult)) where TResult : struct
    {
        return source.Max(selector) ?? defaultValue;
    }

Utilisation, sur une colonne ou une propriété de type int nommée Id:

    sequence.DefaultOrMax(s => (int?)s.Id);
0
Stephen Kennedy

Je viens d'avoir un problème similaire, mes tests unitaires ont réussi à utiliser Max () mais ont échoué lors de l'exécution sur une base de données active.

Ma solution consistait à séparer la requête de la logique en cours d'exécution, et non à les joindre dans une requête.
J'avais besoin d'une solution pour pouvoir effectuer des tests unitaires avec Linq-objects (dans Linq-objects, Max () fonctionne avec des valeurs NULL) et Linq-sql lors de l'exécution dans un environnement réel.

(Je me moque du Select () dans mes tests)

var requiredDataQuery = _dataRepo.Select(x => new { x.NullableDate1, .NullableDate2 }); 
var requiredData.ToList();
var maxDate1 = dates.Max(x => x.NullableDate1);
var maxDate2 = dates.Max(x => x.NullableDate2);

Moins efficace? Probablement.

Est-ce que je m'en soucie tant que mon application ne tombe pas sur la prochaine fois? Nan.

0
Seb

Pour Entity Framework et Linq to SQL, nous pouvons y arriver en définissant une méthode d’extension qui modifie une méthode Expression passée à la méthode IQueryable<T>.Max(...):

static class Extensions
{
    public static TResult MaxOrDefault<T, TResult>(this IQueryable<T> source, 
                                                   Expression<Func<T, TResult>> selector)
        where TResult : struct
    {
        UnaryExpression castedBody = Expression.Convert(selector.Body, typeof(TResult?));
        Expression<Func<T, TResult?>> lambda = Expression.Lambda<Func<T,TResult?>>(castedBody, selector.Parameters);
        return source.Max(lambda) ?? default(TResult);
    }
}

Usage:

int maxId = dbContextInstance.Employees.MaxOrDefault(employee => employee.Id);
// maxId is equal to 0 if there is no records in Employees table

La requête générée est identique, elle fonctionne comme un appel normal à la méthode IQueryable<T>.Max(...), mais s’il n’ya pas d’enregistrement, elle renvoie une valeur par défaut de type T au lieu de lancer une exception.

0
Ashot Muradian