代码改变世界

NHibernate之旅(16):探索NHibernate中使用存储过程(中)

2008-11-06 16:40 by 李永京, ... 阅读, ... 评论, 收藏, 编辑

本节内容

  • 引入
  • 实例分析
    • 2.创建对象
    • 3.更新对象
  • 结语

引入

上一篇,怎么使用MyGeneration提供的模板创建存储过程和删除对象存储过程的使用,这篇接下来介绍在NHibernate中如何使用存储过程创建对象、更新对象整个详细过程,这些全是在实际运用中积累的经验,涉及使用的错误信息,如何修改存储过程,并且比较没有使用存储过程的不同点,并非官方比较权威的资料,所以敬请参考,这篇继续,如果你还没有来及看上一篇,那赶紧去看看吧。

实例分析

2.创建对象

Step1:为了比较,我们先执行CreateCustomerTest()测试方法,没有使用存储过程下创建对象生成SQL语句如下:

INSERT INTO Customer (Version, Firstname, Lastname) VALUES (@p0, @p1, @p2);
select SCOPE_IDENTITY(); @p0 = '1', @p1 = 'YJing', @p2 = 'Lee'

Step2:修改映射文件添加存储过程,打开Customer.hbm.xml映射文件,在Class元素下添加<sql-insert>节点,调用CustomerInsert存储过程,CustomerInsert 存储过程有四个参数,这里用四个问号表示:

<sql-insert>exec CustomerInsert ?,?,?,?</sql-insert>

Step3:执行CreateCustomerTest()测试方法,出现错误“NHibernate.Exceptions.GenericADOException : could not insert: [DomainModel.Entities.Customer][SQL: exec CustomerInsert ?,?,?,?];System.Data.SqlClient.SqlException : 参数化查询 '(@p0 int,@p1 nvarchar(3),@p2 nvarchar(7),@p3 int)exec CustomerIn' 需要参数 '@p3',但未提供该参数”,这应该是参数问题,仔细看看原来的存储过程,参数位置有些问题。

Step4:修改CustomerInsert存储过程,去掉SET NOCOUNT ON并把参数移动位置,代码片段如下:

ALTER PROCEDURE [dbo].[CustomerInsert]
(
    @Version int,
    @Firstname nvarchar(50) = NULL,
    @Lastname nvarchar(50) = NULL,
    @CustomerId int = NULL OUTPUT
)
AS
    INSERT INTO [Customer]
    (
        [Version],
        [Firstname],
        [Lastname]
    )
    VALUES
    (
        @Version,
        @Firstname,
        @Lastname
    )
    SELECT @CustomerId = SCOPE_IDENTITY();
    RETURN @@Error

Step4:执行CreateCustomerTest()测试方法失败,还是以上问题,是不是生成器问题?

这里,先看看NHibernate中最常用的两个内置生成器:

native:根据底层数据库的能力选择identity、sequence 或者hilo中的一个。

increment:生成类型为Int64、Int16或Int32的标识符,只当没有其他进程同时往同一个表插入数据时,能够保持唯一性。

附:

  • identity:对DB2、MySQL、SQL Server、Sybase数据库内置标识字段提供支持。数据库返回的标识符用Convert.ChangeType加以转换,因此能够支持任何整型。
  • sequence:在DB2、PostgreSQL、Oracle数据库中使用序列或者Firebird的生成器。数据库返回的标识符用Convert.ChangeType加以转换,因此能够支持任何整型。
  • hilo:使用一个高/低位算法高效地生成Int64、Int32或Int16类型的标识符。给定一个表和字段(默认分别是 hibernate_unique_key 和next_hi)作为高位值的来源。高/低位算法生成的标识符只在一个特定的数据库中是唯一的。如果是用户提供的连接,不要使用此生成器。

测试上面方法时,使用NHibernate内置生成器类型native,它根据数据库配置自动选择了identity类型,identity类型对表的主键提供支持,可以为主键插入显式值(标识增量加一)。我们在第一步就是使用NHibernate内置的生成器类型为主键增量加一,没有任何问题,但是看看CustomerInsert存储过程,主键CustomerId使用SCOPE_IDENTITY()插入显式值(标识增量加一),我们就没有必要使用内置的生成器来插入值了,把设置IDENTITY_INSERT为OFF,NHibernate正好提供了increment类型,生成类型仅仅是整型。

解决方法有两种:

  • 1.修改存储过程:如果你在开发,你最好修改存储过程,因为在面向对象开发中,尽量不要使用存储过程,何况现在存储过程破坏了你的设计。
  • 2.修改主键生成类型:如果你已经部署好你的数据库,你没有权限修改存储过程的话,那么只要修改程序来将就存储过程了。

还有一点注意:如果你主键生成器类型为“native”,那么存储过程的参数必须相一致。

Step5:修改主键生成器类型

为了演示,这里我们修改主键生成器类型,还可以总结错误信息。使用increment类型,关闭IDENTITY_INSERT,这时执行存储过程,由存储过程来为表'Customer'中的标识列(主键)插入显式值(标识增量加一)。

<generator class ="increment"></generator>

Step6:执行CreateCustomerTest()测试方法成功,生成SQL如下,其中p0是Version,p3是CustomerId

exec CustomerInsert @p0,@p1,@p2,@p3;
@p0 = '1', @p1 = 'YJing', @p2 = 'Lee' ,@p3 = '18' 
错误提示

其实我在调试过程中还有一些错误,这里总结一下:

方案1:使用主键生成器类型为"native"

  • 直接创建对象:正常
  • 存储过程创建对象:参数化查询 '(@p0 int,@p1 nvarchar(5),@p2 nvarchar(7),@p3 int)exec CustomerIn' 需要参数 '@p3',但未提供该参数。解决方法:使用increment类型

方案2:使用主键生成器类型为"increment"

  • 直接创建对象:当IDENTITY_INSERT设置为OFF时,不能为表'Customer'中的标识列插入显式值。解决方法:使用native类型
  • 存储过程创建对象:Batch update returned unexpected row count from update; actual row count: -1; expected: 1。解决方法:去掉SET NOCOUNT ON

另外,如果你不喜欢存储过程的话,你也可以这样写,效果和使用存储过程一样。

<sql-insert>INSERT INTO Customer (Version, Firstname, Lastname) VALUES (?,?,?)</sql-insert>

但是这样的话,主键生成器类型必须为"increment"。

3.更新对象

Step1:为了比较,我们先执行UpdateCustomerTest()测试方法,没有使用存储过程下创建对象生成SQL语句如下:

UPDATE Customer SET Version = @p0, Firstname = @p1, Lastname = @p2 
WHERE CustomerId = @p3 AND Version = @p4;
@p0 = '2', @p1 = 'YJingCnBlogs', @p2 = 'Lee', @p3 = '13', @p4 = '1'

Step2:修改映射文件添加存储过程,打开Customer.hbm.xml映射文件,在Class元素下添加<sql-update>节点,调用CustomerUpdate存储过程,CustomerUpdate存储过程有四个参数,这里用四个问号表示:

<sql-update>exec CustomerUpdate ?,?,?,?</sql-update>

Step3:执行UpdateCustomerTest()测试方法,出现错误“Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [DomainModel.Entities.Customer#15]”,这个错误同删除对象存储过程一样,我们修改CustomerUpdate存储过程,去掉SET NOCOUNT ON,再次执行UpdateCustomerTest()测试方法,输出结果如下:

exec CustomerUpdate @p0,@p1,@p2,@p3; 
@p0 = '2', @p1 = 'YJingCnBlogs', @p2 = 'Lee', @p3 = '15', @p4 = '1'

这段根据我们写的存储过程实质SQL语句为:

UPDATE [Customer] SET [Version] = '2', [Firstname] = 'YJingCnBlogs',
    [Lastname] = 'Lee' WHERE [CustomerId] ='1'

虽然NHibernate知道Version列是版本控制,它自动由原来的1更新为2,但是看看上面生成的SQL语句还是不怎么舒服,其@p4参数无缘无故的在那里。

Step4:修改CustomerUpdate存储过程,把版本控制用好,编写如下:

ALTER PROCEDURE [dbo].[CustomerUpdate]
(
    @Version int,
    @Firstname nvarchar(50) = NULL,
    @Lastname nvarchar(50) = NULL,
    @CustomerId int,
    @OldVersion int
)
AS    
    UPDATE [Customer]
    SET
        [Version] = @Version,
        [Firstname] = @Firstname,
        [Lastname] = @Lastname
    WHERE 
        [CustomerId] = @CustomerId and [Version] =@OldVersion
    RETURN @@Error

Step4:执行UpdateCustomerTest()测试方法,出现错误“过程或函数 'CustomerUpdate' 需要参数 '@OldVersion',但未提供该参数”,Oh!映射文件中调用存储过程忘了增加一个参数,现在是五个参数了!

Step5:修改存储过程参数数量,打开映射文件在<sql-update>中添加一个参数即添加“,?”

<sql-update>exec CustomerUpdate ?,?,?,?,?</sql-update>

Step6:执行UpdateCustomerTest()测试方法,NHibernate生成语句如下,这下完美了。

exec CustomerUpdate @p0,@p1,@p2,@p3,@p4; 
@p0 = '4', @p1 = 'YJingCnBlogsCnBlogs', @p2 = 'Lee', @p3 = '13', @p4 = '3'

另外,如果你不喜欢存储过程的话,你也可以这样写,效果和使用存储过程一样。

<sql-update>UPDATE [Customer] SET [Version] = ?,[Firstname] = ?,[Lastname] = ? 
                  WHERE [CustomerId] =? and [Version] =?</sql-update>

结语

这一篇和上一篇介绍了如何使用存储过程删除对象、创建对象、更新对象,还有一种使用<sql-query>来调用存储过程,非常方便,下篇继续介绍。

本系列链接:NHibernate之旅系列文章导航

NHibernate Q&A

下次继续分享NHibernate!