Hello World
Spiga

NHibernate中一对一关联的延迟加载

2009-08-17 09:08 by 老赵, 8186 visits

由于项目需要,我最近对.NET平台下各ORM框架(LINQ to SQL、Entity Framework V2 & V4、以及NHibernate)进行了功能对比,NHiberante可以说是各框架之中历史最为悠久,功能最强,也是使用最为复杂的一个。在使用NHibernate的过程中也遇到了许多麻烦,不过也得到了不少体会。例如NH的不足之处,我理想中的ORM框架是怎么样的,等等这些,以后有机会也可以慢慢和各位进行讨论。

不过这篇文章谈论的其实只是一个小技巧,一个workaround,而且甚至于这个是由于我对NHibernate不够了解而造成的。因此,如果您有更好的做法也请不吝指出。这个问题也就是“如何实现NHibernate中一对一映射的延迟加载”。

问题描述

之前对于问题的描述,其实还有很多额外的要求没有讲清楚,而需要“workaround”的现状,也是这些要求共同形成的。经过尝试,如果放弃其中任何一个(如把主表ID的生成策略从identity改为native),则可能就会有更直接的做法了。这些条件是:

  • 一对一映射
  • 主键关联
  • 主表的ID为自增字段
  • 所有字段NOT NULL。
  • 主表和子表设置级联删除

现在的问题,就是在这些条件下,如何实现“获取主表对象时,并不加载其对应的子表数据”,也就是所谓的“延迟加载”。当然,除了能够“延迟加载”以外,还必须可以插入,更新和删除——我也尝试过使用某些特殊的映射方式,可以实现延迟加载,但是却无法插入,这自然也无法满足要求。

为了便于理解和实验,我在这里也将其“具体化”。首先是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; }
}

而数据库方面则是一个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表产生关联。同时,外键上设置了级联删除,也就是在删除User表的纪录时,会自动删除UserDetail的纪录。

对于环境的描述就到这里,如果您想要自己实验的话,可以直接使用这些代码。值得强调一下的是,有些朋友可能会使用NHibernate自动生成数据表,那么请注意严格调整NHibernate的配置,使其与这个环境完全相同。

传统一对一映射

关于一对一映射是否可以延迟加载的问题,我在互联网上找了许多资料。有NHibernate的资料,也有没N的资料。有的资料上说不支持,有的资料却又说可以实现。不过根据那些说“可以”的资料进行配置,却还是无法做到延迟加载。而把这个问题发到NHibernate的用户邮件列表中也没有得到答复。不管怎么样,我把普通的配置也发布在这里吧。

<?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节点的lazy设为true,便可以实现延迟加载。也就是说,在执行以下代码时,并不会去获取UserDetail的内容:

var user = session.Get<User>(1);

可是现在,NHibernate告诉我们现在使用的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一并获取出来了。如果您觉得这里哪里错了,请告诉我。

开始绕弯路

从现在开始,我们就要走“弯路”了。虽然我们无法在一对一映射的情况下实现延迟加载,但是我们可以轻易做到“一对多”映射时,延迟加载“集合”中的子对象。我们这个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

请注意两条输出(已标红)的位置,很明显现在已经实现了延迟加载。那么我们要“饥渴加载(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

我们的饥渴换来数据库的级联,和谐而统一。

总结

至此,我们成功地实现了“一对一”的延迟加载,但是对NHibernate来说,一切都是个一对多的关系。我们获得了表面的成功,付出了“Model被污染”的代价。

如果您有什么更合适的方法,请告诉我。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

92 条回复

  1. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-08-17 09:12:00

    又是沙发?

  2. seagreen7
    *.*.*.*
    链接

    seagreen7 2009-08-17 09:17:00

    老赵,请教下,Nhibernate 在那些方面能用得到,我是小鸟。

  3. 老赵
    admin
    链接

    老赵 2009-08-17 09:21:00

    @seagreen7
    去看一下NH的主页,会告诉你它是做什么的。

  4. 老赵
    admin
    链接

    老赵 2009-08-17 09:22:00

    @CoolCode
    你抢到的不是沙发,是寂寞。

  5. Clingingboy
    *.*.*.*
    链接

    Clingingboy 2009-08-17 09:28:00

    Fluent NHibernate好用不?

  6. kiler
    *.*.*.*
    链接

    kiler 2009-08-17 09:29:00

    我从来都是用一对多的配置代替一对一甚至是多对多的配置的,虽然不优雅,但是方便代码生成。

  7. 疯流成性
    *.*.*.*
    链接

    疯流成性 2009-08-17 09:34:00

    我看的不是文章,是寂寞。

    弱弱的问下,确定用哪个ORM了么?

  8. LeoXing
    *.*.*.*
    链接

    LeoXing 2009-08-17 09:35:00

    不错不错~~~希望赵哥以后多讨论讨论NH~~~

  9. 老赵
    admin
    链接

    老赵 2009-08-17 09:37:00

    Clingingboy:Fluent NHibernate好用不?


    相当好用啊,看着别人的xml可以很轻易写出Fluent代码,又快又准。

  10. 老赵
    admin
    链接

    老赵 2009-08-17 09:38:00

    kiler:我从来都是用一对多的配置代替一对一甚至是多对多的配置的,虽然不优雅,但是方便代码生成。


    需要像我一样的借助一个ProxySet,还是直接一个属性就可以了呢?

  11. 老赵
    admin
    链接

    老赵 2009-08-17 09:40:00

    疯流成性:
    弱弱的问下,确定用哪个ORM了么?


    其实我发现,没有令我满意的ORM。
    最后使用矮子里拔长子的方式选择了ORM。
    Entity Framework V4似乎还不错,但是可惜是.NET 4.0中才有。

  12. kiler
    *.*.*.*
    链接

    kiler 2009-08-17 09:40:00

    Jeffrey Zhao:

    kiler:我从来都是用一对多的配置代替一对一甚至是多对多的配置的,虽然不优雅,但是方便代码生成。


    需要像我一样的借助一个ProxySet,还是直接一个属性就可以了呢?



    没有更好的办法了,和你的写法一样。

  13. 李永京
    *.*.*.*
    链接

    李永京 2009-08-17 10:07:00

    IoC、动态代理都还没有发展到属性监听的地步,如果有这功能了,属性延迟加载功能就支持了......
    目前的做法还是使用两张表来解决...

  14. Karron Qiu
    *.*.*.*
    链接

    Karron Qiu 2009-08-17 10:10:00

  15. 老赵
    admin
    链接

    老赵 2009-08-17 10:13:00

    李永京:
    IoC、动态代理都还没有发展到属性监听的地步,如果有这功能了,属性延迟加载功能就支持了......
    目前的做法还是使用两张表来解决...


    其实技术上我看不出什么无法实现的,可为什么就是没人实现。
    // 你没有看我的文章,我没有在说这个……

  16. 李永京
    *.*.*.*
    链接

    李永京 2009-08-17 10:14:00

    @Karron Qiu
    老赵的需求很BT,他要映射1张表,而且一对一的两条记录要保存在一行记录上,很像组件那种。但是不是。

  17. 老赵
    admin
    链接

    老赵 2009-08-17 10:16:00

    @Karron Qiu
    这就是我在文章里一再强调的,我的ID Generator是Identity,如果是Native我也可以解决了。
    这两篇文章一篇是Identity,一篇是Guid……都不符合我的要求。
    // 我有机会再试试看。

  18. 老赵
    admin
    链接

    老赵 2009-08-17 10:17:00

    @李永京
    你没有看我的文章,我没有说这个,你说的这个我已经放弃了。

  19. yyliuliang
    *.*.*.*
    链接

    yyliuliang 2009-08-17 10:17:00

    fluentnhibernate的自动映射很不错
    就是有些问题”看上去“的比较古怪
    比如convertion的 IIdConvention已经设置了对象的id字段映射
    但还是必须要在 Mapping的setup里设置才能生效

  20. 老赵
    admin
    链接

    老赵 2009-08-17 10:19:00

    @yyliuliang
    我倒不用自动映射。

  21. yyliuliang
    *.*.*.*
    链接

    yyliuliang 2009-08-17 10:21:00

    Jeffrey Zhao:
    @yyliuliang
    我倒不用自动映射。



    为什么呢 是为了方便控制每一个细节?

    现在确实稳妥 毕竟api还处在变动期 官网好些文档都过时了

  22. Karron Qiu
    *.*.*.*
    链接

    Karron Qiu 2009-08-17 10:23:00

    数据库如果是sql server, 那么native和identity理论上两者应该是一样的啊.

  23. Karron Qiu
    *.*.*.*
    链接

    Karron Qiu 2009-08-17 10:25:00

    @yyliuliang
    我到没有遇到这种情况.

    auto mapping里面就是多对多不要搞, 遇到多对多, 我都要覆盖一下. 主要是表名.

  24. 老赵
    admin
    链接

    老赵 2009-08-17 10:27:00

    Karron Qiu:数据库如果是sql server, 那么native和identity理论上两者应该是一样的啊.


    native是自己指定的,identity是数据库生成的,不一样啊。

  25. 老赵
    admin
    链接

    老赵 2009-08-17 10:29:00

    @Karron Qiu
    不过,我再照那篇文章原版抄一遍试试看吧,我记得我尝试过,但不是看这篇文章。
    谢谢。

  26. yyliuliang
    *.*.*.*
    链接

    yyliuliang 2009-08-17 10:29:00

    Karron Qiu:
    @yyliuliang
    我到没有遇到这种情况.

    auto mapping里面就是多对多不要搞, 遇到多对多, 我都要覆盖一下. 主要是表名.



    config.Mappings(
    //m => m.FluentMappings.AddFromAssembly(assembly).ExportTo("c:\\temp"));

    m => m.AutoMappings.Add(
    new AutoPersistenceModel(assembly)

    .Setup(exp =>
    {
    exp.IsBaseType = isbasetype;
    exp.FindIdentity = f => f.Name == "ID";
    })
    比如上面这段里的 setup


    然后 实现了 IIdConvetion
    public void Apply(IIdentityInstance instance)
    {
    //这一节去掉了 也能正常工作
    instance.Column("id");
    instance.GeneratedBy.Identity();
    }

    反而有IIdConvention 而没有在Setup里设置 就不能映射id

  27. 老赵
    admin
    链接

    老赵 2009-08-17 10:58:00

    @Karron Qiu
    http://blogs.hibernatingrhinos.com/nhibernate/archive/2008/11/17/lazy-loading-blobs-and-the-like-in-nhibernate.aspx
    我刚尝试了这个做法,不行。
    为什么不行,我会写篇文章详细阐述一下。

  28. 老赵
    admin
    链接

    老赵 2009-08-17 11:06:00

    @Karron Qiu
    算了,就在这里说吧。
    其实就是它的数据表设计要求和我不一样。
    我要的是主键关联,它是在User表里多加一个字段,叫做UserDetailID。然后UserDetail.UserDetailID自己管自己负责。
    User.UserDetailID关联到UserDetail上。
    这时候,UserDetail变主表,User变副表了。

  29. 老赵
    admin
    链接

    老赵 2009-08-17 11:06:00

    @Karron Qiu
    http://nhforge.org/wikis/howtonh/lazy-loaded-one-to-one-with-nhibernate.aspx
    至于这篇文章的做法……其实它的方式和另一篇是一模一样的,所以也不行……

  30. Karron Qiu
    *.*.*.*
    链接

    Karron Qiu 2009-08-17 11:09:00

    Jeffrey Zhao:

    Karron Qiu:数据库如果是sql server, 那么native和identity理论上两者应该是一样的啊.


    native是自己指定的,identity是数据库生成的,不一样啊。



    native
    picks identity, sequence or hilo depending upon the capabilities of the underlying database.

    如果你设置为native, nh会根据你的数据库不一样就选择identity, sequence或者hilo. 如果是sql server的话, 应该就是identity.

    并且, 我奇怪的是. 按照理论上来说, Id的生成规则不同只是在新增的时候才有区别啊, 查询的时候应该没差吧.

    不过这个one2one的延迟加载, 由于我没有自己做过实验, 所以也不好说. 期待你的完整的文章.

  31. Jerry Gao
    *.*.*.*
    链接

    Jerry Gao 2009-08-17 11:37:00

    Jeffrey Zhao:
    @CoolCode
    你抢到的不是沙发,是寂寞。



    老赵似乎对刚刚的AnyTao的贴子还记忆犹新,这还是头一次看你暴粗口。呵呵!

    自从去年用过一次NHibernate做项目,还真不想用第二次,要写的东西太多,容易报错,性能方面也没有比较大的突出.

  32. 老赵
    admin
    链接

    老赵 2009-08-17 11:42:00

    @Jerry Gao
    寂寞这个东西流行了一段时间了,呵呵。
    NH有了Fluent NHibernate还是可以接受的,我觉得。
    性能……本来就不是为了性能去NH的啊,为了它的丰富功能。

  33. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-08-17 11:53:00

    问个问题 手里只有 vs2010 CTP
    这个玩意里面有 EF 4.0么

  34. tager
    *.*.*.*
    链接

    tager 2009-08-17 11:55:00

    老赵blog的文章打印出来字很小,而且不清晰。

  35. 老赵
    admin
    链接

    老赵 2009-08-17 11:59:00

    @韦恩卑鄙
    好像有
    // 为啥不是beta

  36. 老赵
    admin
    链接

    老赵 2009-08-17 11:59:00

    @tager
    Print Friendly的内容,我们可以提供吗?

  37. tager
    *.*.*.*
    链接

    tager 2009-08-17 12:17:00

    @Jeffrey Zhao
    晕,第一次听说Print Friendly这个好东西。

  38. Karron Qiu
    *.*.*.*
    链接

    Karron Qiu 2009-08-17 12:18:00

    @yyliuliang
    下面是我的配置. 并没有什么Setup. 我的fluent版本比较老了. 最近一直没有更新.

    var cfg = new Configuration();
    var fluentConfiguration = Fluently.Configure(cfg)
    .Database(MsSqlConfiguration
    .MsSql2005
    .ConnectionString(c =>
    c.FromConnectionStringWithKey("TestConnection")
    )
    .Driver<ExtSqlClientDriver>()
    .ShowSql()
    .QuerySubstitutions("true 1, false 0, yes 'Y', no 'N'")
    )
    .Mappings(m =>
    m.AutoMappings.Add(
    AutoPersistenceModel.MapEntitiesFromAssemblyOf<Customer>()
    .Where(GetAutoMappingFilter)
    .ConventionDiscovery.AddFromAssemblyOf<IdConversation>()
    .UseOverridesFromAssemblyOf<EmployeeMap>()
    .WithSetup(s => s.IsBaseType = IsBaseTypeConvention)
    ).ExportTo(".\\") // export hbm.xml to this folder, the folder must exist!
    );

    下面是IdConversation :

    public class IdConversation : IIdConvention
    {
    public bool Accept(IIdentityPart target)
    {
    return true;
    }

    public void Apply(IIdentityPart target)
    {
    target.GeneratedBy.HiLo("100");
    target.ColumnName("Id");
    }
    }

  39. 李永京
    *.*.*.*
    链接

    李永京 2009-08-17 12:27:00

    Fluent NHibernate 1.0 RC版发布了啊
    http://www.lostechies.com/blogs/jagregory/archive/2009/08/16/fluent-nhibernate-1-0rc.aspx

    要开始研究了哦

  40. 老赵
    admin
    链接

    老赵 2009-08-17 12:33:00

    tager:
    @Jeffrey Zhao
    晕,第一次听说Print Friendly这个好东西。


    我的意思是,一般来说网站对于文章都会提供一个print-friendly版本。
    但是博客园好像不支持这个,博客拥有者无法自己提供这个版本。

  41. Jeff Wong
    *.*.*.*
    链接

    Jeff Wong 2009-08-17 12:39:00

    再温习一遍汉语拼音啊, 得意应,顶

  42. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-08-17 12:52:00

    Jeffrey Zhao:
    其实我发现,没有令我满意的ORM。
    最后使用矮子里拔长子的方式选择了ORM。
    Entity Framework V4似乎还不错,但是可惜是.NET 4.0中才有。



    期待老赵的Entity Framework V4 文章, 哈哈

  43. gongxiaohu[未注册用户]
    *.*.*.*
    链接

    gongxiaohu[未注册用户] 2009-08-17 13:07:00

    NHibernate两种方式实现一对一
    第一种是搂主说的方式,从表主键为主表外键,确实不能实现lazyload.
    还有一种是一对多,从表外键不是主键,唯一性约束,可以实现lazyload.
    PS:
    nhibernate不用xml配置有个NHibernate.Mapping.Attributes不错.
    官方7月26日发布了nhibernate linq 1.0正式版,现在可以直接用lambda写hql了.

  44. 老赵
    admin
    链接

    老赵 2009-08-17 13:33:00

    @gongxiaohu
    nhibernate linq我一些最简单的case也出了问题,不知道为什么。

  45. yyliuliang
    *.*.*.*
    链接

    yyliuliang 2009-08-17 13:54:00

    Karron Qiu:
    @yyliuliang
    下面是我的配置. 并没有什么Setup. 我的fluent版本比较老了. 最近一直没有更新.

    var cfg = new Configuration();
    var fluentConfiguration = Fluently.Configure(cfg)
    .Database(MsSqlConfiguration
    .MsSql2005
    .ConnectionString(c...



    那难怪了 我是svn搞下来最新版本编译的
    接口部分有很大变化

  46. James.Ying
    *.*.*.*
    链接

    James.Ying 2009-08-17 14:32:00

    Fluent 官网打不开了嘛

    李永京:
    Fluent NHibernate 1.0 RC版发布了啊
    http://www.lostechies.com/blogs/jagregory/archive/2009/08/16/fluent-nhibernate-1-0rc.aspx

    要开始研究了哦



    赞一个,不知道rc版会有什么突破.

    one-to-one的延迟加载找了很久,确实没有好的办法,老赵这个已经算很好的解决方案了。

  47. 李永京
    *.*.*.*
    链接

    李永京 2009-08-17 15:13:00

    @James.Ying
    但是这样破坏了Domain模型,我觉得就用上面两个链接就能做到了。

  48. Joyaspx
    *.*.*.*
    链接

    Joyaspx 2009-08-17 15:41:00

    我也想学学NHibernate,老赵有推荐的资源么?

  49. andy.wu
    *.*.*.*
    链接

    andy.wu 2009-08-17 15:50:00

    虽然我用NHibernate并不是很深。但据我所知,NHibernate有一个很重要的细节,那就是Session.Get是不支持延迟加载的,只有用Session.Load才支持延迟加载。

    所以老赵你可以用session.Load试一下,应该就可以延迟加载one-to-one了。

  50. andy.wu
    *.*.*.*
    链接

    andy.wu 2009-08-17 15:53:00

    另外,NHibernate的延迟加载是通过castle的dynamicproxy实现的。session.Load返回的是一个代理对象,而session.Get并没有返回代理对象。从这一点也可以看出session.Get并不支持延迟加载。

  51. andy.wu
    *.*.*.*
    链接

    andy.wu 2009-08-17 16:02:00

    我在用NHibernate时,感觉orm与scale out有较大矛盾。简单的说,目前应该还没有orm能很好的支持分库分表。hibernate.sharding号称是支持分库了,但粗略看了一下,还是有较多的限制。

    但从理论上讲,orm透明的支持分库分表应该是可以做到的。另一种data mapping方案“Ibatis”, 通过修改源码(网上有一些了)据说已可做到这一点。但ibatis并不是纯粹意义的orm,用着实在有点麻烦。

    我现在发现, NHibernate至少有一大优点,那就是大大简化了数据库的操作,从而提高了生产力。我目前倾向于仅将NHibernate当作数据访问层,而领域层则完全另做,也就是说NH的模型更接近er模型。

  52. yyliuliang
    *.*.*.*
    链接

    yyliuliang 2009-08-17 16:28:00

    感觉nhibernate用起来确实局限不小,
    不过用来处理一些“对象间的典型关系”的应用还是挺方便的

  53. 5207
    *.*.*.*
    链接

    5207 2009-08-17 16:52:00

    1对1的关联延迟加载意义不大呀。。。

    还是关心Linq,老赵是不是也共享下Linq to sql 的心得呀?

  54. asem又来了[未注册用户]
    *.*.*.*
    链接

    asem又来了[未注册用户] 2009-08-17 16:52:00

    请看这里:http://developer.51cto.com/art/200908/144314.htm

    转载后改了你的标题,也没有你的链接,去骂他们,我支持你!

  55. 老赵
    admin
    链接

    老赵 2009-08-17 17:12:00

    andy.wu:
    我现在发现, NHibernate至少有一大优点,那就是大大简化了数据库的操作,从而提高了生产力。我目前倾向于仅将NHibernate当作数据访问层,而领域层则完全另做,也就是说NH的模型更接近er模型。


    难道……不就是这样做的吗?领域中的逻辑肯定要自己写啊。

  56. 老赵
    admin
    链接

    老赵 2009-08-17 17:13:00

    李永京:
    @James.Ying
    但是这样破坏了Domain模型,我觉得就用上面两个链接就能做到了。


    上面两个链接不满足我的要求,你可以尝试一下,注意严格按照我文章里的需求来做,就是列出来的几点。

  57. 老赵
    admin
    链接

    老赵 2009-08-17 17:14:00

    5207:
    1对1的关联延迟加载意义不大呀。。。


    很大啊,打个比方,如果UserDetail中存放了非常庞大的数据,但是只有在某些特定的时候才要获取出来,你会怎么做呢?

  58. andy.wu
    *.*.*.*
    链接

    andy.wu 2009-08-17 17:23:00

    Jeffrey Zhao:
    难道……不就是这样做的吗?领域中的逻辑肯定要自己写啊。



    呵呵,可能没表达的清楚。

    举个例子,比如User对象,以前是数据是在这个对象,领域模型也是这个对象。这个更象是所谓的充血模型

    但我现在更倾向于分成两个,一个是UserData,一个是UserDomain,当然前提是UserDomain有必要的话。




  59. ISet[未注册用户]
    *.*.*.*
    链接

    ISet[未注册用户] 2009-08-17 17:28:00

    我想问一下,return this.DetailLazyProxySet.Count <= 0 ? null :
    this.DetailLazyProxySet.Single();
    这一句为什么编译不过?

  60. 老赵
    admin
    链接

    老赵 2009-08-17 17:47:00

    @ISet
    编译错误是什么?

  61. ISet[未注册用户]
    *.*.*.*
    链接

    ISet[未注册用户] 2009-08-17 17:53:00

    编译错误是:“Iesi.Collections.Generic.ISet<xx.UserDetail>”不包含“Single”的定义,并且找不到可接受类型为“Iesi.Collections.Generic.ISet<xx.UserDetail>”的第一个参数的扩展方法“Single”(是否缺少 using 指令或程序集引用?)

  62. MotoMoto
    *.*.*.*
    链接

    MotoMoto 2009-08-17 17:59:00

    @andy.wu
    IBatisNet + CodeSmith开发起来很舒服哈

  63. 老赵
    admin
    链接

    老赵 2009-08-17 18:23:00

    @ISet
    在.NET 3.5里using System.Linq
    .NET 2.0不支持。

  64. 老赵
    admin
    链接

    老赵 2009-08-17 18:24:00

    @MotoMoto
    iBatisNet我觉得就好像只是个负责加载数据的框架而已,实现的是Transactional Scripts,NH是个完整的ORM框架。

  65. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-08-17 21:10:00

    Jeffrey Zhao:
    @韦恩卑鄙
    好像有
    // 为啥不是beta


    公司 cmmi5 不让装~~~
    家里 wpf 版本的 vs10太慢 等个快的

  66. progame
    *.*.*.*
    链接

    progame 2009-08-18 01:08:00

    NH把orm搞得太复杂了 EF比较清晰
    但是多字段 多关联下性能堪忧
    还有那大一统的设计器

    而且现在EF自动生成的sql实在是繁琐了一点
    毕竟有时候我们还是要trace profiler的
    但EF的是否加载关联对象或列表的设计很好

    我现在核心用IQToolkit 在外围用自己的映射方法

  67. 老赵
    admin
    链接

    老赵 2009-08-18 01:32:00

    @progame
    EF生成的SQL很复杂吗?就算是多字段多关联,也是因为程序需要,它不会把简单的东西复杂化的吧。
    设计器……我一直不看好这类东西(除非可以生成标准化数据,且位置无关),有机会慢慢讨论吧。
    // 能否举些例子说明EF生成的SQL繁琐?

  68. progame
    *.*.*.*
    链接

    progame 2009-08-18 10:53:00

    不是复杂 是繁琐 linq to sql的简洁多了 和期望的基本一样

  69. 老赵
    admin
    链接

    老赵 2009-08-18 11:01:00

    progame:不是复杂 是繁琐 linq to sql的简洁多了 和期望的基本一样


    能不能举一些例子?LINQ to SQL对于简单的项目来说真的是非常好用的。

  70. progame
    *.*.*.*
    链接

    progame 2009-08-18 11:14:00

    linq to sql是好用
    只是比起ef 关联加载 类设计器确实弱一些
    毕竟后生的小孩更遭爹娘疼
    所以我在一个表不多的小项目里用的是EF

    以前我都自己实现ORM, 毕竟很多meta data现有的ORM支持不够
    但linq一出, 实在是没心去做了, linq查询的强类型 便捷性无可匹敌
    如果自己完全实现IQueryable 复杂度太大 时间不允许 所引入的不稳定性 BUG也无法接受
    现在使用iqtoolkit 虽然有些小bug 但还在可控范围内, 自己添加了属性映射 不完全字段加载 只脏数据更新 以用其它的一些和数据绑定相关的修改 觉得要好用 易用很多 我用vs addin 从定义的domain class可以生成entity class 和另外一个partial的business class定义

    顺便录了一段EF的sql
    exec sp_executesql N'SELECT
    1 AS [C1],
    [Intersect1].[Name] AS [C2],
    [Intersect1].[CreatedDatetime] AS [C3],
    [Intersect1].[ViewType] AS [C4],
    [Intersect1].[Icon] AS [C5],
    [Intersect1].[Code] AS [C6],
    [Intersect1].[Type] AS [C7],
    [Intersect1].[Company] AS [C8],
    [Intersect1].[CreatedBy] AS [C9]
    FROM ( SELECT DISTINCT [Var_15_1].[Name] AS [Name], [Var_15_1].[CreatedDatetime] AS [CreatedDatetime], [Var_15_1].[ViewType] AS [ViewType], [Var_15_1].[Icon] AS [Icon], [Var_15_1].[Code] AS [Code], [Var_15_1].[Type] AS [Type], [Var_15_1].[Company] AS [Company], [Var_15_1].[CreatedBy] AS [CreatedBy]
    FROM ( SELECT
    [Extent1].[Name] AS [Name],
    [Extent1].[CreatedDatetime] AS [CreatedDatetime],
    [Extent1].[ViewType] AS [ViewType],
    [Extent1].[Icon] AS [Icon],
    [Extent1].[Code] AS [Code],
    [Extent1].[Type] AS [Type],
    [Extent1].[Company] AS [Company],
    [Extent1].[CreatedBy] AS [CreatedBy]
    FROM [dbo].[Project] AS [Extent1]
    ) AS [Var_15_1]
    WHERE EXISTS (SELECT [Var_15_2].[Name] AS [Name], [Var_15_2].[CreatedDatetime] AS [CreatedDatetime], [Var_15_2].[ViewType] AS [ViewType], [Var_15_2].[Icon] AS [Icon], [Var_15_2].[Code] AS [Code], [Var_15_2].[Type] AS [Type], [Var_15_2].[Company] AS [Company], [Var_15_2].[CreatedBy] AS [CreatedBy]
    FROM ( SELECT
    [Extent2].[Name] AS [Name],
    [Extent2].[CreatedDatetime] AS [CreatedDatetime],
    [Extent2].[ViewType] AS [ViewType],
    [Extent2].[Icon] AS [Icon],
    [Extent2].[Code] AS [Code],
    [Extent2].[Type] AS [Type],
    [Extent2].[Company] AS [Company],
    [Extent2].[CreatedBy] AS [CreatedBy]
    FROM [dbo].[Project] AS [Extent2]
    WHERE ( EXISTS (SELECT
    cast(1 as bit) AS [C1]
    FROM [dbo].[ProjectOwner] AS [Extent3]
    WHERE ([Extent2].[Code] = [Extent3].[Project]) AND ([Extent3].[Owner] = @p__linq__6)
    )) OR ( EXISTS (SELECT
    cast(1 as bit) AS [C1]
    FROM [dbo].[ProjectViewer] AS [Extent4]
    WHERE ([Extent2].[Code] = [Extent4].[Project]) AND ([Extent4].[Viewer] = @p__linq__7)
    )) OR ([Extent2].[CreatedBy] = @p__linq__8)
    ) AS [Var_15_2]
    WHERE (([Var_15_1].[Name] = [Var_15_2].[Name]) OR (([Var_15_1].[Name] IS NULL) AND ([Var_15_2].[Name] IS NULL))) AND (([Var_15_1].[CreatedDatetime] = [Var_15_2].[CreatedDatetime]) OR (([Var_15_1].[CreatedDatetime] IS NULL) AND ([Var_15_2].[CreatedDatetime] IS NULL))) AND (([Var_15_1].[ViewType] = [Var_15_2].[ViewType]) OR (([Var_15_1].[ViewType] IS NULL) AND ([Var_15_2].[ViewType] IS NULL))) AND (([Var_15_1].[Icon] = [Var_15_2].[Icon]) OR (([Var_15_1].[Icon] IS NULL) AND ([Var_15_2].[Icon] IS NULL))) AND (([Var_15_1].[Code] = [Var_15_2].[Code]) OR (([Var_15_1].[Code] IS NULL) AND ([Var_15_2].[Code] IS NULL))) AND (([Var_15_1].[Type] = [Var_15_2].[Type]) OR (([Var_15_1].[Type] IS NULL) AND ([Var_15_2].[Type] IS NULL))) AND (([Var_15_1].[Company] = [Var_15_2].[Company]) OR (([Var_15_1].[Company] IS NULL) AND ([Var_15_2].[Company] IS NULL))) AND (([Var_15_1].[CreatedBy] = [Var_15_2].[CreatedBy]) OR (([Var_15_1].[CreatedBy] IS NULL) AND ([Var_15_2].[CreatedBy] IS NULL))))
    ) AS [Intersect1]',N'@p__linq__6 uniqueidentifier,@p__linq__7 uniqueidentifier,@p__linq__8 uniqueidentifier',@p__linq__6='237D2BD4-4900-4134-A5D5-19B384FE40BB',@p__linq__7='237D2BD4-4900-4134-A5D5-19B384FE40BB',@p__linq__8='237D2BD4-4900-4134-A5D5-19B384FE40BB'

  71. 老赵
    admin
    链接

    老赵 2009-08-18 11:23:00

    @progame
    还真是一大块,是比LINQ to SQL命名乱很多。
    不过我仔细看了看,这句LINQ应该也蛮复杂的吧。

  72. progame
    *.*.*.*
    链接

    progame 2009-08-18 11:46:00

    linq挺好理解的
    var query = ctx.Project.Intersect(filter.GetProjects());
    public IQueryable<Project> GetProjects()
    {
    return m_Context.Project.Where(
    p => p.Owners.Any(u => u.ID == m_User.ID)
    || p.Viewers.Any(u => u.ID == m_User.ID)
    || p.CreatedByUser.ID == m_User.ID);
    }

    就是显示当前用户可以查看的project


    这只是我用EF和MVC搭建的一个小东西
    真实项目由于行业特殊性, 几百个字段的表是很正常的
    也许有人会认为说应该分表
    但其实这几百个字段确实是一个单据上的
    而且也要同时显示 这些字段里还有大量的关联
    可有些时候我却只需要显示其中的几十个字段
    所以部分字段加载我是必须要的

  73. 老赵
    admin
    链接

    老赵 2009-08-18 11:49:00

    @progame
    是挺容易理解的,不过的确写成SQL会一大堆,呵呵。
    我一直觉得LINQ to SQL和EF的LINQ Provider写得实在太完整了,姑且不论性能,基本上各种代码的表达方式都能执行出来,实在不简单。
    // 我也认为加载部分字段是必须的。

  74. progame
    *.*.*.*
    链接

    progame 2009-08-18 11:53:00

    linqpad 生成的sql就很好 iqtoolkit要差一点
    linqpad
    from o in Orders
    select new {o.OrderID,o.ShipViaShipper.CompanyName, SumQuantity =o.OrderDetails.Sum(od=>od.Quantity)}

    SELECT [t0].[OrderID], [t1].[CompanyName], (
    SELECT SUM(CONVERT(Int,[t2].[Quantity]))
    FROM [Order Details] AS [t2]
    WHERE [t2].[OrderID] = [t0].[OrderID]
    ) AS [SumQuantity]
    FROM [Orders] AS [t0]
    LEFT OUTER JOIN [Shippers] AS [t1] ON [t1].[ShipperID] = [t0].[ShipVia]

    iqtoolkit
    from o in db.Orders
    select new {o.OrderNo,o.Customer.CustomerName,
    SumAmount = o.Items.Sum(oi=>oi.Amount) }

    SELECT t0.[OrderNo], t0.[c0], t1.[CustomerID] AS [CustomerID1], t1.[CustomerName]
    FROM (
    SELECT t2.[OrderNo], t2.[CustomerID], (
    SELECT SUM(t3.[Amount])
    FROM [OrderItem] AS t3 WITH (NOLOCK)
    WHERE (t3.[OrderID] = t2.[OrderID])
    ) AS [c0]
    FROM [Order] AS t2
    ) AS t0
    LEFT OUTER JOIN [Customer] AS t1
    ON (t1.[CustomerID] = t0.[CustomerID])

  75. progame
    *.*.*.*
    链接

    progame 2009-08-18 11:56:00

    除了业务层处理数据我用实体操作
    展现的数据绑定我都是linq查询 datatable绑定
    因为如果是实体集合的话
    排序必须再实现自定义icomparer才能达到理想的性能
    否则就是反射, 而且我绑定到WPF控件我还需要有很多的处理
    datatable和dataview要方便很多

    因此越好的sql生成对我越方便, iqtoolkit虽然没生成好字段名
    但我通过对expression解析 倒是取得了它的名称映射 所以还是可以用的

  76. 老赵
    admin
    链接

    老赵 2009-08-18 11:59:00

    @progame
    你既然可以用LINQ,应该也可以用source.OrderBy(x => x.Prop)这种方式来排序吧。

  77. progame
    *.*.*.*
    链接

    progame 2009-08-18 12:04:00

    后期网格用户自己排序 不是前期

  78. progame
    *.*.*.*
    链接

    progame 2009-08-18 12:06:00

    而且对我们的列表显示来说
    几十个字段 几千行也是很正常的
    可能有人说用不着这么多数据
    但别人要拉列表统计(不仅仅是合计数, 要明细+合计)
    构造实体开销也是不划算的

  79. 老赵
    admin
    链接

    老赵 2009-08-18 12:20:00

    @progame
    你说是用DataTable比较方便这有可能,毕竟可以和GridView之类的直接集成。
    但是,你确定构造实体的开销大于DataTable?
    DataTable是个非常复杂的数据结构,其中包含很多DataRow,一个Row就相当于一个实体类,但是它是用字典存储的,开销应该大于一个实体类。
    此外,DataTable还要维护元数据之类的,这又是额外的数据。所以实体类应该是节省资源才对。

  80. progame
    *.*.*.*
    链接

    progame 2009-08-18 13:12:00

    实测了一下 你说得对 确实构造实体类再快 即使加上反射赋值和取值

    我简单的用WPFToolkit里的datagrid去绑一个4W行 大概10几列的数据表

    table用时大致在600~700ms
    linq to sql实体在300~400ms

  81. progame
    *.*.*.*
    链接

    progame 2009-08-18 13:14:00

    不过数据绑定后 datagrid默认行为 linq的排序无效 bool型也没有在网格上显示为checkbox 我用其它网格绑一下看

  82. progame
    *.*.*.*
    链接

    progame 2009-08-18 13:23:00

    xceed的可以正确识别属性 性能比和之前差不多

  83. 麦兜888[未注册用户]
    *.*.*.*
    链接

    麦兜888[未注册用户] 2009-08-27 23:48:00

    这个应该是可以延时加载的,LZ的原因在于

    输出了这个.

    Console.WriteLine("===> Age: {0}", user.Detail.Age);

    既然想延迟加载UserDetail,则没有访问他的必要,一访问就加载上来了,可以用单元测试观察NH发出SQL的时刻。

  84. 老赵
    admin
    链接

    老赵 2009-08-28 00:13:00

    @麦兜888
    访问user.Name和user.Detail.Age是必须的,因为我要验证的就是执行SQL的时机,它是分两次的,还是一次全部加载进来。
    我文章里已经打印出NH发出的SQL了,你可以看到是一个join语句,就证明第一次Get<User>()的时候,已经把Detail带入了。
    事实和各种资料证明,的确没有办法“正常”地进行延迟加载,目前必须绕这么一下。

  85. 麦兜888[未注册用户]
    *.*.*.*
    链接

    麦兜888[未注册用户] 2009-08-28 21:37:00

    Jeffrey Zhao:
    @麦兜888
    访问user.Name和user.Detail.Age是必须的,因为我要验证的就是执行SQL的时机,它是分两次的,还是一次全部加载进来。
    我文章里已经打印出NH发出的SQL了,你可以看到是一个join语句,就证明第一次Get<User>()的时候,已经把Detail带入了。
    事实和各种资料证明,的确没有办法“正常”地进行延迟加载,目前必须绕这么一下。




    的确如此,我发现我做的时候是把UserDetail的ID设成的native,然后把User的ID设成的foreign。所以我从User端加载的时候SQL是分两次发出的,然后在从UserDetail端加载的时候却一次把User也加载上来了。

  86. john.net
    *.*.*.*
    链接

    john.net 2009-09-01 17:13:00

    老大还能出视频啊?

  87. 老赵
    admin
    链接

    老赵 2009-09-01 19:38:00

    @john.net
    一时半会儿不行了,等我这阵子忙完了,打算讲讲并行方面的内容。

  88. Shawer
    *.*.*.*
    链接

    Shawer 2009-09-10 11:31:00

    自己指定一下外键约束试试,并且这里是不需要设置lazy属性的
    <one-to-one name="Detail" class="UserDetail" cascade="save-update" lazy="proxy" />
    修改为
    <one-to-one name="Detail" class="UserDetail" cascade="save-update" constrained="true" />

    另外延迟加载class节点应该设置为lazy
    <class name="User" table="`User`" lazy="true">

  89. 老赵
    admin
    链接

    老赵 2009-09-10 11:33:00

    @Shawer
    已经从邮件列表中确认,目前只有现在我这个做法。

  90. Shawer
    *.*.*.*
    链接

    Shawer 2009-09-10 13:11:00

    Jeffrey Zhao:
    @Shawer
    已经从邮件列表中确认,目前只有现在我这个做法。



    很奇怪,我这样设置后却能达到如下的效果:
    两个实体 Article, ArticleExt, 其中 ArticleExt 映射到 Article 的 Content 属性。
    两边的one-to-one节点设置如下:
    <one-to-one name="Content" class="Test.ArticleExt, Test" cascade="all" constrained="true"></one-to-one>
    <one-to-one name="Article" class="Test.Article, Test"></one-to-one>

    实例化 Article 后,我访问 article.Title 生成 SQL 语句如下
    SELECT article0_.ID as ID0_0_, article0_.Title as Title0_0_, article0_.CreatedTime as CreatedT3_0_0_ FROM Article article0_ WHERE article0_.ID=@p0

    访问 article.Content.Contents 生成SQL语句如下:
    SELECT article0_.ID as ID0_1_, article0_.Title as Title0_1_, article0_.CreatedTime as CreatedT3_0_1_, articleext1_.ID as ID1_0_, articleext1_.Contents as Contents1_0_ FROM Article article0_ left outer join ArticleExt articleext1_ on article0_.ID=articleext1_.ID WHERE article0_.ID=@p0

    很明显是延迟加载了,而且是你所谓的“Eager Load”方式。

  91. 老赵
    admin
    链接

    老赵 2009-09-10 13:13:00

    @Shawer
    你要把插入,删除,修改一起试,并且用上我的数据库schema和model,有要求的:
    1. 一对一映射
    2. 主键关联
    3. 主表的ID为自增字段
    4. 所有字段NOT NULL。
    5. 主表和子表设置级联删除

    我的做法是要严格在这个前提下的……

  92. Shawer
    *.*.*.*
    链接

    Shawer 2009-09-10 13:18:00

    @Jeffrey Zhao

    :),的确是这样,我仅限于RD操作,CU操作目前还存在问题.
    其他基本上满足了
    1. 一对一映射
    2. 主键关联
    3. 主表的ID为NH自动生成的GUID
    4. 所有字段NOT NULL。
    5. 主表和子表设置级联删除

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我