Since I gave a presentation on NHibernate a couple of months ago, I've been talking to a lot of people interested in using it. A big hangup that a number of people have is NHibernate 1.0's lack of support for data access via stored procedures. After all, many enterprise applications are built on top of a DBA- or developer-mandated stored procedure layer.
(Aside: This seems to be more often the case in the .NET world. Perhaps people have not read or been swayed by the arguments in this famous blog entry. Java-shop people are generally less in love with stored procedures. No doubt the reason why Hibernate has been slow to support them.)
That's why I was excited this week when Beta 1 of NHibernate 1.2 was released. Among other things, Version 1.2 is bringing support for stored-procedure-driven CRUD (create/read/update/delete) in both SQL Server and Oracle. So I thought I would give this a spin and let you know about my experience.
As I usually do for my samples, I'm using the Northwind database and SQL Server. (I have not yet tested the Oracle support.) I built the following set of CRUD stored procedures for the Categories table:
/*
pr_GETCATEGORY
*/
ALTER PROCEDURE [dbo].[pr_GETCATEGORY]
(
@CategoryID int
)
AS
SET NOCOUNT ON;
SELECT CategoryID, CategoryName, Description
FROM Categories
WHERE (CategoryID = @CategoryID)
/*
pr_INSERTCATEGORY
*/
ALTER PROCEDURE [dbo].[pr_INSERTCATEGORY]
(
@CategoryName nvarchar(15),
@Description ntext
)
AS
SET NOCOUNT OFF;
INSERT INTO [Categories] ([CategoryName], [Description]) VALUES (@CategoryName, @Description);
/*
pr_UPDATECATEGORY
*/
ALTER PROCEDURE [dbo].[pr_UPDATECATEGORY]
(
@CategoryName nvarchar(15),
@Description ntext,
@CategoryID int
)
AS
SET NOCOUNT OFF;
UPDATE [Categories] SET [CategoryName] = @CategoryName, [Description] = @Description WHERE (([CategoryID] = @CategoryID));
/*
pr_DELETECATEGORY
*/
ALTER PROCEDURE [dbo].[pr_DELETECATEGORY]
(
@CategoryID int
)
AS
SET NOCOUNT OFF;
DELETE FROM [Categories] WHERE (([CategoryID] = @CategoryID))
pr_GETCATEGORY
*/
ALTER PROCEDURE [dbo].[pr_GETCATEGORY]
(
@CategoryID int
)
AS
SET NOCOUNT ON;
SELECT CategoryID, CategoryName, Description
FROM Categories
WHERE (CategoryID = @CategoryID)
/*
pr_INSERTCATEGORY
*/
ALTER PROCEDURE [dbo].[pr_INSERTCATEGORY]
(
@CategoryName nvarchar(15),
@Description ntext
)
AS
SET NOCOUNT OFF;
INSERT INTO [Categories] ([CategoryName], [Description]) VALUES (@CategoryName, @Description);
/*
pr_UPDATECATEGORY
*/
ALTER PROCEDURE [dbo].[pr_UPDATECATEGORY]
(
@CategoryName nvarchar(15),
@Description ntext,
@CategoryID int
)
AS
SET NOCOUNT OFF;
UPDATE [Categories] SET [CategoryName] = @CategoryName, [Description] = @Description WHERE (([CategoryID] = @CategoryID));
/*
pr_DELETECATEGORY
*/
ALTER PROCEDURE [dbo].[pr_DELETECATEGORY]
(
@CategoryID int
)
AS
SET NOCOUNT OFF;
DELETE FROM [Categories] WHERE (([CategoryID] = @CategoryID))
Using the NHibernate 1.2 Beta 1 documentation and source as my guide, my self-imposed mission is to wire these up to NHibernate and exercise them with ordinary (as in, indistinguisable from the case where NHibernate generates executable SQL) NHibernate-based code. Here is the mapping file I came up with:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">
<class
name="NorthwindClasses.Category, NorthwindClasses"
table="Categories"
>
<id name="CategoryId" type="Int32" unsaved-value="0" column="CategoryID" >
<generator class="native" />
</id>
<property name="CategoryName" type="String(15)" />
<property name="Description" type="StringClob" />
<loader query-ref="pr_GETCATEGORY"/>
<sql-insert>exec pr_INSERTCATEGORY ?, ?</sql-insert>
<sql-update>exec pr_UPDATECATEGORY ?, ?, ?</sql-update>
<sql-delete>exec pr_DELETECATEGORY ?</sql-delete>
</class>
<sql-query name="pr_GETCATEGORY">
<return alias="cat" class="NorthwindClasses.Category, NorthwindClasses">
<return-property name="CategoryId" column="CategoryID"></return-property>
<return-property name="CategoryName" column="CategoryName"></return-property>
<return-property name="Description" column="Description"></return-property>
</return>
exec pr_GETCATEGORY ?
</sql-query>
</hibernate-mapping>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">
<class
name="NorthwindClasses.Category, NorthwindClasses"
table="Categories"
>
<id name="CategoryId" type="Int32" unsaved-value="0" column="CategoryID" >
<generator class="native" />
</id>
<property name="CategoryName" type="String(15)" />
<property name="Description" type="StringClob" />
<loader query-ref="pr_GETCATEGORY"/>
<sql-insert>exec pr_INSERTCATEGORY ?, ?</sql-insert>
<sql-update>exec pr_UPDATECATEGORY ?, ?, ?</sql-update>
<sql-delete>exec pr_DELETECATEGORY ?</sql-delete>
</class>
<sql-query name="pr_GETCATEGORY">
<return alias="cat" class="NorthwindClasses.Category, NorthwindClasses">
<return-property name="CategoryId" column="CategoryID"></return-property>
<return-property name="CategoryName" column="CategoryName"></return-property>
<return-property name="Description" column="Description"></return-property>
</return>
exec pr_GETCATEGORY ?
</sql-query>
</hibernate-mapping>
Let's break this down a little bit, starting at the bottom with the <sql-query>. (Note that this is just a standard named-query and as such it appears outside the context of the <class> mapping for NorthwindClasses.Category.) I intend to use this query as the primary-key loader for my Category class. I named the query after the stored procedure, pr_GETCATEGORY, although you should be able to name it whatever you want, because the actual code to execute the stored procedure appears as a parameterized "exec" statement in the element text. Then the only thing remaining is to indicate the NorthwindClasses.Category class as the output and map the columns to Category class properties.
To actually make this the loader for my Category class, look at this element of the <class> mapping for NorthwindClasses.Category:
<loader query-ref="pr_GETCATEGORY"/>
Here I'm just pointing at the named query and telling NHibernate to use it whenever it needs to load a Category by primary key. And indeed, when I execute this C# code with a properly configured session factory:
Category cat = null;
using (ISession session = sessionFactory.OpenSession())
{
cat = (Category)session.Get(typeof(Category), 2);
}
using (ISession session = sessionFactory.OpenSession())
{
cat = (Category)session.Get(typeof(Category), 2);
}
I see the following trace in SQL Profiler, after which my Category is correctly rehydrated:
exec sp_executesql N'exec pr_GETCATEGORY @p0',N'@p0 int',@p0=2
The other new and interesting parts of this mapping are the <sql-insert>, <sql-update>, and <sql-delete> elements. (As with many things in Hibernate mappings, the order of these elements is important lest you get errors processing the mapping file.) As you probably expect by now, these elements point to the "C", "U", and "D" parts of the CRUD stored procedures, via an "exec" call with the correct number of "?" parameter placeholders. Putting these through their paces with normal C# NHibernate code as you can see in any example, NHibernate executes the following:
exec sp_executesql N'INSERT INTO Categories (CategoryName, Description) VALUES (@p0, @p1); select SCOPE_IDENTITY()',N'@p0 nvarchar(14),@p1 nvarchar(14)',@p0=N'Scott
Category',@p1=N'Scott Cat Desc'
exec sp_executesql N'exec pr_UPDATECATEGORY @p0, @p1, @p2',N'@p0 nvarchar(20),@p1 nvarchar(14),@p2 int',@p0=N'Scott''s New Category',@p1=N'Scott Cat Desc',@p2=13
exec sp_executesql N'exec pr_DELETECATEGORY @p0',N'@p0 int',@p0=13
Category',@p1=N'Scott Cat Desc'
exec sp_executesql N'exec pr_UPDATECATEGORY @p0, @p1, @p2',N'@p0 nvarchar(20),@p1 nvarchar(14),@p2 int',@p0=N'Scott''s New Category',@p1=N'Scott Cat Desc',@p2=13
exec sp_executesql N'exec pr_DELETECATEGORY @p0',N'@p0 int',@p0=13
There is one potential drawback here: The documentation says (and it is indeed the case) that in a tail-wagging-dog scenario, you must make your stored procedure parameter order match the order in which NHibernate will spit them out. For example, when I write an UPDATE stored procedure, I prefer to place the primary key parameter first in the parameter list. NHibernate forces me to put it last. This doesn't seem like a big problem when you get to write new stored procedures to back your NHibernate code, but it's a big limitation if you were hoping to wire NHibernate to your legacy stored procedure layer. I was hoping that you would be able to name and map your input parameters like you can in vanilla ADO.NET, but unfortunately, that is not yet the case.
NHibernate 1.2 has a number of other exciting features, such as support for .NET 2.0 generic collections, a new SQL Server 2005 dialect, and SQL Server batch-update support. I may tackle some of those in future posts, so stay tuned.