Hello World
Spiga

NHibernate自定义集合类型(中):通用实现方式

2009-10-11 11:27 by 老赵, 16520 visits

上一片文章中我们观察了在代码中自定义一个基于Set的集合类型该怎么做,以及简单了解了一下NHibernate的这些自定义支持大致是如何工作的。不过文章最后还是留了两个问题,一是认为这种扩展方式不够通用,二是其中会出现的“重复”或是“反向依赖”。现在我们就需要在上文的基础上进行总结,提出一个通用的实现,可以方便我们构建自定义的集合类型。

既然要通用,我们要做的第一件事情就是对之前的例子进行总结。在Question - Answer的例子中,我们自定义了Answer的集合,它动用了以下几个组件:

  • IAnswerSet:接口
  • AnswerSet:标准实现
  • AnswerSetType:告诉NHibernate该怎么用AnswerSet
  • PersistentAnswerSet:封装了AnswerSet,提供持久化信息的接口

其中最关键的东西其实是IAnswerSet和AnswerSet,前者提供了集合的标准化接口,后者则提供了领域需要的实现。这两者是和领域需求有关的变化物,因此肯定无法在“通用性”上做文章。AnswerSetType实现了IUserCollectionType,相对较为“通用”,是我们的入手点。至于PersistentAnswerSet,它的问题便是要完全拷贝一遍IAnswerSet上定义的实现,造成重复。

既然如此,我们就从AnswerSetType入手,使用泛型提取出公共的逻辑:

public class SetType<TItem, TSet, TAbstractSet> : IUserCollectionType
    where TSet : TAbstractSet, new()
    where TAbstractSet : ISet<TItem>
{
    #region IUserCollectionType Members

    public bool Contains(object collection, object entity)
    {
        return ((TAbstractSet)collection).Contains((TItem)entity);
    }

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

    public object IndexOf(object collection, object entity)
    {
        throw new NotSupportedException();
    }

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

    public object ReplaceElements(object original, object target, ...) 
    {
        var result = (ISet<TItem>)target;

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

        return result;
    }

    public IPersistentCollection Instantiate(ISessionImplementor session, ...)
    {
        ...
    }

    public IPersistentCollection Wrap(ISessionImplementor session, object collection)
    {
        ...
    }

    #endregion
}

我们使用三种泛型类型:

  • TItem:元素类型,如Answer
  • TAbstractSet:集合类型的接口,如IAnswerSet,它必须实现ISet<TItem>
  • TSet:具体集合类型,如AnswerSet,它必须实现TAbstractSet接口

有了这三者,IUserCollectionType的大部分操作已经可以确定了,例如原本AnswerSetType类型现在就可以用以下这个类来表示:

SetType<Answer, AnswerSet, IAnswerSet>

当然,还有个问题,就是SetType类的Instantiate方法和Wrap方法该如何实现。它们的关键是提供一个如PersistentAnswerSet一样的PersistentSet。这个PersistentSet会继承PersistentGenericSet<TItem>类,并实现TAbstractSet接口。它会封装一个TAbstractSet对象,而PersistentGenericSet<TItem>已经把ISet<TItem>上的操作委托给内部的TAbstractSet对象了,因此我们需要关注的只是TAbstractSet接口中不属于ISet<TItem>的那一部分。

上一篇文章中也已经分析了,PersistentAnswerSet它是用于替代AnswerSet作为IAnswerSet的实现,并为NHibernate提供持久化信息,因此PersistentAnswerSet又IAnswerSet暴露出来的外部行为必须完全和AnswerSet一致。那么如果我们不希望在PersistentSet中在实现一遍TSet中的逻辑,那应该怎么办才好呢?

我们再来回头想一下。TSet中属于ISet<Item>的那部分逻辑已经保留了,这是因为PersistentSet会把这部分操作委托给TSet对象(自然其中也记录下了一些持久化的数据)。那么,如果我们把剩下的操作也委托给内部封装的对象,不就可以了吗?也就是说,如果我们还是实现IAnswerSet的话,可以这样:

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 this.InnerSet.Calculate();
    }

    private IAnswerSet InnerSet { get { ... } }
}

如果我们能找出这个内部封装好的,TAbstractSet类型的InnerSet对象,那么其他的操作直接委托给它便是了。当然,值得注意的是,我们必须避开ISet<TItem>上定义的成员,因为如果我们把它们也委托给InnerSet的话,那么PersistentSet就无法记录到持久化信息了。那么InnerSet哪里来?很幸运,PersistentGenericSet类型中有一个protected的set“字段”,可读可写,保存的就是它封装的ISet类型。这种设计虽然很奇怪,但的确给我们带来了方便。

为了方便起见,我们同样定义一个通用的PersistentSet类型:

public class PersistentSetBase<TItem, TAbstractSet> : PersistentGenericSet<TItem>
    where TAbstractSet : ISet<TItem>
{
    public static readonly MethodInfo InnerSetGetter =
        typeof(PersistentSetBase<TItem, TAbstractSet>).GetProperty(
            "InnerSet",
            BindingFlags.Instance | BindingFlags.NonPublic).GetGetMethod(true);

    public PersistentSetBase(ISessionImplementor session)
        : base(session) { }

    public PersistentSetBase(ISessionImplementor session, TAbstractSet innerSet)
        : base(session, innerSet) { }

    protected TAbstractSet InnerSet
    {
        get
        {
            return (TAbstractSet)this.set;
        }
    }
}

称其为Base,那是因为我们会把它作为各个不同的PersistentSet的基类。定义基类的目的,是为了如果有一天,NHibernate做了修改,那么我们也可以尽可能在编译期发现问题。好,那么实现了各个不同接口的PersistentSet类型又从哪里来呢?

自动来。我们已经制定了一个“策略”,把所有ISet<TItem>以外的操作都委托给InnerSet,如果让人手动地反复实在没有必要,我们应该让它自动进行。因此,SetType中另外两个方法的“样子”也就确定下来了:

public IPersistentCollection Instantiate(ISessionImplementor session, ICollectionPersister persister)
{
    return PersistentSetFactory<TItem, TAbstractSet>.Create(session);
}

public IPersistentCollection Wrap(ISessionImplementor session, object collection)
{
    return PersistentSetFactory<TItem, TAbstractSet>.Create(session, (TAbstractSet)collection);
}

Instantiate和Wrap方法都会调用PersistentSetFactory<TItem, TAbstractSet>的Create方法得到一个PersistentSetBase<TItem, TAbstractSet>的子类实例。这种生成动态类型的方式自然需要使用Emit,这一切就在PersistentSetFactory中实现了:

public static class PersistentSetFactory<TItem, TAbstractSet>
    where TAbstractSet : ISet<TItem>
{
    static PersistentSetFactory()
    {
        if (!typeof(TAbstractSet).IsInterface)
        {
            throw new ArgumentException("TAbstractSet must be interface.", "TAbstractSet");
        }

        var typeBuilder = CreateTypeBuilder(); // 1

        CreateConstructors(typeBuilder); // 2

        ImplementTAbstractSet(typeBuilder); // 3

        GenerateCreators(typeBuilder.CreateType()); // 4
    }

    ...

    private static Func<ISessionImplementor, IPersistentCollection> s_creator;
    private static Func<ISessionImplementor, TAbstractSet, IPersistentCollection> s_wrapCreator;

    public static IPersistentCollection Create(ISessionImplementor session)
    {
        return s_creator(session);
    }

    public static IPersistentCollection Create(ISessionImplementor session, TAbstractSet innerSet)
    {
        return s_wrapCreator(session, innerSet);
    }
}

Emit操作不是难,只是“烦”。只要我们有一幅清晰的图片,用Emit也只是的体力活而已。例如,构造这样一个动态的PersistentSet类型,我们可以分为4步进行:

  1. 创建TypeBuilder
  2. 创建两个构造函数
  3. (显式)实现TAbstractSet接口中ISet<TItem>外的成员
  4. 创建委托

至于其中大段的Emit代码就不在这里贴出了,如果您感兴趣它的实现,或需要本文的功能,可以访问这些功能的完整代码

至此一切都完成了。本文涉及到的组件,都是数据访问层的功能,它不需要领域模型对它有任何依赖。现在,如果您需要使用自定义的集合类型,例如IAnswerSet和AnswerSet,则不需要关心AnswerSetType和PersistentAnswerSet两个类型了。对于前者我们有通用的泛型类,而后者则会自动生成。于是,目前在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<SetType<Answer, AnswerSet, IAnswerSet>>()
            .KeyColumns.Add("QuestionID")
            .Cascade.All()
            .Inverse();
    }
}

如果您不使用Fluent NHibernate,需要手动编写XML的话,可能泛型的全命名不是那么容易编写。这时候就稍微麻烦一点点,需要您自己写一个类型来简单继承一下:

public class AnswerSetType : SetType<Answer, AnswerSet, IAnswerSet> { }

不过,您不需要为它提供任何成员,它的作用只是让您在写XML的时候方便一些而已。

在下次的文章中,我们会来看一下如何使用自定义的集合类型来维护一对多关系中实体之间的双向引用,也算是今天这些内容的一个实际使用案例吧。

相关文章

Creative Commons License

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

Add your comment

10 条回复

  1. heros
    *.*.*.*
    链接

    heros 2009-10-11 11:48:00

    就是这个。叹!

  2. 暴走
    *.*.*.*
    链接

    暴走 2009-10-11 12:18:00

    不知道其他人是如何用NHibernate的哦,我現在爲了完成項目,已經到了我想寫寫sql語句來完成的地步了

  3. 老赵
    admin
    链接

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

    @暴走
    你的项目用了啥功能?

  4. heros
    *.*.*.*
    链接

    heros 2009-10-11 12:29:00

    可以把hibernatedao , ado.netdao……抽像一下,idao什么的。用ibatis的daomanager来统一管理。爱用什么用什么。想写sql了,放ibatis里就是了,或ado.netdao里也成。但是到了业务里要保证能拿到统一的对象。

  5. 老赵
    admin
    链接

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

    @heros
    我理想中的数据访问层功能,还就是ORM方式的,而目前也就NHibernate有能力实现。
    至于dao那种,太复杂了,我认为,过一段时间我也想写写。
    ibatis我看来根本不是orm,只是sql语句映射工,最多再配合一个对象转换工具。

  6. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-10-11 13:41:00

    这个实现挺帅了,相当的,相当的。

  7. 老赵
    admin
    链接

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

    @JimLiu
    是啊,我也很满意。
    所以各位NH大牛要多现身多给我点提示,我还是挺追求完美的,正好可以研究这些。

  8. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-10-11 14:48:00

    @Jeffrey Zhao
    对了,不知道关于DDD方向,老赵有什么好推荐的书籍呢?

  9. andy.wu
    *.*.*.*
    链接

    andy.wu 2009-10-11 20:52:00

    Jeffrey Zhao:
    ibatis我看来根本不是orm,只是sql语句映射工,最多再配合一个对象转换工具。



    呵呵,ibatis自己就是反复这么强调的。

  10. lonelybrother
    117.80.164.*
    链接

    lonelybrother 2010-11-21 10:59:43

    赵老师,是否用过nh3连接sqlite,我试了多次,都没有成功. 报错为: //无法将类型为“Finisar.SQLite.SQLiteConnection”的对象强制转换为类型“System.Data.Common.DbConnection

    您是否做过,而nh2的最低版本是可以的,个人认为:nh2.nh3对sqlite的支持可能没怎么测试?

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我