SELECCIONE * DESDE X DONDE ENTRADA (...) con Dapper ORM

231

¿Cuál es la mejor manera de escribir una consulta con cláusula IN utilizando Dapper ORM cuando la lista de valores para la cláusula IN proviene de la lógica empresarial? Por ejemplo, digamos que tengo una consulta:

SELECT * 
  FROM SomeTable 
 WHERE id IN (commaSeparatedListOfIDs)

El commaSeparatedListOfIDsse pasa de la lógica de negocios y puede ser cualquier tipo de IEnumerable(of Integer). ¿Cómo construiría una consulta en este caso? ¿Tengo que hacer lo que he estado haciendo hasta ahora, que es básicamente la concatenación de cadenas o hay algún tipo de técnica avanzada de mapeo de parámetros que no conozco?

Marko
fuente

Respuestas:

366

Dapper apoya esto directamente. Por ejemplo...

string sql = "SELECT * FROM SomeTable WHERE id IN @ids"
var results = conn.Query(sql, new { ids = new[] { 1, 2, 3, 4, 5 }});
LukeH
fuente
47
Creo que es importante tener en cuenta que hay un límite finito a la cantidad de elementos que puede enviar en su matriz. Me di cuenta de esto de la manera difícil cuando pasé demasiados identificadores. No recuerdo el número exacto, pero de mi memoria creo que son 200 elementos antes de que Dapper deje de funcionar / ejecutar la consulta.
Marko
8
Marko, eso es importante. Y, si lo está haciendo de esa manera, podría considerar buscar otra forma de consultar sus datos, como hacer una unión o una anti-unión en lugar de pasar una lista de identificadores. La cláusula IN no es la consulta de mayor rendimiento y, a menudo, se puede reemplazar por una cláusula existente, que será más rápida.
Don Rolling
24
FYI: SQL Server 2008 R2 tiene un límite de 2100 entradas en la INcláusula.
Jesse
66
Y SQLite tiene un límite predeterminado de 999 variables.
Cameron
8
Cuidado: en SQL Server esto falla si tiene varios elementos en su matriz y ajusta el parámetro entre paréntesis. Quitar los corchetes solucionará el problema.
ajbeaven
66

Directamente desde la página de inicio del proyecto GitHub :

Dapper le permite pasar IEnumerable y parametrizará automáticamente su consulta.

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

Será traducido a:

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
Factor místico
fuente
43

Si su INcláusula es demasiado grande para que la maneje MSSQL, puede usar un TableValueParameter con Dapper con bastante facilidad.

  1. Cree su tipo de TVP en MSSQL:

    CREATE TYPE [dbo].[MyTVP] AS TABLE([ProviderId] [int] NOT NULL)
  2. Cree un DataTablecon las mismas columnas que el TVP y complételo con valores

    var tvpTable = new DataTable();
    tvpTable.Columns.Add(new DataColumn("ProviderId", typeof(int)));
    // fill the data table however you wish
  3. Modifique su consulta Dapper para hacer un INNER JOINen la tabla TVP:

    var query = @"SELECT * FROM Providers P
        INNER JOIN @tvp t ON p.ProviderId = t.ProviderId";
  4. Pase la DataTable en su llamada de consulta Dapper

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

Esto también funciona fantásticamente cuando desea hacer una actualización masiva de varias columnas: simplemente construya un TVP y realice una UPDATEunión interna al TVP.

Señor t
fuente
Gran solución, sin embargo, no funciona en .Net Core, vea esta pregunta: stackoverflow.com/questions/41132350/… . También vea esta página: github.com/StackExchange/Dapper/issues/603
pcdev
3
También es posible que desee considerar hacer ProviderIdon MyTVPbe PRIMARY KEY CLUSTERED, ya que esto solo resolvió un problema de rendimiento para nosotros (los valores que pasábamos no contenían duplicados).
Richardissimo
@ Richardissimo ¿Puedes mostrar un ejemplo de cómo hacerlo? Parece que no puedo obtener la sintaxis correcta.
Mike Cole el
14

Esta es posiblemente la forma más rápida de consultar una gran cantidad de filas con Dapper utilizando una lista de ID. Te prometo que esto es más rápido que casi cualquier otra forma en que puedas pensar (con la posible excepción de usar un TVP como se indica en otra respuesta, y que no he probado, pero sospecho que puede ser más lento porque todavía tienes que llenar El TVP). Es planetas más rápido que Dapper usando INsintaxis y universos más rápido que Entity Framework fila por fila. Y es aún más rápido de lo que pasa continentes en una lista de VALUESo UNION ALL SELECTartículos. Se puede ampliar fácilmente para usar una clave de varias columnas, solo agregue las columnas adicionales a la DataTabletabla temporal y las condiciones de unión.

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;
   }
}

Tenga en cuenta que necesita aprender un poco sobre las inserciones masivas. Hay opciones para disparar disparadores (el valor predeterminado es no), respetar las restricciones, bloquear la tabla, permitir inserciones concurrentes, etc.

ErikE
fuente
Sí, estoy de acuerdo con su idea general de crear una tabla temporal con Ids y luego una unión interna en esa tabla. Lo hemos hecho internamente y ha mejorado drásticamente el rendimiento de la consulta. No estoy seguro de usar la clase DataTable para nada, pero su solución es totalmente válida. Esta es una forma mucho más rápida.
Marko
Se DataTablerequiere para el inserto a granel. ¿Cómo se inserta a la tabla temporal 50.000 valores?
ErikE
1
¿En trozos de 1000 si recuerdo el límite correctamente? De todos modos, no sabía que puedes pasar el límite con DataTable, así que aprendí algo nuevo hoy ...
Marko
1
Es una cantidad ridícula de trabajo a la que recurrir cuando podría usar un parámetro de valor de tabla. Dapper admite limpiamente pasar una DataTable como TVP, lo que le permite prescindir de la creación y destrucción de una tabla temporal, así como completar esa tabla temporal a través de BulkCopy. Usamos la solución basada en TVP de forma rutinaria en casos en que el número de parámetros para la cláusula IN sería demasiado.
Sr. T
3
Esta no es una cantidad ridícula de trabajo, especialmente si se abstrae un poco con una clase auxiliar o un método de extensión.
ErikE
11

También asegúrese de no colocar paréntesis alrededor de su cadena de consulta de la siguiente manera:

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

Esto causó un error de sintaxis SQL al usar Dapper 1.50.2, solucionado eliminando paréntesis

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

No es necesario agregar ()la cláusula WHERE como lo hacemos en un SQL normal. Porque Dapper hace eso automáticamente por nosotros. Aquí está el syntax: -

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

var conditions = new { listOfIntegers };

var results = connection.Query(SQL, conditions);
Codificador absoluto
fuente
6

Ejemplo para postgres:

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

En mi caso, he usado esto:

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

mis "identificadores" variables en la segunda línea son un IEnumerable de cadenas, también pueden ser enteros, supongo.

Cesar
fuente
List<string>?
Kiquenet
2

En mi experiencia, la forma más amigable de lidiar con esto es tener una función que convierta una cadena en una tabla de valores.

Hay muchas funciones de división disponibles en la web, encontrará fácilmente una para lo que sea, si su sabor de SQL.

Entonces puedes hacer ...

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

O

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

(O similar)

MatBailie
fuente