OUTPUT-параметры хранимой процедуры SQL Server в ASP.NET

РазделДанные и привязкаУровеньСредний Чтение8 минОпубликовано ТегиASP.NETSQL ServerADO.NETVB.NET

Хранимая процедура может вернуть не только набор строк, но и отдельные значения через OUTPUT-параметры. Разбираем, как объявить их в ADO.NET, когда читать результат и почему он бывает пустым.

Главное. OUTPUT-параметр в ADO.NET объявляется с ParameterDirection.Output, строкам нужен Size, а значение читается только после завершения выполнения: сразу после ExecuteNonQuery или после закрытия DataReader.

Зачем нужны выходные параметры

Типичная ситуация: процедура вставляет заказ и должна сообщить приложению его новый номер. Или процедура проверяет логин и возвращает идентификатор пользователя вместе с признаком блокировки. Можно оформить ответ как SELECT из одной строки, но тогда приходится открывать DataReader или DataTable ради пары значений.

OUTPUT-параметры решают это изящнее. Значение передаётся через тот же объект SqlParameter, который вы добавили в команду, и читается после выполнения одной строкой кода. Набор строк при этом остаётся свободен для основных данных: процедура может одновременно вернуть список товаров и общее их количество в выходном параметре.

Процедура на стороне SQL Server

В объявлении процедуры выходной параметр помечается ключевым словом OUTPUT. Внутри ему присваивают значение как обычной переменной. Помимо этого, у любой процедуры есть код возврата: целое число из оператора RETURN. Его принято использовать как статус выполнения, а данные передавать через OUTPUT.

AddCategory.sqlSQL
CREATE PROCEDURE dbo.AddCategory
    @CategoryName nvarchar(15),
    @Description  ntext = NULL,
    @CategoryID   int OUTPUT          -- сюда вернём новый ключ
AS
BEGIN
    SET NOCOUNT ON;
    IF EXISTS (SELECT 1 FROM Categories WHERE CategoryName = @CategoryName)
        RETURN 1;                     -- статус: такая категория уже есть

    INSERT INTO Categories (CategoryName, Description)
    VALUES (@CategoryName, @Description);

    SET @CategoryID = SCOPE_IDENTITY();
    RETURN 0;                         -- успех
END
Используйте SCOPE_IDENTITY, а не @@IDENTITY. Второй вернёт ключ из триггера, если он вставляет запись в другую таблицу.

Объявляем параметры в ADO.NET

На стороне приложения команде нужен CommandType.StoredProcedure и набор параметров. Входные добавляются как обычно. Для выходного указываем Direction = ParameterDirection.Output, а для кода возврата ParameterDirection.ReturnValue. Имя параметра кода возврата может быть любым, но принято @RETURN_VALUE.

Для строковых выходных параметров обязательно задавайте Size. Без него ADO.NET выбросит исключение о нулевом размере строки, потому что не знает, сколько места выделить под ответ.

CategoryService.vbVB.NET
Imports System.Data
Imports System.Data.SqlClient

Public Function AddCategory(name As String, ByRef newId As Integer) As Boolean
    Using cn As New SqlConnection(ConnectionString), _
          cmd As New SqlCommand("dbo.AddCategory", cn)
        cmd.CommandType = CommandType.StoredProcedure
        cmd.Parameters.Add("@CategoryName", SqlDbType.NVarChar, 15).Value = name

        Dim pId = cmd.Parameters.Add("@CategoryID", SqlDbType.Int)
        pId.Direction = ParameterDirection.Output

        Dim pRet = cmd.Parameters.Add("@RETURN_VALUE", SqlDbType.Int)
        pRet.Direction = ParameterDirection.ReturnValue

        cn.Open()
        cmd.ExecuteNonQuery()

        If CInt(pRet.Value) <> 0 Then Return False   ' дубликат
        newId = CInt(pId.Value)
        Return True
    End Using
End Function

Когда значение становится доступно

С ExecuteNonQuery и ExecuteScalar всё просто: после возврата из метода выходные параметры уже заполнены. С DataReader иначе, и на этом спотыкаются чаще всего. SQL Server отправляет значения OUTPUT-параметров в самом конце ответа, после всех наборов строк. Пока DataReader открыт, свойство Value содержит Nothing.

Порядок должен быть таким: прочитать все строки, закрыть reader вызовом Close или выходом из блока Using, и только потом обращаться к параметру. То же касается DataAdapter.Fill: он сам дочитывает и закрывает reader, так что после Fill параметры заполнены.

ProductService.vbVB.NET
' Процедура возвращает страницу товаров и общее число в @Total
Dim pTotal = cmd.Parameters.Add("@Total", SqlDbType.Int)
pTotal.Direction = ParameterDirection.Output

Dim items As New List(Of Product)
Using rdr = cmd.ExecuteReader()
    While rdr.Read()
        items.Add(New Product With {
            .Id = rdr.GetInt32(0), .Name = rdr.GetString(1)})
    End While
End Using                         ' reader закрыт, параметр заполнен

Dim total = If(pTotal.Value Is DBNull.Value, 0, CInt(pTotal.Value))

Output или InputOutput

В T-SQL нет отдельного вида «только выход»: параметр с OUTPUT всегда ещё и входной. ADO.NET различает два режима. При ParameterDirection.Output значение из приложения не передаётся, и внутри процедуры параметр стартует с NULL. При InputOutput вы задаёте начальное значение, процедура может его прочитать и изменить.

Это полезно для счётчиков и курсоров постраничной выборки: приложение передаёт номер последней загруженной записи, процедура возвращает следующий. Если же процедура только пишет в параметр, используйте Output, так намерение понятнее.

Типичные ошибки

Список проблем, которые встречаются при работе с выходными параметрами чаще всего:

  • Value равно Nothing: значение читается до закрытия DataReader;
  • исключение «String[n]: the Size property has an invalid size of 0»: у строкового выходного параметра не задан Size;
  • InvalidCastException при CInt: процедура не присвоила значение, и пришёл DBNull, проверяйте его перед преобразованием;
  • код возврата всегда 0: забыт параметр с ReturnValue или процедура завершается без RETURN;
  • «Procedure expects parameter which was not supplied»: выходной параметр объявлен в процедуре без значения по умолчанию, но не добавлен в команду.

Как это делают сегодня

Сегодня вместо ручной сборки SqlParameter часто используют Dapper: DynamicParameters.Add с direction Output и затем Get(Of Integer) дают тот же результат в две строки. В EF Core процедуры вызывают через Database.ExecuteSqlRaw, передавая SqlParameter с Direction Output. А для получения нового ключа после вставки в SQL Server удобнее конструкция INSERT ... OUTPUT inserted.Id, которая возвращает значение прямо набором строк.

Вопросы и ответы

Почему выходной параметр пустой после ExecuteReader?

SQL Server передаёт выходные значения после всех строк. Дочитайте и закройте DataReader, затем читайте Value.

Можно ли вернуть несколько значений?

Да, объявите столько OUTPUT-параметров, сколько нужно. Для больших объёмов данных удобнее вернуть набор строк.

Чем RETURN отличается от OUTPUT?

RETURN возвращает только целое число и обычно служит статусом. OUTPUT-параметры могут быть любого типа и предназначены для данных.

Читайте также