Stored Procedures in NHibernate

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))
 
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 >
 
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 ) ;
}
 
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
 
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. 

你可能感兴趣的:(Hibernate)