ВЫБЕРИТЕ * ОТ X ГДЕ id IN (…) с помощью Dapper ORM

231

Каков наилучший способ написать запрос с предложением IN, используя Dapper ORM, если список значений для предложения IN исходит из бизнес-логики? Например, скажем, у меня есть запрос:

SELECT * 
  FROM SomeTable 
 WHERE id IN (commaSeparatedListOfIDs)

Это commaSeparatedListOfIDsпередается из бизнес-логики, и это может быть любой тип IEnumerable(of Integer). Как бы я построить запрос в этом случае? Нужно ли мне делать то, что я делал до сих пор, это в основном конкатенация строк или есть какая-то продвинутая техника отображения параметров, о которой я не знаю?

Marko
источник

Ответы:

366

Dapper поддерживает это напрямую. Например...

string sql = "SELECT * FROM SomeTable WHERE id IN @ids"
var results = conn.Query(sql, new { ids = new[] { 1, 2, 3, 4, 5 }});
LukeH
источник
47
Я думаю, что важно отметить, что существует ограниченное количество отправляемых элементов в вашем массиве. Я понял это трудным путем, когда я прошел слишком много идентификаторов. Я не помню точное число, но из своей памяти я думаю, что это 200 элементов, прежде чем Даппер перестанет работать / выполнять запрос.
Марко
8
Марко, это важно. И, если вы делаете это таким образом, вы можете подумать о том, чтобы найти другой способ запроса данных, например, выполнить объединение или анти-объединение, а не передавать список идентификаторов. Предложение IN не является наиболее высокоэффективным запросом и часто может быть заменено существующим предложением, которое будет быстрее.
Дон Роллинг
24
К вашему сведению - SQL Server 2008 R2 имеет ограничение в 2100 записей в INпредложении.
Джесси
6
И SQLite имеет ограничение по умолчанию 999 переменных.
Кэмерон
8
Осторожно: в SQL Server это не работает, если в вашем массиве несколько элементов, а параметр заключен в квадратные скобки. Снятие скобок решит проблему.
ajbeaven
66

Непосредственно с домашней страницы проекта GitHub :

Dapper позволит вам передать в IEnumerable и будет автоматически параметризовать ваш запрос.

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

Будет переведено на:

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
Фактор Мистик
источник
43

Если ваше INпредложение слишком велико для MSSQL для обработки, вы можете довольно легко использовать TableValueParameter с Dapper.

  1. Создайте свой тип TVP в MSSQL:

    CREATE TYPE [dbo].[MyTVP] AS TABLE([ProviderId] [int] NOT NULL)
  2. Создайте DataTableстолбец с теми же столбцами, что и TVP, и заполните его значениями.

    var tvpTable = new DataTable();
    tvpTable.Columns.Add(new DataColumn("ProviderId", typeof(int)));
    // fill the data table however you wish
  3. Измените ваш Dapper запрос, чтобы сделать его INNER JOINв таблице TVP:

    var query = @"SELECT * FROM Providers P
        INNER JOIN @tvp t ON p.ProviderId = t.ProviderId";
  4. Передайте DataTable в вашем вызове Dapper

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

Это также работает фантастически, когда вы хотите выполнить массовое обновление нескольких столбцов - просто создайте TVP и выполните UPDATEвнутреннее соединение с TVP.

Мистер Т
источник
Отличное решение, однако не работает на .Net Core, см. Этот вопрос: stackoverflow.com/questions/41132350/… . Также смотрите эту страницу: github.com/StackExchange/Dapper/issues/603
pcdev
3
Вы также можете подумать ProviderIdо MyTVPтом PRIMARY KEY CLUSTERED, чтобы сделать это , поскольку это просто решило проблему производительности для нас (передаваемые нами значения не содержали дубликатов).
Ричардиссимо
@Richardissimo Можете ли вы показать пример того, как это сделать? Я не могу получить правильный синтаксис.
Майк Коул
14

Вот, пожалуй, самый быстрый способ запросить большое количество строк с помощью Dapper, используя список идентификаторов. Я обещаю вам, что это быстрее, чем любой другой способ, которым вы можете придумать (за возможным исключением использования TVP, как указано в другом ответе, и которое я не проверял, но я подозреваю, что может быть медленнее, потому что вам все еще нужно заполнить ТВП). Это планеты быстрее, чем Dapper с использованием INсинтаксиса, и юниверсы быстрее, чем Entity Framework строка за строкой. И это даже континенты быстрее, чем передать список VALUESили UNION ALL SELECTэлементы. Его можно легко расширить, используя ключ из нескольких столбцов, просто добавив дополнительные столбцы в DataTableтаблицу temp и условия соединения.

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

Имейте в виду, что вам нужно немного узнать о массовых вставках. Существуют варианты запуска триггеров (по умолчанию - нет), соблюдения ограничений, блокировки таблицы, разрешения одновременных вставок и т. Д.

ErikE
источник
Да, я согласен с вашей общей идеей создания временной таблицы с идентификаторами и последующим внутренним объединением этой таблицы. Мы сделали это внутренне, и это значительно улучшило производительность запросов. Я не уверен, что использовал бы класс DataTable для чего-либо, но ваше решение полностью допустимо. Это гораздо более быстрый способ.
Марко
DataTableТребуется для массовой вставки. Как вы вставить в таблицу темп 50000 значений?
ErikE
1
В кусках 1000, если я правильно помню предел? Во всяком случае, я не знал, что вы можете обойти ограничение с DataTable, поэтому я узнал что-то новое сегодня ...
Марко
1
Это нелепый объем работы, когда вместо этого можно использовать параметр табличного значения. Dapper поддерживает передачу DataTable в виде TVP, что позволяет вам отказаться от создания и уничтожения временной таблицы, а также заполнить эту временную таблицу с помощью BulkCopy. Мы обычно используем решение на основе TVP в тех случаях, когда число параметров для предложения IN будет слишком большим.
Мистер Т
3
Это не смешная работа, особенно если ее немного абстрагировать с помощью вспомогательного класса или метода расширения.
ErikE
11

Также убедитесь, что вы не заключаете скобки в строку запроса следующим образом:

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

У меня была эта причина ошибки синтаксиса SQL с использованием Dapper 1.50.2, исправлена ​​путем удаления скобок

SELECT Name from [USER] WHERE [UserId] in @ids
Брайан Огден
источник
7

Это не нужно , чтобы добавить ()в предложении WHERE , как мы делаем в обычном SQL. Потому что Даппер делает это автоматически для нас. Вот это syntax: -

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

var conditions = new { listOfIntegers };

var results = connection.Query(SQL, conditions);
Кодер Абсолют
источник
6

Пример для postgres:

string sql = "SELECT * FROM SomeTable WHERE id = ANY(@ids)"
var results = conn.Query(sql, new { ids = new[] { 1, 2, 3, 4, 5 }});
SanŚÝŚ
источник
3

В моем случае я использовал это:

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

моя переменная "идентификаторы" во второй строке представляет собой IEnumerable из строк, также, я думаю, они могут быть целыми числами.

Cesar
источник
List<string>?
Kiquenet
2

По моему опыту, наиболее дружественный способ справиться с этим - иметь функцию, которая преобразует строку в таблицу значений.

В Интернете доступно множество функций разветвителя, и вы легко найдете их для любого вида SQL.

Затем вы можете сделать ...

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

Или

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

(Или похожие)

MatBailie
источник