淺析NHibernate一對一映射的延遲加載
在使用NHibernate的過程中也遇到了許多麻煩,不過也得到了不少體會。例如NH的不足之處,我理想中的ORM框架是怎么樣的,等等這些,以后有機會也可以慢慢和各位進行討論。
不過這篇文章談論的其實只是一個小技巧,一個workaround,而且甚至于這個是由于我對NHibernate不夠了解而造成的。因此,如果您有更好的做法也請不吝指出。這個問題也就是“如何實現(xiàn)NHibernate一對一映射的延遲加載”。
NHibernate一對一映射問題描述
之前對于問題的描述,其實還有很多額外的要求沒有講清楚,而需要“workaround”的現(xiàn)狀,也是這些要求共同形成的。經(jīng)過嘗試,如果放棄其中任何一個(如把主表ID的生成策略從identity改為native),則可能就會有更直接的做法了。這些條件是:
NHibernate一對一映射
主鍵關聯(lián)
主表的ID為自增字段
所有字段NOT NULL。
主表和子表設置級聯(lián)刪除
現(xiàn)在的問題,就是在這些條件下,如何實現(xiàn)“獲取主表對象時,并不加載其對應的子表數(shù)據(jù)”,也就是所謂的“延遲加載”。當然,除了能夠“延遲加載”以外,還必須可以插入,更新和刪除——我也嘗試過使用某些特殊的映射方式,可以實現(xiàn)延遲加載,但是卻無法插入,這自然也無法滿足要求。
為了便于理解和實驗,我在這里也將其“具體化”。首先是Model,User和UserDetail,它們是典型的一對一關系:
- public class User
- {
- public virtual int UserID { get; set; }
- public virtual string Name { get; set; }
- public virtual UserDetail Detail { get; set; }
- }
- public class UserDetail
- {
- public virtual int UserID { get; set; }
- public virtual int Age { get; set; }
- public virtual User User { get; set; }
- }
而數(shù)據(jù)庫方面則是一個User表和一個UserDetail表:
- CREATE TABLE [dbo].[User](
- [UserID] [int] IDENTITY(1,1) NOT NULL,
- [Name] [nvarchar](50) NOT NULL,
- CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
- (
- [UserID] ASC
- ))
- GO
- CREATE TABLE [dbo].[UserDetail](
- [UserID] [int] NOT NULL,
- [Age] [int] NOT NULL,
- CONSTRAINT [PK_UserDetail] PRIMARY KEY CLUSTERED
- (
- [UserID] ASC
- ))
- GO
- ALTER TABLE [dbo].[UserDetail] WITH CHECK ADD
- CONSTRAINT [FK_UserDetail_User] FOREIGN KEY([UserID])
- REFERENCES [dbo].[User] ([UserID])
- ON DELETE CASCADE
- GO
- ALTER TABLE [dbo].[UserDetail] CHECK CONSTRAINT [FK_UserDetail_User]
- GO
User表為主表,主鍵為UserID,自增。UserDetail為副表,主鍵為UserID,同時作為外鍵與User表產(chǎn)生關聯(lián)。同時,外鍵上設置了級聯(lián)刪除,也就是在刪除User表的紀錄時,會自動刪除UserDetail的紀錄。
對于環(huán)境的描述就到這里,如果您想要自己實驗的話,可以直接使用這些代碼。值得強調一下的是,有些朋友可能會使用NHibernate自動生成數(shù)據(jù)表,那么請注意嚴格調整NHibernate的配置,使其與這個環(huán)境完全相同。
傳統(tǒng)一對一映射
關于一對一映射是否可以延遲加載的問題,我在互聯(lián)網(wǎng)上找了許多資料。有NHibernate的資料,也有沒N的資料。有的資料上說不支持,有的資料卻又說可以實現(xiàn)。不過根據(jù)那些說“可以”的資料進行配置,卻還是無法做到延遲加載。而把這個問題發(fā)到NHibernate的用戶郵件列表中也沒有得到答復。不管怎么樣,我把普通的配置也發(fā)布在這里吧。
- <?xml version="1.0" encoding="utf-8" ?>
- <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHTest" namespace="NHTest">
- <class name="User" table="`User`">
- <id name="UserID" type="Int32" column="UserID">
- <generator class="identity" />
- </id>
- <one-to-one name="Detail" class="UserDetail" cascade="save-update" lazy="proxy" />
- <property name="Name" type="String" />
- </class>
- <class name="UserDetail" table="`UserDetail`" lazy="true">
- <id name="UserID" type="Int32" column="UserID">
- <generator class="foreign">
- <param name="property">User</param>
- </generator>
- </id>
- <one-to-one name="User" class="User" constrained="true" />
- <property name="Age" type="Int32" />
- </class>
- </hibernate-mapping>
按照某些資料的說法,我們把one-to-one的lazy設為proxy,并且把UserDetail節(jié)點的lazy設為true,便可以實現(xiàn)延遲加載。也就是說,在執(zhí)行以下代碼時,并不會去獲取UserDetail的內容:
var user = session.Get<User>(1);
可是現(xiàn)在,NHibernate告訴我們現(xiàn)在使用的SQL是這樣子的(您也可以使用SQL Profiler進行觀察):
- SELECT
- user0_.UserID as UserID0_1_,
- user0_.Name as Name0_1_,
- userdetail1_.UserID as UserID1_0_,
- userdetail1_.Age as Age1_0_
- FROM
- [User] user0_
- left outer join
- [UserDetail] userdetail1_
- on user0_.UserID=userdetail1_.UserID
- WHERE
- user0_.UserID=@p0;
- @p0 = 1
很明顯,它仍然把UserDetail一并獲取出來了。如果您覺得這里哪里錯了,請告訴我。
開始繞彎路
從現(xiàn)在開始,我們就要走“彎路”了。雖然我們無法在一對一映射的情況下實現(xiàn)延遲加載,但是我們可以輕易做到“一對多”映射時,延遲加載“集合”中的子對象。我們這個workaround的關鍵,便是利用了“一對多”情況下的延遲加載,把“一對一”作為“一對多”的特殊情況進行處理。不過這里就需要我們修改User的Model了:
- public class User
- {
- public virtual int UserID { get; set; }
- public virtual string Name { get; set; }
- private ISet<UserDetail> m_detailLazyProxySet;
- private ISet<UserDetail> DetailLazyProxySet
- {
- get
- {
- if (this.m_detailLazyProxySet == null)
- {
- this.m_detailLazyProxySet = new HashedSet<UserDetail>();
- }
- return this.m_detailLazyProxySet;
- }
- set
- {
- this.m_detailLazyProxySet = value;
- }
- }
- public virtual UserDetail Detail
- {
- get
- {
- return this.DetailLazyProxySet.Count <= 0 ? null :
- this.DetailLazyProxySet.Single();
- }
- set
- {
- this.DetailLazyProxySet.Clear();
- this.DetailLazyProxySet.Add(value);
- }
- }
- }
也多虧NHibernate支持對private屬性的讀寫,我們可以把DetailLazyProxySet設為私有屬性,對外部保持“純潔”——但是,很明顯我們還是污染了Model。因此,這無論如何也只是一個workaround。
如果您使用xml進行配置,這自然沒有什么問題。不過我還是喜歡使用Fluent NHibernate,流暢,方便,還可以導出為xml。因此,我們這里提供Fluent NHibernate的代碼,相信您也可以輕易得出它所對應的xml配置內容:
- public class UserMap : ClassMap<User>
- {
- public UserMap()
- {
- Id(u => u.UserID).GeneratedBy.Identity();
- Map(u => u.Name);
- var paramExpr = Expression.Parameter(typeof(User));
- var propertyExpr = Expression.Property(paramExpr, "DetailLazyProxySet");
- var castExpr = Expression.Convert(propertyExpr, typeof(IEnumerable<UserDetail>));
- var lambdaExpr = Expression.Lambda<Func<User, IEnumerable<UserDetail>>>(castExpr, paramExpr);
- HasMany(lambdaExpr)
- .LazyLoad()
- .AsSet()
- .KeyColumnNames.Add("UserID")
- .Cascade.All()
- .Inverse();
- }
- }
- public class UserDetailMap : ClassMap<UserDetail>
- {
- public UserDetailMap()
- {
- Id(d => d.UserID).GeneratedBy.Foreign("User");
- Map(d => d.Age);
- HasOne(d => d.User).Constrained();
- }
- }
值得一提的是,由于DetailLazyProxySet是私有的,我們必須手動地構造一個Lambda表達式傳遞給HasMany方法。在實際使用過程中,我們應該提供額外的輔助方法(自然是為ClassMap<T>新增一個擴展方法),并配合約定(屬性名 + LazyProxySet)來進行強類型的編碼定義。它可能是這樣的:
HasOneByProxySet(u => u.Detail)...
嗯,就是這么點代碼。
實驗
打開NHibernate的SQL輸出,并編寫如下代碼:
- var user = session.Get<User>(1);
- Console.WriteLine("Name: {0}", user.Name);
- Console.WriteLine("Age: {0}", user.Detail.Age);
輸出如下:
- NHibernate:
- SELECT
- user0_.UserID as UserID1_0_,
- user0_.Name as Name1_0_
- FROM
- [User] user0_
- WHERE
- user0_.UserID=@p0;
- @p0 = 1
- ===> Name: Jeffrey Zhao
- NHibernate:
- SELECT
- detaillazy0_.UserID as UserID1_,
- detaillazy0_.UserID as UserID0_0_,
- detaillazy0_.Age as Age0_0_
- FROM
- [UserDetail] detaillazy0_
- WHERE
- detaillazy0_.UserID=@p0;
- @p0 = 1
- ===> Age: 25
請注意兩條輸出(已標紅)的位置,很明顯現(xiàn)在已經(jīng)實現(xiàn)了延遲加載。那么我們要“饑渴加載(Eager Load)”又當如何?其實也很簡單:
- var user = session
- .CreateCriteria<User>()
- .SetFetchMode("DetailLazyProxySet", FetchMode.Eager)
- .Add(Expression.IdEq(8))
- .UniqueResult<User>();
- Console.WriteLine("===> Name: {0}", user.Name);
- Console.WriteLine("===> Age: {0}", user.Detail.Age);
同樣,“擴展方法”配合“約定”,我們可以把SetFetchMode這行古怪的代碼改成:
.SetFetchMode(u => u.Detail)...
輸出如下:
- NHibernate:
- SELECT
- this_.UserID as UserID1_1_,
- this_.Name as Name1_1_,
- detaillazy2_.UserID as UserID3_,
- detaillazy2_.UserID as UserID0_0_,
- detaillazy2_.Age as Age0_0_
- FROM
- [User] this_
- left outer join
- [UserDetail] detaillazy2_
- on this_.UserID=detaillazy2_.UserID
- WHERE
- this_.UserID = @p0;
- @p0 = 8
- ===> Name: Jeffrey Zhao
- ===> Age: 25
我們的饑渴換來數(shù)據(jù)庫的級聯(lián),和諧而統(tǒng)一。
NHibernate一對一映射的延遲加載總結
至此,我們成功地實現(xiàn)了“一對一”的延遲加載,但是對NHibernate來說,一切都是個一對多的關系。我們獲得了表面的成功,付出了“Model被污染”的代價。
原文來自趙劼的博客園博文《NHibernate中一對一關聯(lián)的延遲加載》
【編輯推薦】