• NHibernate自定义集合类型(下):自动维护双向关系


    如果使用NHibernate自带的集合类型,其中一个问题就在于需要在代码中手动维护双向关系,迫使开发人员编写额外的代码。其实这就是集合自定义逻辑的一个应用方面。现在,既然我们已经得到了一个方便的自定义集合的解决方案,那么现在便把“自动维护双向关系”作为目标来实现一番,也算是一个非常典型的示例了。

    昨天是休息天,看文章的朋友比较少,如果您遗漏了上一篇的内容,不妨再阅读一次,对理解本文会有一定帮助。

    我们已经知道LINQ to SQL是如何自动维护双向关系的,它的做法是在集合被添加或删除元素时发起一个回调函数,而在回调函数内部对某些属性进行设置。我们也可以采用这种方式。不过在此之前,我们必须知道NHibernate在进行集合操作时的一些顺序,例如在加载父实体时,集合属性的set操作和集合元素的添加操作哪个在前,哪个在后。

    我们还是使用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 AnswerSet();
    
                return this.m_answers;
            }
            set
            {
                Console.WriteLine("Set Question.Answers");
                this.m_answers = value;
            }
        }
    }
    
    public class AnswerSet : HashedSet<Answer>
    {
        public override bool Add(Answer o)
        {
            if (base.Add(o))
            {
                Console.WriteLine("Add Answer");
                return true;
            }
    
            return false;
        }
    }

    我们在Question.Answers属性的Set操作与AnswerSet的Add操作中都添加了一些输出文本的代码。然后,我们使用下面的代码进行测试:

    [Fact]
    public void LazyLoad()
    {
        var session = SessionFactory.Instance.OpenSession();
    
        var question = session.Get<Question>(1);
        question.Answers.Add(new Answer { Name = "Answer", Question = question });
    }
    
    [Fact]
    public void EagerLoad()
    {
        var session = SessionFactory.Instance.OpenSession();
        var question = session
            .CreateCriteria<Question>()
            .Add(Expression.IdEq(1))
            .SetFetchMode("Answers", FetchMode.Eager)
            .UniqueResult<Question>();
    
        question.Answers.Add(new Answer { Name = "Answer", Question = question });
    }

    LazyLoad和EagerLoad分别测试的是延迟加载和“饥渴”加载两种情况下的操作顺序。从输出内容里可以发现,两种情况下结果完全相同:

    Set Question.Answers
    Add Answer
    Add Answer
    Add Answer

    由于原本数据库中该Question有2个Answer对象,因此会输出三条Add Answer信息。可以看出,NHibernate会先设置集合类型,再向其中添加元素。这对我们来说是一个好消息,因为我们可以放心地在Question.Answers的set操作中添加回调函数,然后等待Answer对象一个个添加进来,我们的逻辑为它们一一建立关联。

    为了“回调”,我们可以定义一个通用的ObservableSet<T>:

    public enum ItemChangedType
    {
        Added,
        Removed
    }
    
    public class ItemChangedEventArgs<T> : EventArgs
    {
        public ItemChangedEventArgs(ItemChangedType type, T item)
        {
            this.Type = type;
            this.Item = item;
        }
    
        public ItemChangedType Type { get; private set; }
    
        public T Item { get; private set; }
    }
    
    public interface IObservableSet<T> : ISet<T>
    {
        event EventHandler<ItemChangedEventArgs<T>> ItemChanged;
    }
    
    public class ObservableSet<T> : HashedSet<T>, IObservableSet<T>
    {
        public override bool Add(T o)
        {
            if (base.Add(o))
            {
                var e = new ItemChangedEventArgs<T>(ItemChangedType.Added, o);
                this.OnItemChanged(e);
                return true;
            }
    
            return false;
        }
    
        public override bool Remove(T o)
        {
            if (base.Remove(o))
            {
                var e = new ItemChangedEventArgs<T>(ItemChangedType.Removed, o);
                this.OnItemChanged(e);
                return true;
            }
    
            return false;
        }
    
        public event EventHandler<ItemChangedEventArgs<T>> ItemChanged;
    
        protected void OnItemChanged(ItemChangedEventArgs<T> e)
        {
            var itemChanged = this.ItemChanged;
            if (itemChanged != null) itemChanged(this, e);
        }
    }

    显然,修改一个HashedSet元素内容的接口不止Add和Remove两个方法,于是在override的时候就又要缩手缩脚了。最终,我还是通过阅读HashedSet的源代码才意识到所有的接口最终都会调用Add和Remove方法。因此,我们只需要在Add和Remove的时候发起事件即可。于是在Question对象中:

    private IObservableSet<Answer> m_answers;
    public virtual IObservableSet<Answer> Answers
    {
        get
        {
            if (this.m_answers == null)
            {
                this.Answers = new ObservableSet<Answer>();
            }
    
            return this.m_answers;
        }
        set
        {
            this.ChangeAnswerSet(value);
        }
    }
    
    private EventHandler<ItemChangedEventArgs<Answer>> m_answerSetItemChangedHandler;
    
    private void ChangeAnswerSet(IObservableSet<Answer> answers)
    {
        if (this.m_answerSetItemChangedHandler == null)
        {
            this.m_answerSetItemChangedHandler = this.OnAnswerSetItemChanged;
        }
    
        if (this.m_answers != null)
        {
            this.m_answers.ItemChanged -= this.m_answerSetItemChangedHandler;
        }
    
        this.m_answers = answers;
        this.m_answers.ItemChanged += this.m_answerSetItemChangedHandler;
    }
    
    private void OnAnswerSetItemChanged(object sender, ItemChangedEventArgs<Answer> e)
    {
        ...
    }

    目前,在设置Question.Answers属性的时候,我们会为它添加事件处理函数,并且将旧的事件处理函数剥离。至于OnAnswerSetItemChanged中,便是维护Answer和Question的关系了:

    private void OnAnswerSetItemChanged(object sender, ItemChangedEventArgs<Answer> e)
    {
        var answer = e.Item;
    
        if (e.Type == ItemChangedType.Added)
        {
            if (answer.Question != null)
            {
                answer.Question.Answers.Remove(answer);
            }
    
            answer.Question = this;
        }
        else
        {
            answer.Question = null;
        }
    }

    至此,我们已经在Answer元素添加至Question.Answers集合时保持了逻辑。但是,我们还有两个东西没有搞定:

    • 如果外界有代码直接一锅端地设置Question.Answers集合,那么旧集合中的元素是否要和Question脱离关系?
    • 如果外界有人直接设置Answer.Question属性,是否要将其添加到Question.Answers集合中?

    严格说来,这些都是需要的。但是我在这里不选择这种做法,我选择——将Question.Answers集合和Answer.Question属性的set方法都设置为private!在OnAnswerSetItemChanged方法内部,就动用FastReflectionLib来设置answer.Question属性。至于NHibernate,它的操作都是可靠的,我们信任它。您想想看,这么做有什么问题吗?似乎真没有。我们在任何需要设置Answer.Question属性的地方,都可以通过操作Question.Answers集合来实现。

    最后的配置自然也是必不可少的:

    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, ObservableSet<Answer>, IObservableSet<Answer>>>()
                .KeyColumns.Add("QuestionID")
                .Cascade.All()
                .Inverse();
        }
    }

    当然,您是否觉得现在自动保持双向关系的做法非常繁琐?

    的确如此啊,那么,您是否可以解决(至少缓解)这个问题?

    相关文章

  • 相关阅读:
    git的冲突解决--git rebase之abort、continue、skip
    iOS 13苹果登录
    mac上python3.x安装 图文详解
    iOS Bezier曲线
    《从零开始学Swift》学习笔记(Day 23)——尾随闭包
    《从零开始学Swift》学习笔记(Day 22)——闭包那些事儿!
    《从零开始学Swift》学习笔记(Day 21)——函数返回值
    《从零开始学Swift》学习笔记(Day 20)——函数中参数的传递引用
    《从零开始学Swift》学习笔记(Day 19)——函数参数传递
    《从零开始学Swift》学习笔记(Day 18)——有几个分支语句?
  • 原文地址:https://www.cnblogs.com/aaa6818162/p/2427201.html
Copyright © 2020-2023  润新知