Hello World
Spiga

我对NHibernate的感受(3):有些尴尬的集合支持

2009-10-08 21:59 by 老赵, 14738 visits

长假休息了好多,那么继续谈谈我对NHibernate的感受。

既然是一个ORM框架,那么自然是将O这一端映射R上。至于集合,是O这方面最常见,也是R这一边非常容易表示的关系。例如,一个问题(Question)可以包含多个回答(Answer),于是我的代码里就有这样的结构:

public class Question
{
    public virtual int QuestionID { get; set; }

    public virtual string Name { get; set; }

    private ISet<Answer> m_answers;
    public ISet<Answer> Answers
    {
        get
        {
            if (this.m_answers == null)
                this.m_answers = new HashedSet<Answer>();

            return this.m_answers;
        }
        private set
        {
            this.m_answers = value;
        }
    }
}

public class Answer
{
    public virtual int AnswerID { get; set; }

    public virtual string Name { get; set; }

    public virtual Question Question { get; set; }
}

于是这里就有个问题:为什么Answers属性需要同时读写?有的朋友可能会说,NHibernate支持对私有变量的直接读写,这样就可以对外暴露出只读的属性了。这个说法的确没错(而且我已经在这里使用private set了),不过这并不是我这里不满意的地方。更准确的说,我的质疑是“为什么NHibernate会需要设置整个集合容器”?试想一下,在平时的开发中,我们的操作都是向一个集合中添加/删除对象,而不会傻傻地修改对象的集合属性。因为这个集合是对象自己维护的,而不是交给外界去“一锅端”地设置。

可以设置的容器属性并不仅仅是“感官”上的问题。假如,我使用了上面代码,那么我在向数据库插入数据时可能就是这样做的:

var question = new Question();
question.Answers.Add(new Answer { Name = "Answer 1", Question = question });
question.Answers.Add(new Answer { Name = "Answer 2", Question = question });

// put it into session

看看这两句红色的代码是不是有些多余?不仅仅是多余,这儿的问题在于,如果可以这样自由设置Question属性的话,那么我们是不是也有可能“一不小心”造成Answer与所在Question不匹配的问题呢?仅仅是创建还好,如果在一个场景下需要同时操作两个Question或Answer,它们的关系可能就复杂了。NHibernate就是这样,它需要我们手动地维护Question和Answer的双向引用,否则插入/删除/更新都可能不正确。

有些人的解决方法是添加额外的方法,例如AddAnswer:

public class Question
{
    ...

    public void AddAnswer(Answer answer)
    {
        if (answer.Question != null)
        {
            answer.Question.Answers.Remove(answer);
        }

        answer.Question = this;
        this.Answers.Add(answer);
    }
}

使用AddAnswer方法便可以自动地剥离Answer与原有Question的关系,并且与新的Question建立联系了。同理,从一个Question对象中删除一个Answer对象,或者修改Answer对象的Question属性,应该都会引起双方关系的变化。但是,即便我们提供了完整的关系维护手段,Question.Answers还是对外暴露,开发人员还是可以修改Answers集合。

因此,最好的办法其实应该是在集合中提供一种维护关系的方式。例如LINQ to SQL在这一点上便做的不错:

public partial class Question
{    
    private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
    
    private int _QuestionID;
    
    private string _Name;
    
    private EntitySet<Answer> _Answers;
    
    public Question()
    {
        this._Answers = new EntitySet<Answer>(
            new Action<Answer>(this.attach_Answers),
            new Action<Answer>(this.detach_Answers));
    }
    
    public int QuestionID { ... }
    
    public string Name { ... }
    
    public EntitySet<Answer> Answers
    {
        get
        {
            return this._Answers;
        }
        set
        {
            this._Answers.Assign(value);
        }
    }
    
    private void attach_Answers(Answer entity)
    {
        entity.Question = this;
    }
    
    private void detach_Answers(Answer entity)
    {
        entity.Question = null;
    }
}

看看LINQ to SQL对我们多体贴,自动生成的代码会帮我们维护Question与Answer之间的双向关系。当然,还有一部分逻辑是在Answer类的Question属性中,如果您感兴趣可以自己去观察一下。不过,LINQ to SQL的问题在于它使用了特殊的类型EntitySet,它会使用两个回调函数对外公布集合内元素的添加/删除情况。按理来说,如果我们想要在NHibernate中采用这种“自动维护”的方式,可以使用自定义的集合类型,例如:

private ISet<Answer> m_answers;
public ISet<Answer> Answers
{
    get
    {
        if (this.m_answers == null)
            this.m_answers = new CallbackSet<Answer>(...);

        return this.m_answers;
    }
    private set
    {
        this.m_answers = value;
    }
}

只可惜,在新建对象的时候我们自然利用到CallbackSet<Answer>,其中包含了我们定义的逻辑。但是如果是这样的代码呢?

var question = session.Get<Question>(1);
question.Answers.Add(new Answer { Name = "Answer 1", Question = question });
question.Answers.Add(new Answer { Name = "Answer 2", Question = question });
session.Flush();

在从数据库中获取Question对象的时候,NHibernate便会“自作主张”地将Answers属性“整个”设为自己的ISet<Answer>对象——因为实现延迟加载,它也并不一定是HashedSet<Answer>。换句话说,NHibernate虽然能够保持属性的逻辑,但它不能保持自定义集合的逻辑。在我看来,NHibernate完全可以做到放弃集合属性的set操作,把所有的对象都通过集合的Add方法添加进去。其实这样做同样可以实现集合的延迟加载,就好比放弃对所有方法的强制virtual要求,也能实现对象的延迟加载一样。

为了避免像上次那样误解NHibernate,我刚才又作了一次测试——这次我应该没有搞错。当然,如果NHibernate支持对自定义集合类型那就再好不过了,我们就有办法解决这个问题。但是我不知道该怎么做,如果您知道的话,请告诉我。在我看来,目前的问题是NHibernate对于POCO支持有缺陷造成的。如果是这样的话,那我们的Model就不得不继续迁就NHibernate了。

关于NHibernate集合还有一个有趣的问题是——请关注上面这4行代码(Get-Add-Flush这段),这是一个非常标准也是非常常见的添加Answer对象的方式。只可惜,在调用ISet<Answer>的Add方法添加Answer对象的时候,会引发一次数据库查询操作,加载当前Question下的所有Answer——但是在我看来这根本没有必要啊。我只是“添加”,并没有要查询。其实NHibernate帮我把新的Answer对象保存起来就可以了,为什么要增加无畏的开销呢?当然我承认,这个做法会产生一些麻烦,例如需要将集合的操作分为“读”和“写”两类,当“写”操作发生时不会加载数据,而只有在第一次“读”的时候才去数据库查询。“读”和“写”分离,本来就应该这样。

那么谁又做到这一点了呢?又是LINQ to SQL。其实LINQ to SQL在细节上有非常多的考虑,使用起来也是非常容易的——如果我不是被它“宠坏”的话,可能也就不会在意NHiberante的这个问题了。

只可惜,对于ORM的生命“映射方式”上,LINQ to SQL的支持过于有限,这也大大限制了项目对它的接受程度。

相关文章

 

注:经各位高手指点,已成功总结出NHibernate中使用自定义集合的方式

Creative Commons License

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

Add your comment

50 条回复

  1. Gsanidt
    *.*.*.*
    链接

    Gsanidt 2009-10-08 22:25:00

    哈哈,终于又更新了,学习了...

  2. 菌哥
    *.*.*.*
    链接

    菌哥 2009-10-08 22:25:00

    想问一下,老赵将NH应用到实际项目了吗?

  3. LeoXing
    *.*.*.*
    链接

    LeoXing 2009-10-08 22:27:00

    这个想法真的非常不错,老赵应该和NHibernate提议下~~~

  4. 老赵
    admin
    链接

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

    @菌哥
    是啊,否则没那么多体会的。

  5. LeoXing
    *.*.*.*
    链接

    LeoXing 2009-10-08 22:35:00

    不过方便性和自由性是相对的,想两全其美确实还是很难……

  6. 麦舒
    *.*.*.*
    链接

    麦舒 2009-10-08 22:53:00

    呵呵,其实 Linq to SQL 是很强大的,只不过深入了解的人太少。不知老赵所说的,对于ORM的生命“映射方式”上,LINQ to SQL的支持过于有限。是怎么一回事,希望能够提出来,同共讨论。

  7. LeoXing
    *.*.*.*
    链接

    LeoXing 2009-10-08 22:57:00

    @麦舒
    我了解的不算是很多,不过个人认为LINQ可以支持的东西或许就有NHibernate的30%,甚至更少。。。。

  8. 老赵
    admin
    链接

    老赵 2009-10-08 23:02:00

    LeoXing:不过方便性和自由性是相对的,想两全其美确实还是很难……


    方便和自由不是相对的。
    “方便”表示提供的东西多。“自由”表示扩展点多。
    两者正交的,甚至说提供的东西越多,扩展点应该也越多。

  9. 老赵
    admin
    链接

    老赵 2009-10-08 23:03:00

    @麦舒
    我举个例子,例如对象的继承关系,映射到数据库中就有许多种方式,但是LINQ to SQL只支持1种……

  10. XinXin_Shine
    *.*.*.*
    链接

    XinXin_Shine 2009-10-08 23:03:00

    请问可以从哪里得到NHibernate-2.1.0.GA这个版本的教程呢?或者一个完整一个实例呢?在网上的关于NHibernate-2.1.0.GA基本上是没有的!先谢谢了!

  11. 老赵
    admin
    链接

    老赵 2009-10-08 23:04:00

    @LeoXing
    这个比例我不清楚,不过的确功能比NHibernate少得多就是了。

  12. LeoXing
    *.*.*.*
    链接

    LeoXing 2009-10-08 23:09:00

    @Jeffrey Zhao
    其实我也就是自己乱想出来的~~~没有任何根据,不过LINQ和NHibernate相比确实还有很大的发展空间……

  13. 老赵
    admin
    链接

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

    @LeoXing
    还是让LINQ to SQL负责小项目,Entity Framework负责大项目吧。

  14. 老赵
    admin
    链接

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

    @XinXin_Shine
    只有这个:http://nhforge.org/doc/nh/en/index.html

  15. 沉默的糕点
    *.*.*.*
    链接

    沉默的糕点 2009-10-08 23:21:00

    hibernate 支持自定义集合的,我看Nhibernate的mapping也有相同的设定。但是找不到Nhibernate的实例。只找到hibernate的,请看这里。另外维护双向关联应该是OO的事情,和ORM无关。不过引入使用ORM后确实很复杂。我想到的只有Composite模式,把Answers私有化,再提供一系列的AddAnswer、GetAnswers的public方法。但是写HQL的时候,就很郁闷,因为HQL中的Answers,只能使用field m_Answers。期待有人告诉我更好的办法。

  16. LeoXing
    *.*.*.*
    链接

    LeoXing 2009-10-08 23:21:00

    @Jeffrey Zhao
    希望EF4.0正式出的时候可以给我们一个Surprise

  17. LeoXing
    *.*.*.*
    链接

    LeoXing 2009-10-08 23:22:00

    @XinXin_Shine
    可以去看看http://www.cnblogs.com/lyj/archive/2008/10/30/1323099.html 园子里的李永京写的

  18. 老赵
    admin
    链接

    老赵 2009-10-08 23:26:00

    @沉默的糕点
    维护双向关联的确是OO的事情,但是ORM不支持某些OO写法,问题就出在这里。
    有Hibernate的例子应该就够了,我有空就去试试看啊,谢谢!

  19. 麦舒
    *.*.*.*
    链接

    麦舒 2009-10-08 23:28:00

    LeoXing:
    @麦舒
    我了解的不算是很多,不过个人认为LINQ可以支持的东西或许就有NHibernate的30%,甚至更少。。。。


    我了解 Linq to SQL 很多,而且一直在扩展 Linq to SQL 的功。但是了解 NHibernate 很少。不知你说的 30% ,是如何估计出来的。

  20. 老赵
    admin
    链接

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

    @麦舒
    你做了哪些LINQ to SQL扩展阿?有没有什么地方可以看看?

  21. LeoXing
    *.*.*.*
    链接

    LeoXing 2009-10-08 23:34:00

    @麦舒
    如果你是在做LINQ的扩展的话,那你更应该看看NHIbernate,否则是不是有点做轮子的感觉呢?

  22. Kevin Zou
    *.*.*.*
    链接

    Kevin Zou 2009-10-09 09:24:00

    老趙終於開始寫一些“平民”文章了 :)

    以前用ORM最困擾的地方就是這種關系的設定了~
    在有了ADO.net Entity Framework後曾比較興奮它注意到了這個問題,也還專門寫了一篇blog進行探討http://www.cnblogs.com/tsoukw/archive/2008/09/11/1288942.html

    但經過比較多的項目後,發現這個問題應該是object的終極無解問題了。
    因為出現這種問題基本上是由對象模型本身造成的,同一個事物需要有2個地方進行描述,因此維護時必須考慮二者的一致性
    在博主例子中即是Question和Answer兩個類都要維持相互的引用,但本質上卻是一個事物

    這種問題其實在關系模型中早有闡述,即“數據冗余”的問題,解決方式則是通過添加外鍵欄位完成~

    我不質疑面向對象的能力,但很多時候我們似乎錯了,對象模型在某種程度並不具備思維的簡潔性和實現的無岐義,它在事物的描述上並不比關系模型佔優

    希望能夠引起更深層次的探討~

  23. XinXin_Shine
    *.*.*.*
    链接

    XinXin_Shine 2009-10-09 09:56:00

    Jeffrey Zhao:
    @XinXin_Shine
    只有这个:http://nhforge.org/doc/nh/en/index.html


    谢谢!感觉还是Linq To SQL 写得安逸,现在回头弄NHibernate是有点头痛的感觉,不可否认NHibernate确实有很好的地方可用!希望自己能在合适的项目使用合适的技术.呵呵...

  24. XinXin_Shine
    *.*.*.*
    链接

    XinXin_Shine 2009-10-09 09:57:00

    LeoXing:
    @XinXin_Shine
    可以去看看http://www.cnblogs.com/lyj/archive/2008/10/30/1323099.html 园子里的李永京写的



    感谢提供!不过这个我已经看过了,感觉还不错!学习中.

  25. 老赵
    admin
    链接

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

    @Kevin Zou
    其实我觉得并非无解,或者说在大多数情况下是有解的……要求ORM对POCO的支持够好,有方式可以维护POCO的各些逻辑。

  26. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-10-09 10:43:00

    哎,,,被惯坏的孩子。。。。

    其实这个不难理解,风格不同。比如说我以前也问过Javaer,Java默认所有方法都是虚的,而且派生类有同名方法就直接重写掉了。这样“一不小心”写了一个同名的方法又不想重写岂不会出问题?


    所以要知道Java本身就是一种“一不小心”就出大问题的语言,这种“一不小心”的小问题对于Java程序员来说就不算是大问题了。


    所以我毫不掩饰我对.NET的喜欢,C#在绝大部分语法借鉴Java的基础上,不仅仅把这些“一不小心”就出问题的乱七八糟的东西全部废除,细心的朋友应该还能发现C#使用readonly和sealed代替了Java的final关键字,以及取消了throws关键字。。。。这是为什么呢?因为被惯坏的C#程序员会“一不小心”把final写成finally(因为C#程序员都严重依赖智能提示)。。。。

  27. 老赵
    admin
    链接

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

    @Ivony...
    这里不是这个问题吧……

  28. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-10-09 11:08:00

    Jeffrey Zhao:
    @Ivony...
    这里不是这个问题吧……




    我只是仅仅针对那个“一不小心”铺展开来的,这篇文章所探讨的这样的集合的集合怎么映射到关系数据,以及如何操作,铺展开来大了,再说有人专门研究这个呢。


    不过我也不赞成说这个问题是什么Object的终极无解问题,这种模型应该说是关系数据里面最好表达的模型。(N)Hibernate看似强大,其实很多东西的处理过分迁就Object,因为它的理想是不切实际的ORM,认为程序员不应关心R而只要关注于O,我完全不能认同这种不切实际的ORM思想,So后来就一直没用。倒是EntityFramework一开始就把目标定位于E(ntity)RM,感觉还比较舒服。

  29. 徐少侠
    *.*.*.*
    链接

    徐少侠 2009-10-09 11:39:00


    希望微软的实体框架(EF)能和Hibernate在一个层次上PK
    老实说,所谓MS的程序员,看见Java的影子就烦
    不过,学习他们的思想列外

  30. 李永京
    *.*.*.*
    链接

    李永京 2009-10-09 12:27:00

    NHibernate对集合是不太好,我也是用自定义集合的做法,把Iset附上去,业务使用自定义集合,持久化使用内置集合,这样赋值。哎,
    还有NH支持自定义集合的啊,是用ICollection<T>,后面的实例随便用什么,映射用List或者Bag/Set都可以的,但是也无法摆脱集合的苦恼,例如要这个集合唯一,其不好再增加的时候去判断,只好自定义集合,做个委托方法

  31. 老赵
    admin
    链接

    老赵 2009-10-09 12:42:00

    @李永京
    这点我也说了,但是这和我的要求/意思不一样。
    你还是没看清文章啊,呵呵。

  32. 老赵
    admin
    链接

    老赵 2009-10-09 12:43:00

    @徐少侠
    Java的唯一优势就是项目多,我觉得不如拥抱Java,把项目都拿过来用,就OK了。

  33. 李永京
    *.*.*.*
    链接

    李永京 2009-10-09 12:51:00

    @Jeffrey Zhao
    看的啊,你的做法和我差不多,你自定义了CallbackSet集合。
    我还有的意思,不用他的ISet的实例HashedSet,现在也可以用别的实例。
    还有,有时不必都双向关联,这是NH不提倡的。

  34. 王德水
    *.*.*.*
    链接

    王德水 2009-10-09 13:08:00

    @Jeffrey Zhao
    你用NHibernate是为了用mysql吗?

  35. 老赵
    admin
    链接

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

    @王德水
    没,是看中它的映射能力。

  36. 老赵
    admin
    链接

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

    @李永京
    不双向关联有时候不好搞。而且关键是,似乎Answer到Question这种1对1的单项关联容易,但是Question到Answer这种一对多的单向关联好像不好搞。
    // 如何使用自定的集合啊?

  37. LeonWeng
    *.*.*.*
    链接

    LeonWeng 2009-10-09 13:59:00

    用这个还是不错的,不过感觉还是LINQ来的实在。

  38. feilng
    *.*.*.*
    链接

    feilng 2009-10-09 15:33:00

    “放弃集合属性的set操作,把所有的对象都通过集合的Add方法添加进去”会导致正常的Add操作和数据加载时的Add混淆。

    假设开始该集合映射3条数据库记录,在你读取该集合前,NH需要给你加载数据,使得集合中看起来有对应的3个对象,这相当于体现映射效果的初始化,这些阶段的操作应该和应用使用集合对外的接口方法区别开来。

    要么使用特殊定义的集合,要么set整个集合,或者扩展集合构造函数

  39. feilng
    *.*.*.*
    链接

    feilng 2009-10-09 15:38:00

    还有,如果用户定义集合Add方法中有额外逻辑,可能导致业务逻辑错误。因为这个集合业务上已经对应了3个对象,他已经存在,只是映射需要,业务上来说根本就不是Add语义

  40. 老赵
    admin
    链接

    老赵 2009-10-09 15:59:00

    @feilng
    NH是先设置Set属性再Add元素,
    还是先准备好一个已经有元素的Set再设置得啊?

  41. feilng
    *.*.*.*
    链接

    feilng 2009-10-09 16:22:00

    @Jeffrey Zhao
    根据我以前NH0.X版本的经验是,根据对应接口NH先实例化一个自有实现类,把元素添加好再设置过去

  42. 老赵
    admin
    链接

    老赵 2009-10-09 16:48:00

    @feilng
    这还真有点不自在啊,呵呵。

  43. heros
    *.*.*.*
    链接

    heros 2009-10-09 17:03:00

    hibernate中set 等集合标签在映射时有个collection-type属性。可以设置自定义的集合类型。实现UserCollectionType 接口创建自己的SetType类,在接口方法中返回自定义集合CustomCollection对象,再创建CustomCollection。在collection-type上指定SetType。NH应该差不多。有待验证。

  44. 老赵
    admin
    链接

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

    @heros
    我今天就验证一下

  45. heros
    *.*.*.*
    链接

    heros 2009-10-09 17:18:00

    对集合属性保留set方法,可以方便hibernate加载集合对象。另外get,set访问器中的自定义逻辑可以保留,所以有个最坏的办法可以手动在get或set中判断当前集合是否为自定义类型,不是的话替换掉。

    关于collection-type的用法,参见http://www.javaeye.com/topic/73823

  46. 老赵
    admin
    链接

    老赵 2009-10-09 18:49:00

    @heros
    替换掉?似乎也是值得尝试的做法。
    不过难道不会影响NH的行为吗?似乎还是Collection-Type正统一些。

  47. pplog[未注册用户]
    *.*.*.*
    链接

    pplog[未注册用户] 2009-10-09 18:52:00

    建议老赵仔细看一下Nhibernate的Mapping File
    <bag name="Categories" cascade="none" lazy="proxy" outer-join="false" not-found="ignore" fetch="join" update="false" insert="false">
    <key column="PortalID" />
    <one-to-many class="DotNetPortal.Core.Categories,DotNetPortal.Core" />
    </bag>
    另外就是如果只是依赖NHibernate的Domain的话,不但Insert会有一些问题,Query也有很多问题

  48. 编写人生
    *.*.*.*
    链接

    编写人生 2009-10-10 20:36:00

    老赵还是不要去研究NHibernate了,直接看Microsoft Entity Framework舒服多了。特别是他们现在在搞Code Only计划

  49. 老赵
    admin
    链接

    老赵 2009-10-10 20:48:00

    @编写人生
    现在Entity Framework功能还不够强呢,而且我不是研究Nhibernate,我只是用的时候不满意了,于是看看是不是能够变变,呵呵
    // 什么是Code Only?

  50. 葫芦
    113.111.210.*
    链接

    葫芦 2013-06-07 16:00:52

    Answer类中的Question属性是设计需要还是NH需要呢?

    不知道老赵有没有看过稍微精简一点的ORM,比如Ibatis,或者微型的fluentData和Dapper之类。

    NH看起来有点儿“庞然大物”,对比其他ORM来看,他是否依然还有不可替代的功能呢?

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我