Hello World
Spiga

NHibernate自定义集合类型(上):基本实现方式

2009-10-10 10:54 by 老赵, 24602 visits

前天一篇文章中我说NHibernate的集合类型实现有些“尴尬”,它无法使用自定义集合类型,设计也有些古怪——不过在许多朋友的指点下,我意识到NHibernate是可以使用自定义集合类型的。至于它的设计是否合理(或者说是用是否方便?)……这就是这几篇文章中想要探讨的内容了。不少朋友给出了一些自定义集合类型的示例链接,我参考之余也自己找了一些资料,慢慢尝试,也终于有了一些体会。

这个小系列预计有上中下三篇,在这第一篇里主要是阐述在NHibernate中自定义集合类型的基本原理和方式,进而引发一些问题。第二篇主要便是解决问题,并为了简化开发提供一个思路和“通用”一些的实现。至于第三篇,便是一个“示例”,目的便是在领域模型中为一对多的双方维护双向的关系了。搞这些东西让我头大,因为资料实在太少,就算有也大多数是浅尝辄止,没有多少“通用”的东西,有些呢又过于复杂(在我看来也违背了一些“设计原则”),忽然找到一个似乎是有示例有详细说明的文章,却因为图片和代码全部丢失让我空欢喜一场。总而言之,我这几篇是在参考零散资料的基础上,“连猜带蒙”又经历了无数尝试和挫败总结出的结果——当然,您会发现,其实还很不彻底。

有时候我也想,难道各位用Hibernate和NHibernate的同志真没有遇到我需要的场景,真没有像我一样考虑这么多吗?还是我的想法过于古怪,实际上不会这么做?否则为什么互联网上的资料那么少……

言归正传,我们开始自定义一个集合。作为基本实现方式的演示,我还是打算使用上一篇文章中Question和Answer的一对多关系作为示例:

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

    public virtual string Name { get; set; }

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

            return this.m_answers;
        }
        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; }
}

Question的Answers属性是ISet<Answer>类型,但是这个集合类型太单薄了,我需要它包含一些辅助逻辑和功能都不行(扩展方法不是万能的),那么让我们来扩展它,让Question的Answers集合使用我们自定义的类型吧:

public class Question
{
    ...

    private IAnswerSet m_answers;
    public virtual IAnswerSet Answers
    {
        get
        {
            if (this.m_answers == null)
                this.m_answers = new AnswerSet();

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

public interface IAnswerSet : ISet<Answer>
{
    int Calculate();
}

public class AnswerSet : HashedSet<Answer>, IAnswerSet
{
    public virtual int Calculate() { return 0; }
}

我们基于ISet<Answer>扩展了了一个IAnswerSet,并提供了一个Calculate方法(抱歉在这里我找不出合适的示例)。作为IAnswerSet的默认实现,我们也实现了AnswerSet类,它继承了HashedSet<Answer>,因此也只需要实现额外的Calculate方法就可以了。这两个类非常简单。

不过,NHibernate又如何知道该怎么使用AnswerSet呢?那就需要我们提供一个IUserCollectionType来告诉它这些信息了:

public class AnswerSetType : IUserCollectionType
{
    #region IUserCollectionType Members

    public bool Contains(object collection, object entity)
    {
        return ((IAnswerSet)collection).Contains((Answer)entity);
    }

    public IEnumerable GetElements(object collection)
    {
        return (IEnumerable)collection;
    }

    public object IndexOf(object collection, object entity)
    {
        throw new NotImplementedException(); // 作为Set不需要这个方法
    }

    public object Instantiate(int anticipatedSize)
    {
        return new AnswerSet();
    }

    public IPersistentCollection Instantiate(ISessionImplementor session, ...)
    {
        return new PersistentAnswerSet(session);
    }

    public object ReplaceElements(object original, object target, ...)
    {
        var result = (AnswerSet)target;

        result.Clear();
        foreach (var item in (IEnumerable<Answer>)original)
        {
            result.Add(item);
        }

        return result;
    }

    public IPersistentCollection Wrap(ISessionImplementor session, object collection)
    {
        return new PersistentAnswerSet(session, (IAnswerSet)collection);
    }

    #endregion
}

我们要为AnswerSet实现一个AnswerSetType类型才能告诉NHibernate该如何使用IAnswerSet类型。AnswerSetType的大部分方法都是浅显易懂的,不作赘述。不过有一些东西可能不是那么明白:

  • IPersistentCollection是什么?
  • PersistentAnswerSet又是什么?
  • 返回IPersistentCollection的Instantiate和Wrap方法又是什么?

这就涉及到NHibernate的一个重要功能了:自动跟踪集合状态。说是自动,其实当然还是要我们去告诉它“集合做出了哪些改变”的。怎么告诉呢?向ISessionImplemetor对象提供信息即可。那么怎么提供信息呢?通过IPersistentCollection。

这里又有一个“话题”,那就是为什么NHibernate一定要(还是“建议”?)我们为集合类型提供一个“接口”,而不是个具体类?这是因为它需要为这个接口使用不同的实现,来做到延迟加载,亦或是“跟踪集合状态”。例如下面的代码:

var session = ...;

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

session.Save(question);
session.Flush();

您认为,在question对象被保存进数据库之后,它的Answers集合是什么具体类型的呢?是AnswerSet吗?错了,它已经被替换成为PersistentAnswerSet类型(经过AnswerSetType.Wrap方法封装过)了。PersisitentAnswerSet的作用便是在提供了IAnswerSet的功能以外,还实现了IPersistentCollection接口,“同时”为NHibernate提供了“持久化信息”。很显然的是,PersistentAnswerSet和AnswerSet在IAnswerSet接口的功能上应该完全相同,否则前者就无法替代后者了。因此,PersistentAnswerSet理想的实现应该是这样的:

public class PersisitentAnswerSet : AnswerSet, IPersistentCollection
{ 
    ...
}

那么我们又该如何实现IPersistentCollection接口呢?别急,先来看看它有哪些成员吧。嗯……什么,33个方法和12个属性?没错,IPersistentCollection便是这么一个庞然大物,因为它要为NHibernate提供太多信息了。比如,从上次保存之后弄脏了没有?添加过哪些元素,又删了哪些元素?太多太多了。而且,这些成员的作用是什么呢?也基本没有资料可以告诉我们必要的信息,似乎唯一的做法便是阅读代码了。因此,这简直叫人没法实现。

“幸运的是”,NHiberante内部提供了一个PersistentGenericSet<T>类,实现了ISet<T>所需的持久化操作。于是我们的PersistentAnswerSet可以基于它来实现:

public class PersistentAnswerSet : PersistentGenericSet<Answer>, IAnswerSet
{ 
    public PersistentAnswerSet(ISessionImplementor session)
        : base(session)
    {
    }

    public PersistentAnswerSet(ISessionImplementor session, IAnswerSet collection) :
        base(session, collection)
    {
    }

    public virtual int Calculate() { return 0; }
}

只可惜,为了保持和AnswerSet行为完全一致,我们必须在PersistentAnswerSet中也提供一个一模一样的Calculate方法了——如果AnswerSet还有其他实现,或者重写了ISet<Answer>的Add,Remove等方法,我们在PersistentAnswerSet中也必须一一照办。这是一种臭不可闻的重复。

有人可能会说,那么我们不要AnswerSet类了,直接在PersistentAnswerSet提供IAnswerSet的行为,然后就在Question类中给出PersistentAnswerSet,不可以吗?从实现角度,可行。但是从设计角度上来说,不可取。因为Question是我们的领域模型,而PersistentAnswerSet依赖着NHibernate的持久化逻辑。如果在Question中直接使用PersistentAnswerSet,这就产生了领域模型到持久化逻辑的依赖了——这从领域模型设计起初就是一直在避免的。

从以上的示例中也可以看出,自定义集合的关键是在于提供一个IUserCollectionType以及一个IPersistentCollection对象。有了这两个保证,无论是Set,Bag,List还是其他任何的类型,从理论上来说,NHibernate都是支持的。但是事实上,几乎没有人去这么做。因为其中的设计有一些很古怪的,难以捉摸的地方。例如我除了基于Set的自定义集合之外,还尝试了基于Bag的开发,但是可谓困难重重。

Bag是基于Collection的,PersistentGenericBag<T>构造函数接受的参数也是ICollection<T>,但是在PersistentGenericBag<T>内部却会将集合转化为ICollection——它和ICollection<T>可没有任何联系(不像IEnumerable<T>是基于IEnumerable的)。因此我强烈怀疑PersistentGenericBag只是在Java Hibernate的非泛型基础上,包装的一层机械封装而已。此外,在从集合内部删除元素并保存至数据库的时候,NHibernate还会尝试将我们的集合转化为IList类型。真是奇怪的做法。至于它对List的支持,对普通自定义集合的支持(这要求我们实现IPersistentCollection的45个成员)我就没有尝试了。说实话,我不信任NHibernate除Set以外的集合类型。以前在使用List的时候,也发现它的映射关系并不如文档上写的那么“符合List语义”。如果您感兴趣的话,我们也可以对这方面进行更多探讨和尝试。

还是回到AnswerSet吧,要使用IAnswerSet自定义集合类型,还需要进行配置。用Fluent NHibernate来写,它可能就是这样的:

public class QuestionMap : ClassMap<Question>
{ 
    public QuestionMap()
    {
        Id(q => q.QuestionID).GeneratedBy.Identity();
        Map(q => q.Name);
        HasMany(q => q.Answers)
            .LazyLoad()
            .CollectionType<AnswerSetType>()
            .KeyColumns.Add("QuestionID")
            .Cascade.All()
            .Inverse();
    }
}

目前我们的IAnswerSet支持向集合内添加元素,删除元素并保存,以及延迟加载,满足我基本操作的要求。不过以上还只是“基本实现方式”,在投入生产之前我们还是有两个问题必须解决:

  1. IAnswerSet不是个通用的实现方式,那么给出一个尽可能通用的扩展做法呢?
  2. 在AnswerSet和PersistentAnswerSet中实现两遍完全一样的逻辑是绝对不可取的,但是出现逆向的依赖也不好,那么我们又该怎么做呢?

这真的很不容易,下次我们再来设法解决这个问题。

相关文章

Creative Commons License

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

Add your comment

20 条回复

  1. XinXin_Shine
    *.*.*.*
    链接

    XinXin_Shine 2009-10-10 11:02:00

    如果使用LINQ For NHibernate来做会不会好些呢?自定义集合会不会就不会再使用了呢?

  2. 老赵
    admin
    链接

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

    @XinXin_Shine
    你没搞清楚自定义集合的目的,还有LINQ to NHibernate的作用啊。
    两者没有任何关系的。

  3. 你处的位置比较奇怪[未注册用户]
    *.*.*.*
    链接

    你处的位置比较奇怪[未注册用户] 2009-10-10 11:27:00

    什么叫做架构、设计,试想你带着几十个人几百个人去做个产品或项目你需要关注什么东西?这些是开发者的想法,你的思想趋向于停留在这个层面,不是说不对,只是不同层面考虑的问题
    开发者真正理解考虑和讨论这些问题的不多,就是一堆的学习、顶,做设计的也不想讨论这些东西

  4. 曹赛楠
    *.*.*.*
    链接

    曹赛楠 2009-10-10 11:28:00

    老赵现在研究 NHibernate了,老赵好强啊,什么都很精通啊 ~

  5. 李永京
    *.*.*.*
    链接

    李永京 2009-10-10 11:28:00

    老赵果然好强大

  6. 老赵
    admin
    链接

    老赵 2009-10-10 11:36:00

    @你处的位置比较奇怪
    在我看来,架构分系统架构和程序架构两方面,都是架构师要负责的。
    做程序架构的时候,就会考虑怎么样用现有的框架,让开发人员写出好的程序。
    或者说,给出一个标准,一个扩展,一个通用的方式,减少开发人员的工作,让他们把精力注意在真正业务实现上。
    所以我就花力气来提取业务或功能的相似点,给出好用架子和API了,我平时工作就是这个,呵呵。

  7. 老赵
    admin
    链接

    老赵 2009-10-10 11:41:00

    @曹赛楠
    工作需要……

  8. 你处的位置比较奇怪[未注册用户]
    *.*.*.*
    链接

    你处的位置比较奇怪[未注册用户] 2009-10-10 11:50:00

    这些东西属于架构边缘化范围,一般就由高级程序员、小组长负责,敏捷里面普通开发者就要做这些
    小团队里面,怎么做都无所谓,赚钱了有效益了是正道

  9. 你处的位置比较奇怪[未注册用户]
    *.*.*.*
    链接

    你处的位置比较奇怪[未注册用户] 2009-10-10 11:55:00

    当然不包括框架的选择,主要开发规范的制定

  10. 老赵
    admin
    链接

    老赵 2009-10-10 11:56:00

    @你处的位置比较奇怪
    那我还是奇怪,照这么说,做这些东西的人应该也不少,咋好像没人搞似的……
    希望我这些文章能把NH大牛们引出来发表意见……

  11. Ryan Gene
    *.*.*.*
    链接

    Ryan Gene 2009-10-10 12:12:00

    你处的位置同学说得挺有道理。架构师搞这些的确有点小,当然我说的是一般理解下的架构师,每个公司有每个公司的情况。不过话说会来,多研究总是好的。

  12. heros
    *.*.*.*
    链接

    heros 2009-10-10 12:23:00

    AnswerSet专注业务,但Hibernate需要Persistent,那就实现一个Persistent的AnswerSetProxy给Hibernate呗。老赵给出的详细的例子可以打开,很不错的资料。

  13. XinXin_Shine
    *.*.*.*
    链接

    XinXin_Shine 2009-10-10 12:46:00

    Jeffrey Zhao:
    @XinXin_Shine
    你没搞清楚自定义集合的目的,还有LINQ to NHibernate的作用啊。
    两者没有任何关系的。


    谢谢指导,又学习了,今天练习了一下,又是另一种境界!虽然很多不明白的地方,但您技术真的很强!我也希望自己可以写程序可以写得随心所欲呢!呵呵...

  14. 老赵
    admin
    链接

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

    @heros
    呵呵,你说的东西我文章里都写了啊。
    但问题就出在这里,Persistent的AnwerSetProxy你打算怎么实现呢?
    IPersistentCollection可是有45个成员,实现一便要死人的,更何况还没有明确的指导原则……

    你给的链接没有在AnswerSet里放入自定义的逻辑,如果加入自定义逻辑,他也会遇到我说的问题,呵呵。
    所以现在互联网上的示例不够看啊,都太简单,不符合复杂的社会需求……

  15. 老赵
    admin
    链接

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

    @heros
    对于我的需求,这个链接是满足的,不过他的问题就是实现了两遍。
    其实我文章里把互联网上的问题都已经列出来的,下篇就会要设法解决掉,呵呵。

  16. XinXin_Shine
    *.*.*.*
    链接

    XinXin_Shine 2009-10-10 14:19:00

    想请教一样东西,急!如果我安装了AspNetMVC2_Preview2_VS2008又同时安装了AspNetMVC1.1两个版本,在运行1.1版本会报错的,请问可以解决为个问题吗?因为2.0版本以上已经没有Default.aspx这个文件了.急需回答,先谢谢了!

  17. XinXin_Shine
    *.*.*.*
    链接

    XinXin_Shine 2009-10-10 14:23:00

    补充一下,报:The view at '~/Views/Home/Index.aspx' must derive from ViewPage, ViewPage<TViewData>, ViewUserControl, or ViewUserControl<TViewData>.

    HttpContext.Current.RewritePath(Request.ApplicationPath, false);
    Line 17: IHttpHandler httpHandler = new MvcHttpHandler();
    Line 18: httpHandler.ProcessRequest(HttpContext.Current);这一行错
    Line 19: HttpContext.Current.RewritePath(originalPath, false);

  18. heros
    *.*.*.*
    链接

    heros 2009-10-10 14:59:00

    public class PersistentAnswerSet : PersistentGenericSet<Answer>, IAnswerSet
    {
    IAnswerSet set;

    public virtual int Calculate()
    {
    return set.Calculate();
    }
    }
    现在没有详细的想法。Castle和Spring.net都有dynamicproxy或类似的动太代理生成机制,我在想是不是可以利用它们自动生成这个代理。如果可以,这样就可以只专注去做AnswerSet,剩下的PersistentAnswerSet直接利用AnswerSet来生成个代理。这两天单位在整修,一直干体力活。有空一定要试一下。

  19. 老赵
    admin
    链接

    老赵 2009-10-10 15:03:00

    @heros
    嘿嘿,我也是用Emit干的,不过不是干成这种形式。

  20. 老赵
    admin
    链接

    老赵 2009-10-10 22:56:00

    @heros
    发现最终的结果和你差不多,其他形式的尝试失败了,呵呵。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我