Dapper — поиск элементов в списке, которых нет в таблице

#c# #sql-server #dapper

#c# #sql-сервер #dapper

Вопрос:

У меня есть список идентификаторов (динамически генерируемых), и я хочу найти, какие из них не существуют в таблице.

Я использую Dapper и Microsoft SQL Server.

Dapper позволяет передавать параметры IEnumerable в запросы и использовать их в качестве переменных, что приятно, но я не могу заставить его работать для сценария выбора объектов из этого списка.

Вот мой код, показывающий две вещи, которые я пытался (обе из которых не работают), и попытку 3, которая является уродливым решением, которое я рассматриваю (будет плохо работать).

 using Dapper;
using System;
using System.Data.SqlClient;
using System.Linq;

namespace DapperTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var connectionString = $"Initial Catalog=master;Integrated Security=True;";
            using (SqlConnection con = new SqlConnection(connectionString))
            {
                //Here's my overall problem:
                // Assume a table X exists with a lot of rows in it.
                // Now I have a list of IDs, which IDs in that list are not in table X?
                //Note: I will spoof X with a CTE in each query. Also, the IDs are uniqueidentifiers in the real table, 
                // but I'll use ints for this simple demo.

                Attempt1(con);

                Attempt2(con);

                Attempt3(con);
            }
        }

        //This fails because I cannot select from the @Ids parameter (what I would like to do, but syntax does not support it)
        //System.Data.SqlClient.SqlException (0x80131904): Incorrect syntax near ','
        static void Attempt1(SqlConnection con)
        {
            int[] idsToCheck = new[] { 1, 42, 112 };
            string sql = @"
                WITH X(Id) AS (SELECT 112 UNION SELECT 200)
                SELECT * FROM @Ids 
                WHERE Id NOT IN (SELECT Id FROM X)
            ";
            try
            {
                var result = con.Query(sql, new { Ids = idsToCheck });
                Console.WriteLine("Attempt2: "   string.Join(",", result));
            }
            catch (Exception e)
            {
                Console.WriteLine("Attempt1 - "   e.Message);
            }
        }

        //Here I tried to stuff the list into a table variable and then select from it.
        //Fails with this error:
        // An enumerable sequence of parameters (arrays, lists, etc) is not allowed in this context
        static void Attempt2(SqlConnection con)
        {
            int[] idsToCheck = new[] { 1, 42, 112 };
            string sql = @"
                DECLARE @Tmp TABLE ([Id] int);
                INSERT INTO @Tmp VALUES (@Id);

                WITH X(Id) AS (SELECT 112 UNION SELECT 200)
                SELECT Id FROM @Tmp WHERE Id NOT IN (SELECT Id FROM X)
            ";
            try
            {
                //note I pass in an IEnumerable directly as the second param
                var result = con.Query(sql, idsToCheck.Select(i => new { Id = i }));

                //but in the "Execute" context the below call works just fine - but I can only get back an affected row count from Execute (this frustrates me).
                //var result = con.Execute(sql, idsToCheck.Select(i => new { Id = i }));

                Console.WriteLine("Attempt2: "   string.Join(",", result));
            }
            catch (Exception e)
            {
                Console.WriteLine("Attempt2 - "   e.Message);
            }
        }

        //This works but is *very* undesirable because it fetches back a lot of data (everything in X).
        static void Attempt3(SqlConnection con)
        {
            int[] idsToCheck = new[] { 1, 42, 112 };
            string sql = @"
                WITH X(Id) AS (SELECT 112 UNION SELECT 200)
                SELECT Id FROM X
            ";
            try
            {
                var allIdsInX = con.Query<int>(sql, null);
                var result = idsToCheck.Except(allIdsInX);

                Console.WriteLine("Attempt3: "   string.Join(",", result));
            }
            catch (Exception e)
            {
                Console.WriteLine("Attempt3 - "   e.Message);
            }
        }
    }
}
 

Комментарии:

1. Похоже, это задание для хранимой процедуры с параметром табличного значения.

2. да, параметр с табличным значением может быть way….it тем не менее, кажется, что для решения простой проблемы требуется много накладных расходов на код. Я надеюсь, что есть более простой способ.

3. Не совсем уверен, что вы подразумеваете под подслушанным, можно использовать несколько разных способов. С точки зрения производительности, хотя это почти наверняка самый быстрый метод. По крайней мере, в общем случае.

4. @asawyer, под накладными расходами я подразумеваю, что мне нужно написать больше кода для типа переменной таблицы, для sproc и управлять разрешениями для sproc (развернутого по-разному в нашем коде) и т.д. Это не проблема производительности, просто моя собственная проблема с ленью (и это не является общим, поэтому, если мне нужно сделать это для другой таблицы, я предполагаю, что это будет новый sproc).

5. Как правило, в моей базе данных всегда есть несколько стандартных TVP, таких как одностолбцовый int, короткие и длинные переменные, идентификаторы guid и несколько комбинаций из двух столбцов из вышеперечисленного. Это охватывает большинство моих вариантов использования без дополнительного кодирования для каждого запроса. @asawyer Вам не нужна хранимая процедура для использования TVP

Ответ №1:

@asawyer, @Charlieface — вот как выглядит решение TVP. Больше кода, который я хотел бы, но не ужасно. Если я создам несколько типов таблиц PLACEHOLDERListTableType для разных типов идентификаторов (UNIQUEIDENTIFIER, INT, что угодно), а затем буду рассматривать их как «стандартные», я думаю, я могу назвать это многоразовым.

Я все еще хотел бы иметь более многоразовое решение, которое не требует создания пользовательских типов в каждой базе данных, в которой я его использую. Так что я не буду отмечать это как ответ. Я надеюсь, что кто-то знает некоторые хитрости Dapper / SQL, которые могли бы решить эту проблему проще.

 /*
    Created this table valued type, now try using it.
        CREATE TYPE [dbo].[IntListTableType] AS TABLE 
        (
            [Id] INT NOT NULL,
            PRIMARY KEY ([Id])
        )
*/
static void Attempt4(SqlConnection con)
{
    int[] idsToCheck = new[] { 1, 42, 112 };
    string sql = @"
        WITH X(Id) AS (SELECT 112 UNION SELECT 200)
        SELECT Id FROM @Ids WHERE Id NOT IN (SELECT Id FROM X)
    ";
    try
    {
        var result = con.Query<int>(sql, new { Ids = GetIntListTableValuedParameter(idsToCheck) });

        Console.WriteLine("Attempt4: "   string.Join(",", result));
    }
    catch (Exception e)
    {
        Console.WriteLine("Attempt4 - "   e.Message);
    }
}

static SqlMapper.ICustomQueryParameter GetIntListTableValuedParameter(IEnumerable<int> ids)
{
    var dt = new DataTable();
    dt.Columns.Add("Id", typeof(int));
    foreach (var id in ids)
    {
        dt.Rows.Add(id);
    }
    return dt.AsTableValuedParameter(" [dbo].[IntListTableType]");
}
 

Комментарии:

1. Лично я думаю, что это останется лучшим методом. Возможно, вы могли бы настроить какую-то вещь, основанную на отражении, которая использует свойства класса для создания таблицы данных, в Интернете есть примеры. Кстати, ваш CTE может быть упрощен до WITH X AS (SELECT * FROM (VALUES(112),(200)) v(Id)) или вы можете встроить CTE напрямую в запрос

2. Даже если бы я сделал это reflection-generated-datatable, тип таблицы все равно должен был быть создан в БД, верно? Это та часть, которая мне не нравится.

3. Да, но тогда хранимые процедуры также находятся в БД, в этом нет ничего плохого. Я полагаю, у вас может быть DDL для типа таблицы в клиенте, и он проверяет при запуске, был ли он уже создан, но не уверен, что это такая хорошая идея.