Skip to main content

配置显式关联

场景分析

我们先来分析一个关联关系,老师与班级之间的教学关系。在这一关联中,老师与班级是两个参与方,均为关联端,“教学”是这一关联关系的实际内容。一位老师通常会教授多个班级,而一个班级通常也有多位老师,因此这是一个“多对多”关联。 我们再来分析一个特殊的属性“是否班主任”,即某一位老师是否为某一班级的班主任。我们第一印象可能觉得它应该是老师的属性,但仔细分析你会发现事实并非如此简单,因为某一老师是这个班的班主任,但对于他所任教的另一班级他可能不是班主任。也就是说,“是否班主任”这个属性不能是某个对象的属性,而应该是“教学”这个关联本身所具有的属性,离开了“教学”关系谈论是否为班主任,是没有意义的。

附加说明

显式关联的实例就是关联类的实例,即关联对象。

下面我们建立一个包含显式关联的领域模型,该模型包含老师和班级两个类,包含老师和班级之间的“教学”关联,该关联有一个属性“是否班主任”。“班级”类的代码上一节已给出,“老师”类和“教学”关联类的代码如下。

/// <summary>
/// 表示教师
/// </summary>
public class Teacher
{
/// <summary>
/// 教师ID
/// </summary>
public long TeacherId {get;set;}

/// <summary>
/// 老师名称
/// </summary>
public string Name {get;set;}
}

/// <summary>
/// “教学”关联类
/// </summary>
public class Teaching
{
/// <summary>
/// 班级
/// </summary>
public virtual Class Class {get;set;}

/// <summary>
/// 教师
/// </summary>
public virtual Teacher Teacher {get;set;}

/// <summary>
/// 是否是班主任
/// </summary>
public bool IsManage {get;set;}
}

我们看到,在关联类Teaching中定义两个指针Teacher和Class,它们分别指向两个关联端(对象)。在上一节我们已经说明过,关联端是指参与关联关系的对象,不过在显式关联中,它还引申为定义在关联对象内部指向关联端的指针。在不引起歧义的情况下,我们可以在引申的意义上称Teacher和Class为Teaching的两个关联端。 在这里我们没有定义ClassId和TeacherId这样的存储两端键属性的属性,但是如果需要对关联引用使用延迟加载的话,是必须要定义的。

附加说明

通常,我们会在关联类中定义访问关联端的属性(Property),但这仅仅只是习惯。你可以采用其它替代方案,如构造函数传入、方法赋值等等。Obase也不强制要求您定义这种属性(Property)。

附加说明

我们有时会在关联类上定义表示关联端标识的属性,如上述示例的Teaching类可以定义ClassId和TeacherId分别表示参与关联的班级和老师的ID。这类属性可以视为特殊的关联属性,虽然从概念层面来说它们确实不是关联的特性。 在以下两种情景下,采用这种方式可以提升性能: (1)创建关联对象时,端对象还未实施反持久化,但你已经知道了它们的ID,并且后续也不需要用到端对象,因此没有必要仅仅为了创建关联对象而查询数据库; (2)从数据库查询关联对象,但后续不需要(或不一定需要)使用端对象,等到确实需要时再通过延迟加载机制加载端对象。 除非是上述两种情况,我们不建议在关联类中定义这类属性。_

我们再来回顾一下“关联引用”这一概念,它是定义在关联端(对象)内部的指针,通过它我们可以沿关联关系导航,即从关联一端访问到另一端。同样的,我们也可以在显示关联的端对象中定义关联引用,不过它不会直接指向关联另一端,而是指向关联对象(即关联类的实例)。

附加说明

基于显示关联定义的关联引用,称为显式引用;基于隐式关联定义的关联引用,称为隐式引用。显式引用直接引用关联实例;而隐式引用不会引用关联实例(因为对象系统中没有物理存在的关联实例),而是引用关联另一端,将当前对象与隐式引用的对象配对即可间接得到关联实例。

由此可见,如果我们要基于显式关联实现对象导航,应该采取两步:首先基于关联引用从一端访问到关联对象,然后基于关联端(指针)访问到另一端。以下代码显示我们在Class类中增加基于“教学”关联定义的关联引用,由于“教学”关联是“多对多”的,因此它是一个集合。

/// <summary>
/// 班级的教师
/// </summary>
private List<Teaching> _classTeachers;

/// <summary>
/// 班级的教师
/// </summary>
public virtual List<Teaching> ClassTeachers
{
get => _classTeachers;
}

与隐式关联一样,显式关联也有伴随映射(存储)和独立映射(存储)两种映射方式。显然,由于显式关联具有自己的属性,如果采用伴随映射,需要在伴随端的映射表中增加相应字段来存储关联属性。“教学”关联是多对多的,因此我们采用独立映射。

注册显式关联

在Obase中注册显式关联的方式与上节讲到的隐式关联类似,如以下代码所示:

//注册实体型
modelBuilder.Entity<Teacher>();
var classsEntity = modelBuilder.Entity<Class>();

//注册关联型
var teachingAssociation = modelBuilder.Association<Teaching>();
//注册关联端 分别为两个关联端配置映射
teachingAssociation.AssociationEnd(p => p.Class).HasEntityType<Class>()
.HasMapping(p => p.ClassId, "ClassId")
.HasDefaultAsNew(true);
teachingAssociation.AssociationEnd(p => p.Teacher).HasEntityType<Teacher>()
.HasMapping(p => p.TeacherId, "TeacherId")
.HasDefaultAsNew(true);
//注册“班级”对于“教学”关联的引用
var refTeacher = classsEntity.AssociationReference<Teaching>("ClassTeachers",true);
//显式关联引用只需要设置左端
refTeacher.HasLeftEnd(p => p.Class);

这里有个新的配置选项:HasDefaultAsNew,这个选项默认为False,在设置为True后,新增显式关联时会同时尝试将两端对象也作为新对象进行保存。 当然,这些配置也可以简化为如下形式:


//注册实体型
modelBuilder.Entity<Teacher>();
var classsEntity = modelBuilder.Entity<Class>();

//注册关联型
var teachingAssociation = modelBuilder.Association<Teaching>();

//注册关联端 设置为端默认附加新对象
teachingAssociation.AssociationEnd(p => p.Class).HasDefaultAsNew(true);
teachingAssociation.AssociationEnd(p => p.Teacher).HasDefaultAsNew(true); ;

最后记着在对象上下文中定义相应的对象集合。

新增关联对象

以下代码首先新增了一个班级和两个老师,然后将班级分别与两个老师建立“教学”关联。

var context = new DbContext();

//新增一个班级
var newclass = new Class
{
Name = @"某某班"
};

//新两个增老师
var teacher1 = new Teacher { Name = "老师1" };
var teacher2 = new Teacher { Name = "老师2" };

//与第一个老师建立关联
var teaching1 = new Teaching
{
Class = newclass,
Teacher = teacher1,
IsManage = true
};

//与第二个老师建立关联
var teaching2 = new Teaching
{
Class = newclass,
Teacher = teacher2,
IsManage = false
};

//持久化
context.ClassTeachers.Attach(clasTeacher1);
context.ClassTeachers.Attach(clasTeacher2);
context.SaveChanges();

这样我们就同时新增了一个班级,两名教师和他们之间的关联对象,如果DefaultAsNew设置为False,则此处需要自己手动附加教师和班级对象。

删除关联对象

与隐式关联一样,您可以通过为关联引用设值或者从引用集合中移除项来删除关联实例(即关联对象),此种方式较为复杂,在关联端配置为延迟加载的情况可能需要访问关联端对象以保证在保存更改时可以追踪,我们推荐使用直接从上下文中移除关联对象。

var context = new ClassAndTeacherContext();
//查询对象
var cla = context.ClassTeachers.Include(p=>p.Class).Include(p => p.Teacher).FirstOrDefault();
//移除
context.ClassTeachers.Remove(cla);

context.SaveChanges();

在查询时强制包含两个关联端,之后从上下文内移除即可。

查询关联对象

在上下文中增加对应的集合:

/// <summary>
/// 教学关系集合
/// </summary>
public ObjectSet<Teaching> ClassTeachers { get; set; }

这样就可以从数据库查询关联对象了。

加载关联端

var context = new ClassAndTeacherContext();
//查询对象
var cla = context.Classes.FirstOrDefault();
if (cla != null)
//输出
foreach (var teaching in cla.ClassTeachers)
Console.WriteLine(teaching);

这样就可以通过关联类导航至关联的另外一端,当然我们也可以使用Include直接强制包含此关联引用。

        cla = context.Classes.Include(p=>p.ClassTeachers).FirstOrDefault();
if (cla != null)
//输出
foreach (var teaching in cla.ClassTeachers)
{
Console.WriteLine(teaching.Class);
Console.WriteLine(teaching.Teacher);
}

投影到关联端

和Linq的投影类似,我们可以对显式关联型的关联端进行投影,之前在新手进阶中讲过关联投影可以分为多重投影和合并投影,那么我们现在也可以得到对关联端的多重投影和合并投影。

var context = new ClassAndTeacherContext();
//查询对象
var classes = context.ClassTeachers.Select(p => p.Class).ToList();
var teachers = context.ClassTeachers.Select(p => p.Teacher).ToList();

//classes的投影结果是内含一个班级的列表
foreach (var cla in classes)
{
Console.WriteLine(cla);
}
//teachers的投影结果是内含两个件教师的列表
foreach (var tea in teachers)
{
Console.WriteLine(tea);
}

多方显式关联

考察学校上课的场景,其参与方自然有班级和老师两类对象。同时,上课需要教室,我们考虑“走班制”情形,一个班级的上课教室是不固定的,那么就必须把“教室”纳入到这一关联中,因此“上课”是一个三方关联。在这一关联中,一个班级和一个老师会在多个教室上课(即虽为同一学科但每次课的教室也不固定),一个老师会在同一教室给多个班级上课,一个班级会在同一教室接受多个老师授课(不同学科老师不一样),可见“上课”是* : * : *关联。 进一分析业务场景,学校规定,老师须在每次上课前点名,记录缺勤数,系统将根据班级人数计算缺勤率。这样,“上课”关联便需要两个属性:缺勤数、缺勤率,因此它应作为显式关联,应当在领域模型中定义关联类HavingClass。

/// <summary>
///教室
/// </summary>
public class Room
{

/// <summary>
///学校名称
/// </summary>
public string Name {get;set;}

/// <summary>
///教室ID
/// </summary>
public long RoomId {get;set;}
}
/// <summary>
///走班制上课
/// </summary>
public class HavingClass
{

/// <summary>
///班级
/// </summary>
public virtual Class Class {get;set;}

/// <summary>
///教师
/// </summary>
public virtual Teacher Teacher {get;set;}

/// <summary>
///班级ID
/// </summary>
public long ClassId {get;set;}

/// <summary>
///教师ID
/// </summary>
public long TeacherId {get;set;}

/// <summary>
///缺勤人数
/// </summary>
public int AbsenceCount {get;set;}

/// <summary>
///缺勤率 百分之多少
/// </summary>
public int AbsenceRate {get;set;}

/// <summary>
///教室ID
/// </summary>
public long RoomId {get;set;}
/// <summary>
///教室
/// </summary>
public virtual Room Room {get;set;}
}
/// <summary>
///表示班级
/// </summary>
public class Class
{
/// <summary>
///班级id
/// </summary>
public long ClassId {get;set;}

/// <summary>
///班级名称
/// </summary>
public string Name {get;set;}

/// <summary>
///班级的课程
/// </summary>
public virtual List<HavingClass> HavingClasses {get;set;}
}

这里的Class只是将之前的Teaching换成了HavingClass,其余不变;新增了HavingClass和Room类。 多方显式关联的注册及配置与二方显式关联没有区别,只是需要多配置关联端。

//注册实体型
modelBuilder.Entity<Teacher>();
var classConfiguration = modelBuilder.Entity<Class>();
modelBuilder.Entity<Room>();

//注册关联型
var havingClassAssociation = modelBuilder.Association<HavingClass>();

//注册关联端 设置为端默认附加新对象
havingClassAssociation.AssociationEnd(p => p.Class).HasDefaultAsNew(true);
havingClassAssociation.AssociationEnd(p => p.Teacher).HasDefaultAsNew(true);
havingClassAssociation.AssociationEnd(p => p.Room).HasDefaultAsNew(true);
//多方显示关联需要配置关联引用和左端
var associationReference = classConfiguration.AssociationReference<HavingClass>("HavingClasses", true);
associationReference.HasLeftEnd(p => p.Class);

配置完成后,多方显式关联的新增、删除,关联端加载及投影都与二方显式关联无异,概从略。

自关联

如果关联的各参与方(不一定只有两个)是同一个类型,这种关联被称为自关联。自然而然,自关联也有隐式与显式之分。 区域类是对现实世界中行政区划的抽象,一个省下有若干市,一个市下有若干区县,于是我们得到了如下的一个区域类,区域包含对子区域引用_subAreas,还包含对父区域引用_parentArea,这两个引用都是关联引用,是基于“行政上下级”关联关系定义的。这一关联的两端都是区域,作为“上级”端它需要引用的另一端是下级,作为“下级”端它需要引用的另一端是上级。

/// <summary>
///表示一个区域
/// </summary>
public class Area
{
/// <summary>
///区域代码
/// </summary>
public string Code {get;set;}

/// <summary>
///父级区域代码
/// </summary>
public string ParentCode {get;set;}

/// <summary>
///父级区域
/// </summary>
public virtual Area ParentArea {get;set;}

/// <summary>
///子区域
/// </summary>
public virtual List<Area> SubAreas {get;set;}

/// <summary>
///友好区域
/// </summary>
public virtual List<FriendlyArea> FriendlyAreas {get;set;}
/// <summary>
///名字
/// </summary>
public string Name {get;set;}
}

如果两个区域建立了“友好”关系,且我们需要记录建立关系的时间,那么就存在一个显式自关联,如以下代码所示。

/// <summary>
///友好区域
/// </summary>
public class FriendlyArea
{
/// <summary>
///区域
/// </summary>
public virtual Area Area {get;set;}

/// <summary>
///友好区域
/// </summary>
public virtual Area Friend {get;set;}

/// <summary>
///友好关系开始时间
/// </summary>
public DateTime StartTime {get;set;}
}

这里我们仅写出他们简化后的配置,完全手工配置可以参考之前配置的显式和隐式关联,内容大同小异:

//注册区域作为模型
var areaCfg = modelBuilder.Entity<Area>();
areaCfg.HasKeyAttribute(p => p.Code).HasKeyIsSelfIncreased(false);

//注册区域和区域间的关联型
modelBuilder.Association<Area, Area>();

//注册友好区域作为关联型
var friendlyAreaAss = modelBuilder.Association<FriendlyArea>();
friendlyAreaAss.ToTable("FriendlyArea");
friendlyAreaAss.AssociationEnd(p => p.Area).HasEntityType<Area>()
.HasMapping("Code", "AreaCode");
friendlyAreaAss.AssociationEnd(p => p.Friend).HasEntityType<Area>()
.HasMapping("Code", "FriendlyAreaCode");


//注册友好区域的关联引用
var refFriendlyArea = areaCfg.AssociationReference<FriendlyArea>("FriendlyAreas", true);
refFriendlyArea.HasLeftEnd(p => p.Area);
refFriendlyArea.HasRightEnd(p => p.Friend);

再加上区域类和友好区域关联类的对象集合:

/// <summary>
///区域对象上下文
/// </summary>
public class AreaContext : ObjectContext<AreaContextConfiger>
{
/// <summary>
///区域
/// </summary>
public ObjectSet<Area> Areas { get; set; }

/// <summary>
///友好区域
/// </summary>
public ObjectSet<FriendlyArea> FriendlyAreas { get; set; }
}

接下来我们就可以对其操作了,和一般的隐式关联及显式关联类似,这里仅列出如何添加和查询,其余方法读者们可以自己编写代码。

//对象上下文
var context = new AreaContext();
var area1 = new Area()
{
Code = "P1",
Name = "某某省"
};

var area2 = new Area()
{
Code = "C1",
Name = "某某市A",
ParentCode = "P1"
};

var area3 = new Area()
{
Code = "C2",
Name = "某某市B",
ParentCode = "P1"
};

var friendly = new FriendlyArea()
{
Area = area2,
AreaCode = area2.Code,
Friend = area3,
FriendlyAreaCode = area3.Code,
StartTime = DateTime.Now
};

context.Areas.Attach(area1);
context.Areas.Attach(area2);
context.Areas.Attach(area3);
context.FriendlyAreas.Attach(friendly);

context.SaveChanges();

这样我们就添加了一个省,下面有两个市,并且这A市将B市列为了友好城市.其他的操作(修改、删除、查询)和之前的隐式关联和显示关联类似,这里不再赘述。

由上可以看出,其实自关联和一般的隐式关联或显式关联并没有区别,只是在配置关联端的映射时要理清映射关系,读者可以多思考这个区域的例子,自行写出类似的自关联。