framework - requete linq to sql c#




Max ou par défaut? (12)

Depuis .Net 3.5, vous pouvez utiliser DefaultIfEmpty () en passant la valeur par défaut en argument. Quelque chose comme l'un des moyens suivants:

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 utilisé 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 la table Default Values .

Le SELECT résultant ne sera pas si élégant mais c'est acceptable.

J'espère que cela aide.

Quel est le meilleur moyen d'obtenir la valeur Max à partir 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

J'obtiens 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 si simple. Ai-je manqué une meilleure façon de le faire?

MISE À JOUR: Voici l'histoire arrière: J'essaie de récupérer le prochain compteur d'admissibilité à partir d'une table enfant (système hérité, ne me lance pas ...). La première ligne d'admissibilité pour chaque patient est toujours 1, la seconde est 2, etc. (évidemment ce n'est pas la clé primaire de la table enfant). Donc, je sélectionne la valeur de compteur max existante pour un patient, puis en ajoutant 1 pour créer une nouvelle ligne. Quand il n'y a pas de valeur enfant existante, j'ai besoin de la requête pour retourner 0 (donc l'addition de 1 me donnera une valeur de 1). Notez que je ne veux pas compter sur le nombre brut de lignes enfants, dans le cas où l'application existante introduit des écarts dans les valeurs du compteur (possible). Mon mauvais pour essayer de rendre la question trop générique.


J'ai assommé une méthode d'extension MaxOrDefault . Il n'y a pas grand-chose 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 named:

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

J'ai juste eu un problème similaire, mes tests unitaires ont passé avec Max () mais ont échoué lors de l'exécution sur une base de données en direct.

Ma solution était de séparer la requête de la logique en cours, ne pas les joindre dans une requête.
J'avais besoin d'une solution pour travailler dans des tests unitaires en utilisant des objets Linq (dans Linq-objects Max () avec des zéros) et Linq-sql lors de l'exécution dans un environnement live.

(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 la prochaine fois? Nan.


Je pense que le problème est ce que vous voulez arriver quand 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érerais l'exception générée par la requête standard. Si la requête ne renvoie aucun résultat, vous devez déterminer ce que vous voulez que le résultat soit dans ce cas. Il se peut que la réponse de David @ (ou quelque chose de similaire fonctionne). C'est-à-dire que si le MAX est toujours positif, il peut suffire d'insérer une "mauvaise" valeur connue dans la liste qui ne sera sélectionnée que s'il n'y a pas de résultats. Généralement, je m'attendrais à une requête qui récupère un maximum pour avoir des données sur lesquelles travailler et j'irais sur la route try / catch, sinon vous serez 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

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, c'est une question vide de sens. Le maximum de l'ensemble vide n'est simplement pas défini. Tenter 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 il est correct que l'ordinateur lance une exception lorsque le programmeur lui demande de diviser par zéro, il est donc correct que l'ordinateur lance une exception lorsque le programmeur lui demande de prendre le maximum de l'ensemble vide. La division par zéro, en prenant le maximum de l'ensemble vide, en remuant le spacklerorke, et en chevauchant la licorne volante à Neverland sont toutes insignifiantes, impossibles, indéfinies.

Maintenant, qu'est-ce que vous voulez vraiment faire?


Pour Entity Framework et Linq to SQL, nous pouvons y parvenir en définissant une méthode d'extension qui modifie une 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'y a pas d'enregistrements, elle renvoie une valeur par défaut de type T au lieu de lancer une exception


Puisque DefaultIfEmpty n'est pas implémenté dans LINQ to SQL, j'ai fait une recherche sur l'erreur retournée et j'ai trouvé un article fascinant qui traite des ensembles nuls dans les fonctions agrégées. Pour résumer ce que j'ai trouvé, vous pouvez contourner cette limitation en transformant en nullable dans votre sélection. Mon VB est un peu rouillé, mais je pense que ça irait quelque chose comme ça:

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();

Une autre possibilité serait le regroupement, similaire à la façon dont vous pourriez l'aborder 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 (tester à nouveau dans LINQPad) passer à la saveur VB 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 sais pas comment le refléter dans VB.

Le SQL généré serait quelque chose du genre:

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é semble icky, comme si l'exécution de la requête récupérait toutes les lignes puis sélectionnait celle de l'ensemble récupéré ... 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 maîtrise pas bien l'interprétation des plans d'exécution dans SQL Server, mais il semble que lorsque la clause WHERE est sur le SELECT externe, le nombre de lignes réelles résultant de cette étape est 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% du coût est transféré à l'étape suivante lorsque toutes les lignes sont considérées, et de toute façon, une seule ligne revient de SQL Server, alors peut-être que ce n'est pas une grosse différence .


Vous pouvez toujours ajouter Double.MinValue à la séquence. Cela assurerait qu'il y ait au moins un élément et Max le renverrait seulement si c'était le 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();

peu tard, mais j'avais la même inquiétude ...

En reformulant votre code du message original, vous voulez que le maximum de l'ensemble S soit défini par

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

Prise en compte de ton dernier commentaire

Il suffit de dire que je sais que je veux 0 quand 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: Vous voulez le 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();

decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;

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

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







linq-to-sql