21

I want to pass a collection of ids to a stored procedure that will be mapped using NHibernate. This technique was introduced in Sql Server 2008 ( more info here => Table-Valued Parameters ). I just don't want to pass multiple ids within an nvarchar parameter and then chop its value on the SQL Server side.

DDA
  • 991
  • 10
  • 28
IamDeveloper
  • 5,156
  • 6
  • 34
  • 50

4 Answers4

36

My first, ad hoc, idea was to implement my own IType.

public class Sql2008Structured : IType {
    private static readonly SqlType[] x = new[] { new SqlType(DbType.Object) };
    public SqlType[] SqlTypes(NHibernate.Engine.IMapping mapping) {
        return x;
    }

    public bool IsCollectionType {
        get { return true; }
    }

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

    public void NullSafeSet(DbCommand st, object value, int index, NHibernate.Engine.ISessionImplementor session) {
        var s = st as SqlCommand;
        if (s != null) {
            s.Parameters[index].SqlDbType = SqlDbType.Structured;
            s.Parameters[index].TypeName = "IntTable";
            s.Parameters[index].Value = value;
        }
        else {
            throw new NotImplementedException();
        }
    }

    #region IType Members...
    #region ICacheAssembler Members...
}

No more methods are implemented; a throw new NotImplementedException(); is in all the rest. Next, I created a simple extension for IQuery.

public static class StructuredExtensions {
    private static readonly Sql2008Structured structured = new Sql2008Structured();

    public static IQuery SetStructured(this IQuery query, string name, DataTable dt) {
        return query.SetParameter(name, dt, structured);
    }
}

Typical usage for me is

DataTable dt = ...;
ISession s = ...;
var l = s.CreateSQLQuery("EXEC some_sp @id = :id, @par1 = :par1")
            .SetStructured("id", dt)
            .SetParameter("par1", ...)
            .SetResultTransformer(Transformers.AliasToBean<SomeEntity>())
            .List<SomeEntity>();

Ok, but what is an "IntTable"? It's the name of SQL type created to pass table value arguments.

CREATE TYPE IntTable AS TABLE
(
    ID INT
);

And some_sp could be like

CREATE PROCEDURE some_sp
    @id IntTable READONLY,
    @par1 ...
AS
BEGIN
...
END

It only works with Sql Server 2008 of course and in this particular implementation with a single-column DataTable.

var dt = new DataTable();
dt.Columns.Add("ID", typeof(int));

It's POC only, not a complete solution, but it works and might be useful when customized. If someone knows a better/shorter solution let us know.

Joel Christophel
  • 2,604
  • 4
  • 30
  • 49
Pawel Kupis
  • 476
  • 3
  • 2
  • 1
    someone know about two column DataTable ? – rcarvalhoxavier Jun 16 '15 at 17:56
  • Pawel Kupis - will you look at this similar but different question: http://stackoverflow.com/questions/42228253/sqldbtype-structured-to-pass-table-valued-parameters-in-nhibernate-to-select-wit – Remy Feb 14 '17 at 16:50
2

A simpler solution than the accepted answer would be to use ADO.NET. NHibernate allows users to enlist IDbCommands into NHibernate transactions.

DataTable myIntsDataTable = new DataTable();
myIntsDataTable.Columns.Add("ID", typeof(int));

// ... Add rows to DataTable
ISession session = sessionFactory.GetSession();
using(ITransaction transaction = session.BeginTransaction())
{
    IDbCommand command = new SqlCommand("StoredProcedureName");
    command.Connection = session.Connection;
    command.CommandType = CommandType.StoredProcedure;
    var parameter = new SqlParameter();
    parameter.ParameterName = "IntTable";
    parameter.SqlDbType = SqlDbType.Structured;
    parameter.Value = myIntsDataTable;
    command.Parameters.Add(parameter);            
    session.Transaction.Enlist(command);
    command.ExecuteNonQuery();
}
Michael
  • 8,362
  • 6
  • 61
  • 88
DDA
  • 991
  • 10
  • 28
  • Could I attach the `parameter` to an `NHibernate.ISQLQuery`, rather than using ADO for the whole query? [Ref](https://stackoverflow.com/q/46244598/241211) – Michael Sep 15 '17 at 17:15
2

For my case, my stored procedure needs to be called in the middle of an open transaction. If there is an open transaction, this code works because it is automatically reusing the existing transaction of the NHibernate session:

NHibernateSession.GetNamedQuery("SaveStoredProc")
    .SetInt64("spData", 500)
    .ExecuteUpdate();

However, for my new Stored Procedure, the parameter is not as simple as an Int64. It's a table-valued-parameter (User Defined Table Type) My problem is that I cannot find the proper Set function. I tried SetParameter("spData", tvpObj), but it's returning this error:

Could not determine a type for class: …

Anyways, after some trial and error, this approach below seems to work. The Enlist() function is the key in this approach. It basically tells the SQLCommand to use the existing transaction. Without it, there will be an error saying

ExecuteNonQuery requires the command to have a transaction when the connection assigned to the command is in a pending local transaction…

using (SqlCommand cmd = NHibernateSession.Connection.CreateCommand() as SqlCommand)
{
    cmd.CommandText = "MyStoredProc";
    NHibernateSession.Transaction.Enlist(cmd); // Because there is a pending transaction
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add(new SqlParameter("@wiData", SqlDbType.Structured) { Value = wiSnSqlList });
    int affected = cmd.ExecuteNonQuery();
}

Since I am using the SqlParameter class with this approach, SqlDbType.Structured is available.

This is the function where wiSnList gets assigned:

private IEnumerable<SqlDataRecord> TransformWiSnListToSql(IList<SHWorkInstructionSnapshot> wiSnList)
{
    if (wiSnList == null)
    {
        yield break;
    }
    var schema = new[]
    {
        new SqlMetaData("OriginalId", SqlDbType.BigInt),           //0
        new SqlMetaData("ReportId", SqlDbType.BigInt),             //1
        new SqlMetaData("Description", SqlDbType.DateTime),        //2
    };

    SqlDataRecord row = new SqlDataRecord(schema);
    foreach (var wi in wiSnList)
    {
        row.SetSqlInt64(0, wi.OriginalId);
        row.SetSqlInt64(1, wi.ShiftHandoverReportId);
        if (wi.Description == null)
        {
            row.SetDBNull(2);
        }
        else
        {
            row.SetSqlString(2, wi.Description);
        }

        yield return row;
    }
}
Michael
  • 8,362
  • 6
  • 61
  • 88
remondo
  • 318
  • 2
  • 7
  • Can you add a little extra code? I'm interested in what `wiSnSqlList` looks like. I am wondering whether I can use your approach to answer an old ([Roomba'd](https://stackoverflow.com/help/roomba)) [question of mine](https://stackoverflow.com/q/46244598/241211). – Michael Feb 20 '19 at 14:58
  • @Michael Hi Michael I have added that function. Thanks for formatting my answer last time :-) SO newbie here...sorry that function is not formatted properly. I don't have time now though – remondo Feb 21 '19 at 03:07
  • @Michael the link to the old question/answer does not seem to be accessible from my side... – remondo Feb 21 '19 at 03:09
  • 1
    @Michael Yeah so my newly added sample code seems to fulfill what you need in that link. You can give it a try. Actually you can also pass in a Datatable instead of IEnumerable. – remondo Feb 22 '19 at 07:03
1

You can pass collections of values without the hassle.

Example:

var ids = new[] {1, 2, 3};
var query = session.CreateQuery("from Foo where id in (:ids)");
query.SetParameterList("ids", ids);

NHibernate will create a parameter for each element.

Michael
  • 8,362
  • 6
  • 61
  • 88
Diego Mijelshon
  • 52,548
  • 16
  • 116
  • 154
  • 5
    Isn't there limitation of 2100 parameters? Besides, I don't think it's a solution for my need :) Or is it? – IamDeveloper Sep 13 '10 at 17:40
  • 5
    This is very slow when many parameters are used, bloats plan cache and breaks with more than 2100. Good for the simplest of cases, but useless for more serious usage. – Alejandro Dec 06 '16 at 17:50