用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、查询

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

  • 小明所有课程的成绩;
  • 化学这门课所有学生的成绩等

都比较容易处理,因为这只需要在一个实体上加过滤条件。

如果需要在两个实体上加过滤条件,如查询小明化学课的成绩,就有些复杂,其查询语句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所表达的,操作起来更为复杂。但这样的代价,我们乐意承担。

这是因为,我们更看重模型设计,更重视概念完整性。是模型决定具体实现,而不是反过来,根据具体实现来修改模型的设计。



时间: 2024-10-22 19:43:57

用NHibernate处理带属性的多对多关系的相关文章

[NHibernate]多对多关系(关联查询)

目录 写在前面 文档与系列文章 多对多关系关联查询 总结 写在前面 上篇文章介绍了nhibernate中对一对多关系进行关联查询的几种方式,以及在使用过程需要注意的问题.这篇文章对多对多关系的查询处理也采用上篇文章的描述方式进行说明. 文档与系列文章 [Nhibernate]体系结构 [NHibernate]ISessionFactory配置 [NHibernate]持久化类(Persistent Classes) [NHibernate]O/R Mapping基础 [NHibernate]集合

NHibernate之旅(11):探索多对多关系及其关联查询

本节内容 多对多关系引入 多对多映射关系 多对多关联查询 1.原生SQL关联查询 2.HQL关联查询 3.Criteria API关联查询 结语 多对多关系引入 让我们再次回顾在第二篇中建立的数据模型: 在图上,我已经清晰的标注了表之间的关系,上两篇分析Customer和Order之间的"外键关系"或者称作"父子关系"."一对多关系"和关联查询,这一篇以Order为中心,分析Order和Product之间的关系,直接看下面一幅图的两张表: 上面两

[转]NHibernate之旅(11):探索多对多关系及其关联查询

本节内容 多对多关系引入 多对多映射关系 多对多关联查询 1.原生SQL关联查询 2.HQL关联查询 3.Criteria API关联查询 结语 多对多关系引入 让我们再次回顾在第二篇中建立的数据模型: 在图上,我已经清晰的标注了表之间的关系,上两篇分析Customer和Order之间的“外键关系”或者称作“父子关系”.“一对多关系”和关联查询,这一篇以Order为中心,分析Order和Product之间的关系,直接看下面一幅图的两张表: 上面两张表关系表达的意思是:Order有多个Produc

01-08-03【Nhibernate (版本3.3.1.4000) 出入江湖】二级缓存:NHibernate自带的HashtableProvider之缓存管理

http://www.cnblogs.com/lyj/archive/2008/11/28/1343418.html 管理NHibernate二级缓存 NHibernate二级缓存由ISessionFactory创建并由ISessionFactory自行维护.我们使用NHibernate操作数据时,ISessionFactory能够自动同步缓存,保证缓存的有效性.但是当我们批量操作数据时,往往NHibernate不能维护缓存持久有效.ISessionFactory提供了可编程方式的缓存管理方法.

创建带属性的XML文档

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; namespace 创建带属性的XML文档 { class Program { static void Main(string[] args) { XmlDocument doc = new XmlDocument(); XmlDe

带属性的字符串 NSMutableAttributedString/NSAttributedString

由于iOS7新出的NSTextStorge是NSMutableAttributedString的子类,所以要用好NSTextStorage,首先要学好NSMutableAttributedString和NSAttributedString. 按个人的理解,NSAttributedString是一个带有属性的字符串,通过该类可以灵活地操作和呈现多种样式的文字数据. alignment //对齐方式 firstLineHeadIndent //首行缩进 headIndent //缩进 tailInd

盒子自带属性

<style type="text/css"> *{ /*重置盒子的自带属性值*/ margin: 0; padding: 0; } h1,p{ background: yellow; } span,i{ /*盒子属性失效 对行标签*/ background: red; height: 100px; width: 100px; display: block; /*行元素转换成块元素*/ } </style> /*注意:浮动过后行元素可以设置宽高了 注意:marg

带属性页的对话框

以带2个属性页的对话框为例 (VS2008) 1. 在资源试图 添加三个 对话框.修改三个对话框的ID名字为合适的名字. 2. 修改两个属性页对话框的属性.  Style -- Child;  Border -- None . 3. 给三个对话框添加三个类.  都是CDialog的子类.  右击点击资源--> 添加类 即可. 4. 给基础对话框(我们这里表示用来包含属性页的哪个对话框)添加一个Tab Control 控件, 并且给Tab Control 添加控件变量m_tab 5. 在对话框的头

KVC设置系统自带属性,不管是不是私有的属性

KVC 可以设置系统自带属性,不管是不是私有的属性: 1, 2,