SELECT * FROM X ONDE id IN (…) com Dapper ORM

231

Qual é a melhor maneira de escrever uma consulta com a cláusula IN usando o Dapper ORM quando a lista de valores da cláusula IN vem da lógica de negócios? Por exemplo, digamos que eu tenho uma consulta:

SELECT * 
  FROM SomeTable 
 WHERE id IN (commaSeparatedListOfIDs)

O commaSeparatedListOfIDsestá sendo transmitido da lógica de negócios e pode ser de qualquer tipo IEnumerable(of Integer). Como eu construiria uma consulta neste caso? Preciso fazer o que venho fazendo até agora, que é basicamente concatenação de strings ou existe algum tipo de técnica avançada de mapeamento de parâmetros que eu não conheço?

Marko
fonte

Respostas:

366

Dapper suporta isso diretamente. Por exemplo...

string sql = "SELECT * FROM SomeTable WHERE id IN @ids"
var results = conn.Query(sql, new { ids = new[] { 1, 2, 3, 4, 5 }});
LukeH
fonte
47
Acho importante notar que há um limite finito para quantos itens você pode enviar em sua matriz. Percebi isso da maneira mais difícil quando passei em muitos IDs. Não me lembro do número exato, mas da minha memória acho que são 200 elementos antes que o Dapper pare de trabalhar / executar a consulta.
Mark3
8
Marko, isso é importante. E, se você estiver fazendo dessa maneira, considere encontrar outra maneira de consultar seus dados, como fazer uma associação ou uma anti-associação, em vez de passar uma lista de IDs. A cláusula IN não é a consulta com melhor desempenho e geralmente pode ser substituída por uma cláusula existente, que será mais rápida.
Don Rolando
24
FYI - O SQL Server 2008 R2 tem um limite de 2100 entradas na INcláusula.
Jesse
6
E o SQLite tem um limite padrão de 999 variáveis.
Cameron
8
Cuidado: no SQL Server, isso falhará se você tiver vários itens em sua matriz e envolver o parâmetro entre colchetes. A remoção dos suportes corrigirá o problema.
ajbeaven
66

Diretamente da página inicial do projeto GitHub :

O Dapper permite que você passe IEnumerable e irá parametrizar automaticamente sua 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á traduzido para:

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
Fator Místico
fonte
43

Se sua INcláusula for muito grande para o MSSQL manipular, você poderá usar um TableValueParameter com Dapper com bastante facilidade.

  1. Crie seu tipo de TVP no MSSQL:

    CREATE TYPE [dbo].[MyTVP] AS TABLE([ProviderId] [int] NOT NULL)
  2. Crie um DataTablecom as mesmas colunas que o TVP e preencha-o com valores

    var tvpTable = new DataTable();
    tvpTable.Columns.Add(new DataColumn("ProviderId", typeof(int)));
    // fill the data table however you wish
  3. Modifique sua consulta Dapper para fazer um INNER JOINna tabela TVP:

    var query = @"SELECT * FROM Providers P
        INNER JOIN @tvp t ON p.ProviderId = t.ProviderId";
  4. Passe o DataTable na sua chamada de consulta do Dapper

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

Isso também funciona de maneira fantástica quando você deseja fazer uma atualização em massa de várias colunas - basta criar um TVP e fazer uma UPDATEjunção interna ao TVP.

Mr. T
fonte
Ótima solução, no entanto, não funciona no .Net Core, veja esta pergunta: stackoverflow.com/questions/41132350/… . Consulte também esta página: github.com/StackExchange/Dapper/issues/603
pcdev
3
Você também pode pensar ProviderIdem fazer no MyTVPbe PRIMARY KEY CLUSTERED, pois isso acabou de resolver um problema de desempenho para nós (os valores que estávamos passando não continham duplicatas).
Richardissimo
@ Richardhardissimo Você pode mostrar um exemplo de como fazer isso? Não consigo obter a sintaxe correta.
Mike Cole
14

Esta é possivelmente a maneira mais rápida de consultar um grande número de linhas com o Dapper usando uma lista de IDs. Eu prometo a você que isso é mais rápido do que quase qualquer outra maneira em que você possa pensar (com a possível exceção de usar um TVP como dado em outra resposta, e que eu não testei, mas suspeito que possa ser mais lento porque você ainda precisa preencher TVP). É planetas mais rápido que o Dapper usando INsintaxe e universos mais rápido que o Entity Framework linha por linha. E é até continentes mais rápido do que passar em uma lista de VALUESou UNION ALL SELECTitens. Ele pode ser facilmente estendido para usar uma chave de várias colunas, basta adicionar as colunas extras às DataTable, tabela temporária e condições de junção.

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

Esteja ciente de que você precisa aprender um pouco sobre as inserções em massa. Existem opções sobre acionar acionadores (o padrão é não), respeitar restrições, bloquear a tabela, permitir inserções simultâneas e assim por diante.

ErikE
fonte
Sim, concordo com a sua ideia geral de criar uma tabela temporária com IDs e, em seguida, ingressar internamente nessa tabela. Fizemos isso internamente e melhorou drasticamente o desempenho da consulta. Não tenho certeza se usaria a classe DataTable para qualquer coisa, mas sua solução é totalmente válida. Esta é uma maneira muito mais rápida.
Marko
o DataTable é necessária para a inserção em massa. Como você insere na tabela temporária 50.000 valores?
precisa saber é o seguinte
1
Em pedaços de 1000, se me lembro do limite corretamente? De qualquer forma eu não sabia que você pode ignorar o limite com DataTable assim que eu aprendi algo novo hoje ...
Marko
1
Essa é uma quantidade ridícula de trabalho a ser realizada quando você poderia usar um Parâmetro de Valor de Tabela. O Dapper suporta de maneira limpa a passagem de uma DataTable como TVP, o que permite dispensar a criação e destruição de uma tabela temporária, além de preencher essa tabela temporária via BulkCopy. Usamos a solução baseada em TVP rotineiramente nos casos em que o número de parâmetros para a cláusula IN seria muito grande.
Sr. T
3
Isso não é uma quantidade ridícula de trabalho, especialmente se alguém o abstrai um pouco com uma classe auxiliar ou método de extensão.
ErikE
11

Além disso, certifique-se de não colocar parênteses em torno da string de consulta da seguinte maneira:

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

Eu tive essa causa um erro de sintaxe SQL usando o Dapper 1.50.2, corrigido pela remoção de parênteses

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

Não é necessário adicionar ()a cláusula WHERE como fazemos em um SQL regular. Porque Dapper faz isso automaticamente para nós. Aqui está o syntax: -

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

var conditions = new { listOfIntegers };

var results = connection.Query(SQL, conditions);
Código absoluto
fonte
6

Exemplo para postgres:

string sql = "SELECT * FROM SomeTable WHERE id = ANY(@ids)"
var results = conn.Query(sql, new { ids = new[] { 1, 2, 3, 4, 5 }});
O que outras pessoas estão dizendo
fonte
3

No meu caso, eu usei isso:

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

minha variável "ids" na segunda linha é um IEnumerable de strings, também podem ser números inteiros, eu acho.

Cesar
fonte
List<string>?
Kiquenet
2

Na minha experiência, a maneira mais amigável de lidar com isso é ter uma função que converta uma string em uma tabela de valores.

Existem muitas funções divisoras disponíveis na web; você encontrará facilmente uma para qualquer que seja o seu sabor de SQL.

Você pode fazer ...

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 similar)

MatBailie
fonte