В SQL Server 2008 я могу передать параметр table-valued в мою хранимую процедуру из NHibernate. Как добиться того же в Oracle

Я создал таблицу как тип в SQL Server 2008.

Так как SQL Server 2008 поддерживает параметр передачи таблицы значений как параметр IN для хранимой процедуры. Он работает нормально.

Теперь я должен выполнить тот же подход в Oracle.

Я сделал это через PLSQLAssociativeArray, но ограничение ассоциативного массива - они однородны (каждый элемент должен быть одного типа).

Где, как и в случае с табличным параметром SQL Server 2008, возможно.

Как добиться того же результата в Oracle.

Ниже приведены мой тип и хранимая процедура в SQL Server 2008:

CREATE TYPE [dbo].[EmployeeType] AS TABLE(  
    [EmployeeID] [int] NULL,  
    [EmployeeName] [nvarchar](50) NULL  
)  
GO


CREATE PROCEDURE [dbo].[TestCustom] @location EmployeeType READONLY  
AS  
insert into Employee (EMP_ID,EMP_NAME)   
SELECT EmployeeID,EmployeeName  
FROM @location;

GO

Вызов из NHibernate

   var dt = new DataTable();  
   dt.Columns.Add("EmployeeID", typeof(int));  
   dt.Columns.Add("EmployeeName", typeof(string));  
   dt.Rows.Add(new object[] { 255066, "Nachi11" });  
   dt.Rows.Add(new object[] { 255067, "Nachi12" });                 
   ISQLQuery final = eventhistorysession.CreateSQLQuery("Call TestCustom @pLocation = :id");  
   IQuery result = final.SetStructured("id", dt);  
   IList finalResult = result.List();

Ответы

Ответ 1

CREATE OR REPLACE TYPE employeeType AS OBJECT (employeeId INT, employeeName VARCHAR2(50));

CREATE TYPE ttEmployeeType AS TABLE OF employeeType;

CREATE PROCEDURE testCustom (pLocation ttEmployeeType)
AS
BEGIN
        INSERT
        INTO    employee (emp_id, emp_name)
        SELECT  *
        FROM    TABLE(pLocation);
END;

Ответ 2

Как я понимаю, невозможно использовать параметры таблицы объектов Oracle (см. @Quassnoi answer для примера), используя либо nHibernate, либо ODP.NET. Единственный тип коллекции, поддерживаемый ODP.NET, - PLSQLAssociativeArray.

Однако можно легко добиться того же результата, что и в SQL Server TVP, используя ассоциативные массивы. Трюк состоит в том, чтобы определить массив для каждого параметра вместо одного для всей таблицы.

Я публикую полное доказательство концепции, поскольку я не смог ее найти.

Схема Oracle

Схема включает в себя таблицу и процедуру упакованной вставки. Он обрабатывает каждый параметр как столбец и предполагает, что каждый массив не меньше, чем первый.

create table test_table
(
    foo number(9),
    bar nvarchar2(64)
);
/

create or replace package test_package as
    type number_array is table of number(9) index by pls_integer;
    type nvarchar2_array is table of nvarchar2(64) index by pls_integer;

    procedure test_proc(p_foo number_array, p_bar nvarchar2_array);
end test_package;
/ 

create or replace package body test_package as

    procedure test_proc(p_foo number_array, p_bar nvarchar2_array) as
    begin
        forall i in p_foo.first .. p_foo.last
            insert into test_table values (p_foo(i), p_bar(i));
    end;
end test_package; 
/

nHibernate Mapping

<sql-query name="test_proc">
  begin test_package.test_proc(:foo, :bar); end;
</sql-query>

nHibernate Custom IType

Я взял концепцию из отличного ответа SQL Server и немного изменил класс для работы с ODP.NET. Поскольку IType огромен, я показываю только реализованные методы; остальные бросают NotImplementedException.

Если кто-то хочет использовать это в производственном коде, помните, что я не тестировал этот класс, даже если он делает то, что мне нужно.

public class OracleArrayType<T> : IType
{
    private readonly OracleDbType _dbType;

    public OracleArrayType(OracleDbType dbType)
    {
        _dbType = dbType;
    }

    public SqlType[] SqlTypes(IMapping mapping)
    {
        return new []{ new SqlType(DbType.Object) };
    }

    public bool IsCollectionType
    {
        get { return true; }
    }

    public int GetColumnSpan(IMapping mapping)
    {
        return 1;
    }

    public void NullSafeSet(IDbCommand st, object value, int index, ISessionImplementor session)
    {
        var s = st as OracleCommand;
        var v = value as T[];
        if (s != null && v != null)
        {
            s.Parameters[index].CollectionType = OracleCollectionType.PLSQLAssociativeArray;
            s.Parameters[index].OracleDbType = _dbType;
            s.Parameters[index].Value = value;
            s.Parameters[index].Size = v.Length;
        }
        else
        {
            throw new NotImplementedException();
        }
    }

    // IType boiler-plate implementation follows.

Параметр конструктора указывает тип типа базового массива (т.е. если вы передаете массив строк, пройдите OracleDbType.NVarchar2. Вероятно, существует способ вывести тип БД из типа значения, но я не уверен но как это сделать.

Метод расширения для IQuery

Это завершает создание типа:

public static class OracleExtensions
{
    public static IQuery SetArray<T>(this IQuery query, string name, OracleDbType dbType, T[] value)
    {
        return query.SetParameter(name, value, new OracleArrayType<T>(dbType));
    }
}

Использование

Чтобы связать все это вместе, так используется класс:

using (var sessionFactory = new Configuration().Configure().BuildSessionFactory())
using (var session = sessionFactory.OpenSession())
{
    session
        .GetNamedQuery("test_proc")
        .SetArray("foo", OracleDbType.Int32, new[] { 11, 21 })
        .SetArray("bar", OracleDbType.NVarchar2, new [] { "bar0", "bar1" })
        .ExecuteUpdate();
}

Результат select * from test_table после запуска кода:

FOO   BAR
----------------
11    bar0
21    bar1