• 用NHibernate处理带属性的多对多关系


    1、引言

    老谭在面试开发者的时候,为了考察他们的数据库开发能力,经常祭出我的法宝,就是大学数据库教程中讲到的一个模式:学生选课。这个模式是这种:

    在这个模式中,学生(Student)和课程(Course)都是实体。分别有主键Id。考试成绩(Score)是学生和课程之间的多对多关系。

    基于这个模式,对于新手,能够出一些简单查询要求,对于熟手,能够出一些复杂的查询要求,用起来得心应手。

    但今天要说的重点是。怎么用NHibernate实现这个模式。和一般多对多关系稍有不同的是,这个关系带有一个属性,就是考试成绩(Score)。

    假设多对多关系上带有属性,我们通常会把这个关系扩充为实体。也就是把一个多对多关系转换为一个实体加上两个多对一关系。

    假设多对多关系上有多个属性,将其转换为一个实体还是有必要的,但仅仅有非常少的属性(本例中仅仅有一个),转换为实体实在有些浪费。由于普通情况下,对于实体,我们要为其创建一个人工主键,有了人工主键,又要创建一个序列。在映射时,这个实体自然要有相应的类。这一大堆事情,想想就非常麻烦。

    问题的关键是。在概念上这本来就是一个关系,为了实现的方便,而将其转换为一个实体。这个凭空多出来的实体。使概念变得复杂。很别扭。

    因此,这里要探究一下,让关系恢复为为关系,怎么用NHibernate来处理。

    2、映射

    假设关系表Score中没有Score那个属性字段,Student实体能够映射为这种类:

    public class Student
    {
        public virtual long Id { get; set; }
    
        public virtual string Name { get; set; }
    
        public virtual IList<Course> Courses { get; set; }
    }

    Course也类似。

    但有了属性Score,再这样映射就把这个属性丢了。为了带上Score属性,两个实体应该这样映射:

    public class Student
    {
        public virtual long Id { get; set; }
    
        public virtual string Name { get; set; }
    
        public virtual IDictionary<Course, int> Courses { get; set; }
    }

    public class Course
    {
        public virtual long Id { get; set; }
    
        public virtual string Name { get; set; }
    
        public virtual IDictionary<Student, int> Students { get; set; }
    }

    原来的列表(List)变成了字典(Dictionary),字典的主键是原来列表中的元素,值则是关系上的属性值,即考试成绩。

    相应的映射文件自然也要调整,结果例如以下:

    ...
    <class name="Course" table="Course">
    
      <id name="Id" unsaved-value="0">
        <column name="Id" />
        ...
      </id>
    
      <property name="Name">
        <column name="Name"/>
      </property>
    
      <map name="Students" table="Score" lazy="true">
        <key column="CourseId"/>
        <index-many-to-many column="StudentId" class="Student" />
        <element column="Score" />
      </map>
    </class>

    ...
    <class name="Student" table="Student">
         
      <id name="Id" unsaved-value="0">
          <column name="Id" /> 
          ...
      </id>
    
      <property name="Name">
        <column name="Name"/>
      </property>
    
      <map name="Courses" table="Score" lazy="true">
        <key column="StudentId"/>
        <index-many-to-many column="CourseId" class="Course"/>
        <element column="Score" />
      </map>
    </class>

    经过这种映射。多对多关系中的属性带到了类中,并且避免了为关系创建实体——没有Score这种类。对这样映射结果的操作,和常规多对多关系映射方式多生成的基本类似。但也有几个要注意的问题。


    3、查询

    对于一些简单的查询,如:

    • 小明全部课程的成绩;
    • 化学这门课全部学生的成绩等

    都比較easy处理,由于这仅仅须要在一个实体上加过滤条件。

    假设须要在两个实体上加过滤条件,如查询小明化学课的成绩,就有些复杂。其查询语句Hql是这种:

    select score
      from Course c, Student s
      join c.Students score
     where c.Name='化学'
       and index(score) = s.Id
       and s.Name='小明'

    这里出现了我们非常少用到的hql函数:index()。这是专门应对map的,其目的是获得map的索引字段。

    吊诡的是。尽管我们指明,map的key是对象。如Course.Students的key是Student的对象,但index()的结果,仍然是 <index-many-to-many column="..." class="..."/> 中 column字段的值。在上面的查询中。index(score),我们期望的结果是Student对象,但结果却是对象的Id。因此。在查询语句中,我们不得不关联上Student s,并利用 s.Name 进行过滤。

    即便是“简单的查询”,如查询小明全部课程的成绩,也有一个问题要注意。

    这个问题和懒载入相关。

    在这个查询中。我们已经知道须要获取全部课程。因此,希望进行预先载入:

     from Student s
     join fetch s.Courses course
    where s.Name='小明'

    得到结果后,假设脱离查询的环境,如释放Session,在訪问课程时,如:

    s.Courses.Select(c => c.Key.Name)
    仍会抛出异常。由于,上述的查询并没有把Course对象载入进来。
    眼下还不知道怎么预载入map中索引对象。

    须要的话,仅仅有依赖懒载入机制。

    4、维护

    除了查询,在对关系进行维护时,也有一点值得特别注意:save-update类型的cascade无效。

    cascade属性,为我们进行关系维护带来不少便利。

    在常规(不带属性)的多对多关系中。我们的维护操作能够是这种:

    小明选化学课:

    using (var tx = session.BeginTransaction())
    {
        var student = GetStudent(studentName) ??

    new Student { Name = studentName, Courses = new List<Course>() }; var course = GetCourse(courseName) ?? new Course { Name = courseName, Students = new List<Student>() }; if (!course.Students.Contains(student)) { course.Students.Add(student); } session.SaveOrUpdate(course); tx.Commit(); }

    当中,GetStudent(studentName) 和 GetCourse(courseName) 各自是指,依据学生名字或课程名字从数据库中载入对应对象。

    在上面的代码片段中,假设数据库中没有,则新建。

    维护关系后,进行保存。

    须要注意的是,在保存 course 的时候,并没有确保 student 是一个持久化的对象。假设student尚未被持久化,则在保存时,NHibernate会自己主动保存。并维护和course的关系。可以这么做。依赖于Course中关系上的cascade属性定义(第三行末尾):

      <class name="Course" table="Course">
        ...
        <bag name="Students" table="Scoure" lazy="true" cascade="save-update">
          <key column="CourseId"/>
          <many-to-many column="StudentId" class="Student" />
        </bag>
      </class>

    但在包括属性的多对多关系上。因为要使用map。就无法进行这种配置——配置了也不生效。

    假设数据库中尚未保存该学生。我们不得不首先创建并将其持久化(第7行):

    using (var tx = session.BeginTransaction())
    {
        var student = GetStudent(studentName);
        if (student == null)
        {
            student = new Student {Name = studentName, Courses = new Dictionary<Course, int>()};
            session.Save(student);
        }
    
        var course = GetCourse(courseName) ??
                     new Course {Name = courseName, Students = new Dictionary<Student, int>()};
    
        if (course.Students.ContainsKey(student))
        {
            course.Students[student] = score;
        }
        else
        {
            course.Students.Add(student, score);
        }
    
        session.SaveOrUpdate(course);
    
        tx.Commit();
    }
    

    否则,就等着NHibernate抛出异常吧。

    5、结论

    用Map/Dicitionary表达的多对多关系,要比用Bag/List所表达的,操作起来更为复杂。

    但这种代价。我们乐意承担。

    这是由于。我们更看重模型设计,更重视概念完整性。是模型决定详细实现,而不是反过来。依据详细实现来改动模型的设计。



    
  • 相关阅读:
    MYSQL进阶学习笔记十:MySQL慢查询!(视频序号:进阶_23-24)
    MYSQL进阶学习笔记九:MySQL事务的应用!(视频序号:进阶_21-22)
    MYSQL学习拓展一:MySQL 存储过程之游标的使用!
    MYSQL进阶学习笔记八:MySQL MyISAM的表锁!(视频序号:进阶_18-20)
    linux初级学习笔记十:linux grep及正则表达式!(视频序号:04_4)
    linux初级学习笔记九:linux I/O管理,重定向及管道!(视频序号:04_3)
    MYSQL进阶学习笔记七:MySQL触发器的创建,应用及管理!(视频序号:进阶_16,17)
    linux shell 字符串操作
    How to Install JAVA 8 (JDK/JRE 8u111) on Debian 8 & 7 via PPA
    iptables Data filtering详解
  • 原文地址:https://www.cnblogs.com/liguangsunls/p/6811201.html
Copyright © 2020-2023  润新知