Ошибка «Необходимо объявить скалярную переменную» при передаче табличного параметра параметризованному оператору SQL

#c# #sql-server #sqlcommand #sqlexception #table-valued-parameters

#c# #sql-сервер #sqlcommand #sqlexception #параметры с табличным значением

Вопрос:

В C # я пытаюсь передать DataTable в качестве параметра в оператор SQL. Приведенный ниже код:

 protected virtual void DoDeleteRecords(List<Guid> ids)
{   
    if (ids.Count > 0)
    { 
        DataTable tvp = new DataTable();
        tvp.Columns.Add("Id", typeof(Guid));

        foreach (Guid id in ids)
        {
            DataRow row = tvp.NewRow();
            row["Id"] = id;

            tvp.Rows.Add(row);
        }

        string sql = string.Format("DELETE FROM MyTable WHERE ID IN ({0})", "@IDTable");

        SqlConnection connection = new SqlConnection(CoreSettings.ConnectionString);

        using (connection)
        {
            SqlCommand command = new SqlCommand(sql, connection);
            SqlParameter tvpParam = command.Parameters.AddWithValue("@IDTable", tvp);
            tvpParam.SqlDbType = SqlDbType.Structured;
            tvpParam.TypeName = "dbo.IDList";

            connection.Open();

            command.ExecuteNonQuery();

            connection.Close();
        }
    }
}
 

Однако, когда команда.Вызывается ExecuteNonQuery, я получаю следующую ошибку SQLException:

Необходимо объявить скалярную переменную «@IDTable»

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

Кто-нибудь может увидеть, что я делаю не так?

Большое спасибо.

ОБНОВЛЕНИЕ Я изменил вопрос, чтобы удалить дерьмовый код, обогащенный SQL-инъекциями, из моего примера.

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

1. Вы не можете передавать TVP в специальных операторах. Таким образом поддерживаются только параметры хранимых процедур. Рассмотрите возможность использования Dapper; это неинтересный шаблон, а Dapper поддерживает построение списков.

2. Аааа, хорошо. Ну, тогда это все объясняет! Спасибо @JeroenMostert

3. Что такое "DELETE FROM " tableName " WHERE... «попытка достичь, когда tableName это TVP? Это привело бы к введению скалярного значения в предложение. Кроме того, этот тип синтаксиса является ужасающей уязвимостью в системе безопасности. Сейчас 2021 год, внедрение SQL должно было прекратиться много лет назад.

4. @JeroenMostert Это мусор, конечно, вы можете. Но TVP не используется, OP просто объединяет список прямо в запрос. Правильный код "DELETE FROM " tableName " WHERE " idColumnName " IN (SELECT * FROM @IDTable)" не string.Format требуется. В любом случае этот код выглядит довольно опасным, полностью открытым для SQL-инъекции

5. @Charlieface вы абсолютно правы. Я пропустил ВЫБОР из IN. Черт возьми! Спасибо, что заметили это. И просто для ясности, это не производственный код — просто то, что я использую для проверки некоторых идей. И нет, одна из этих идей не «как максимально использовать мой SQL»!

Ответ №1:

Перво-наперво: я понятия не имею, откуда вы получаете tableName and columnName , но если они предоставлены пользователем, то это открыто для SQL-инъекции. По крайней мере, используйте QUOTENAME() , чтобы гарантировать, что фактический код не вводится.

Во-вторых, вы фактически не используете TVP. В имеющемся у вас коде просто говорится IN (@IDTable) , что это не то, как вы используете TVP.

TVP — это просто табличная переменная, и ее следует использовать как любую другую таблицу:

 protected virtual void DoDeleteRecords(List<Guid> ids)
{   
    if (ids.Count == 0)
        return;
    DataTable tvp = new DataTable();
    tvp.Columns.Add("Id", typeof(Guid));

    foreach (Guid id in ids)
        tvp.Rows.Add(id);

    const string sql = @"
DELETE FROM table
WHERE idColumnName IN (SELECT * FROM @IDTable);
";

    using(SqlConnection connection = new SqlConnection(CoreSettings.ConnectionString))
    using(SqlCommand command = new SqlCommand(sql, connection))
    {
        command.Parameters.Add(
            new SqlParameter("@IDTable", SqlDbType.Structured)
        {
            Value = tvp,
            Direction = ParameterDirection.Input,
            TypeName = "dbo.IDList"
        });

        connection.Open();
        command.ExecuteNonQuery();
    }
}
 

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

1. Вы совершенно правы, исходный код, который я опубликовал, был дерьмом, и теперь я исправил свой вопрос, чтобы жестко закодировать имена таблиц / столбцов в инструкции SQL. Не было никакой реальной причины для их объединения в качестве переменных. Кроме того, спасибо, что заметили мою ошибку с таблицей TVP. Настоящий момент с ладонью!

2. Я полагаю, вам также нужно ParameterDirection.Input (эквивалент READONLY в SQL), добавили