SELECT * FROM X WHERE id IN (…) avec Dapper ORM

231

Quelle est la meilleure façon d'écrire une requête avec la clause IN à l'aide de Dapper ORM lorsque la liste des valeurs de la clause IN provient de la logique métier? Par exemple, disons que j'ai une requête:

SELECT * 
  FROM SomeTable 
 WHERE id IN (commaSeparatedListOfIDs)

Le commaSeparatedListOfIDsest transmis à partir de la logique métier et il peut s'agir de n'importe quel type de IEnumerable(of Integer). Comment pourrais-je construire une requête dans ce cas? Dois-je faire ce que j'ai fait jusqu'à présent, qui est essentiellement une concaténation de chaînes ou existe-t-il une sorte de technique avancée de mappage de paramètres que je ne connais pas?

Marko
la source

Réponses:

366

Dapper le prend en charge directement. Par exemple...

string sql = "SELECT * FROM SomeTable WHERE id IN @ids"
var results = conn.Query(sql, new { ids = new[] { 1, 2, 3, 4, 5 }});
LukeH
la source
47
Je pense qu'il est important de noter qu'il y a une limite finie au nombre d'articles que vous pouvez envoyer dans votre tableau. Je m'en suis rendu compte à la dure quand je suis passé dans trop d'identifiants. Je ne me souviens pas du nombre exact mais de ma mémoire, je pense que c'est 200 éléments avant que Dapper arrête de travailler / d'exécuter la requête.
Marko
8
Marko, c'est important. Et, si vous le faites de cette façon, vous pourriez envisager de trouver une autre façon d'interroger vos données, comme faire une jointure ou une anti-jointure plutôt que de passer une liste d'ID. La clause IN n'est pas la requête la plus performante et peut souvent être remplacée par une clause existe, qui sera plus rapide.
Don Rolling
24
FYI - SQL Server 2008 R2 a une limite de 2100 entrées sur la INclause.
Jesse
6
Et SQLite a une limite par défaut de 999 variables.
Cameron
8
Attention: dans SQL Server, cela échoue si vous avez plusieurs éléments dans votre tableau et si vous enveloppez le paramètre entre crochets. La suppression des supports résoudra le problème.
ajbeaven
66

Directement depuis la page d'accueil du projet GitHub :

Dapper vous permet de passer IEnumerable et paramètrera automatiquement votre requête.

connection.Query<int>(
    @"select * 
      from (select 1 as Id union all select 2 union all select 3) as X 
      where Id in @Ids", 
    new { Ids = new int[] { 1, 2, 3 });

Sera traduit en:

select * 
from (select 1 as Id union all select 2 union all select 3) as X 
where Id in (@Ids1, @Ids2, @Ids3)

// @Ids1 = 1 , @Ids2 = 2 , @Ids2 = 3
Facteur mystique
la source
43

Si votre INclause est trop volumineuse pour être traitée par MSSQL, vous pouvez utiliser un TableValueParameter avec Dapper assez facilement.

  1. Créez votre type TVP dans MSSQL:

    CREATE TYPE [dbo].[MyTVP] AS TABLE([ProviderId] [int] NOT NULL)
  2. Créez un DataTableavec la même colonne (s) que le TVP et remplissez-le avec des valeurs

    var tvpTable = new DataTable();
    tvpTable.Columns.Add(new DataColumn("ProviderId", typeof(int)));
    // fill the data table however you wish
  3. Modifiez votre requête Dapper pour faire une INNER JOINsur la table TVP:

    var query = @"SELECT * FROM Providers P
        INNER JOIN @tvp t ON p.ProviderId = t.ProviderId";
  4. Passer le DataTable dans votre appel de requête Dapper

    sqlConn.Query(query, new {tvp = tvpTable.AsTableValuedParameter("dbo.MyTVP")});

Cela fonctionne également de manière fantastique lorsque vous souhaitez effectuer une mise à jour en masse de plusieurs colonnes - créez simplement un TVP et effectuez une UPDATEavec une jointure interne au TVP.

Mr. T
la source
Une excellente solution, cependant, ne fonctionne pas sur .Net Core, voir cette question: stackoverflow.com/questions/41132350/… . Voir aussi cette page: github.com/StackExchange/Dapper/issues/603
pcdev
3
Vous pouvez également envisager de faire ProviderIdle MyTVPêtre PRIMARY KEY CLUSTERED, comme cela vient de résoudre un problème de performance pour nous (les valeurs que nous passions devant CONTENUES pas de doublons).
Richardissimo
@Richardissimo Pouvez-vous montrer un exemple de la façon de procéder? Je n'arrive pas à obtenir la syntaxe correcte.
Mike Cole
14

Voici probablement le moyen le plus rapide d'interroger un grand nombre de lignes avec Dapper à l'aide d'une liste d'ID. Je vous promets que c'est plus rapide que presque toutes les autres façons auxquelles vous pouvez penser (à l'exception possible de l'utilisation d'un TVP comme indiqué dans une autre réponse, et que je n'ai pas testé, mais je soupçonne qu'il peut être plus lent car vous devez encore remplir le TVP). Il s'agit de planètes plus rapides que Dapper utilisant la INsyntaxe et les univers plus rapidement que Entity Framework ligne par ligne. Et c'est même des continents plus rapides que de passer dans une liste de VALUESou des UNION ALL SELECTéléments. Il peut facilement être étendu pour utiliser une clé multi-colonnes, il suffit d'ajouter les colonnes supplémentaires à la DataTable, la table temporaire et les conditions de jointure.

public IReadOnlyCollection<Item> GetItemsByItemIds(IEnumerable<int> items) {
   var itemList = new HashSet(items);
   if (itemList.Count == 0) { return Enumerable.Empty<Item>().ToList().AsReadOnly(); }

   var itemDataTable = new DataTable();
   itemDataTable.Columns.Add("ItemId", typeof(int));
   itemList.ForEach(itemid => itemDataTable.Rows.Add(itemid));

   using (SqlConnection conn = GetConnection()) // however you get a connection
   using (var transaction = conn.BeginTransaction()) {
      conn.Execute(
         "CREATE TABLE #Items (ItemId int NOT NULL PRIMARY KEY CLUSTERED);",
         transaction: transaction
      );

      new SqlBulkCopy(conn, SqlBulkCopyOptions.Default, transaction) {
         DestinationTableName = "#Items",
         BulkCopyTimeout = 3600 // ridiculously large
      }
         .WriteToServer(itemDataTable);
      var result = conn
         .Query<Item>(@"
            SELECT i.ItemId, i.ItemName
            FROM #Items x INNER JOIN dbo.Items i ON x.ItemId = i.ItemId
            DROP TABLE #Items;",
            transaction: transaction,
            commandTimeout: 3600
         )
         .ToList()
         .AsReadOnly();
      transaction.Rollback(); // Or commit if you like
      return result;
   }
}

Sachez que vous devez en apprendre un peu plus sur les inserts en vrac. Il existe des options concernant le déclenchement des déclencheurs (la valeur par défaut est no), le respect des contraintes, le verrouillage de la table, l'autorisation des insertions simultanées, etc.

ErikE
la source
Oui, je suis d'accord avec votre idée générale de créer une table temporaire avec des identifiants, puis de se joindre à l'intérieur sur cette table. Nous l'avons fait en interne et cela a considérablement amélioré les performances des requêtes. Je ne suis pas sûr que j'utiliserais la classe DataTable pour quoi que ce soit, mais votre solution est totalement valide. C'est un moyen beaucoup plus rapide.
Marko
Le DataTableest requis pour l'insert en vrac. Comment insérez- vous dans la table temporaire 50 000 valeurs?
ErikE
1
En morceaux de 1000 si je me souviens bien de la limite? Quoi qu'il en soit, je ne savais pas que vous pouvez contourner la limite avec DataTable, j'ai donc appris quelque chose de nouveau aujourd'hui ...
Marko
1
C'est un travail ridicule à faire quand vous pouvez utiliser un paramètre de valeur de table à la place. Dapper prend en charge proprement le passage d'un DataTable en tant que TVP, ce qui vous permet de vous dispenser de la création et de la destruction d'une table temporaire ainsi que de remplir cette table temporaire via BulkCopy. Nous utilisons régulièrement la solution basée sur TVP dans les cas où le nombre de paramètres pour la clause IN serait trop élevé.
Monsieur T
3
Ce n'est pas une quantité ridicule de travail, surtout si on l'abstrait un peu avec une classe d'assistance ou une méthode d'extension.
ErikE
11

Assurez-vous également de ne pas placer de parenthèses autour de votre chaîne de requête comme ceci:

SELECT Name from [USER] WHERE [UserId] in (@ids)

J'ai eu cette cause une erreur de syntaxe SQL en utilisant Dapper 1.50.2, corrigée en supprimant les parenthèses

SELECT Name from [USER] WHERE [UserId] in @ids
Brian Ogden
la source
7

Il n'est pas nécessaire d'ajouter ()la clause WHERE comme nous le faisons dans un SQL standard. Parce que Dapper le fait automatiquement pour nous. Voici le syntax: -

const string SQL = "SELECT IntegerColumn, StringColumn FROM SomeTable WHERE IntegerColumn IN @listOfIntegers";

var conditions = new { listOfIntegers };

var results = connection.Query(SQL, conditions);
Coder Absolute
la source
6

Exemple pour postgres:

string sql = "SELECT * FROM SomeTable WHERE id = ANY(@ids)"
var results = conn.Query(sql, new { ids = new[] { 1, 2, 3, 4, 5 }});
SanŚ́́́́́́́́Ś́́́́́
la source
3

Dans mon cas, j'ai utilisé ceci:

var query = "select * from table where Id IN @Ids";
var result = conn.Query<MyEntity>(query, new { Ids = ids });

ma variable "ids" dans la deuxième ligne est un IEnumerable de chaînes, elles peuvent aussi être des entiers je suppose.

César
la source
List<string>?
Kiquenet
2

D'après mon expérience, la façon la plus conviviale de gérer cela est d'avoir une fonction qui convertit une chaîne en une table de valeurs.

Il existe de nombreuses fonctions de séparation disponibles sur le Web, vous en trouverez facilement une pour votre goût de SQL.

Vous pouvez alors faire ...

SELECT * FROM table WHERE id IN (SELECT id FROM split(@list_of_ids))

Ou

SELECT * FROM table INNER JOIN (SELECT id FROM split(@list_of_ids)) AS list ON list.id = table.id

(Ou similaire)

MatBailie
la source