obase社区Obase开发者QQ群:962698871
obase

欢迎查阅
OBASE帮助文档

本节内容对应的Demo位于Obase.Test.Demo中Obase.Test/Advance,Obase.Test.Infrastructure/Advance和Obase.Test.Domain内,所使用的数据库表建表脚本(MySql版)位于Obase.Test.Infrastructure内。

上一节我们以学生与班级间的“隶属”关联为例,介绍了对象间的关联关系以及Obase对关联持久化的支持。而在实际的项目中,关联的复杂性无非如此简单,对象系统的复杂性其实主要体现为关联的复杂性。Obase的重大创新之一就是对这种复杂的关联关系实现了全面的支持。本节我们将介绍三种复杂关联。

显式关联

定义

我们先来分析一个关联关系,老师与班级之间的教学关系。在这一关联中,老师与班级是两个参与方,均为关联端,“教学”是这一关联关系的实际内容。一位老师通常会教授多个班级,而一个班级通常也有多位老师,因此这是一个“多对多”关联。 我们再来分析一个特殊的属性“是否班主任”,即某一位老师是否为某一班级的班主任。我们第一印象可能觉得它应该是老师的属性,但仔细分析你会发现事实并非如此简单,因为某一老师是这个班的班主任,但对于他所任教的另一班级他可能不是班主任。也就是说,“是否班主任”这个属性不能是某个对象的属性,而应该是“教学”这个关联本身所具有的属性,离开了“教学”关系谈论是否为班主任,是没有意义的。 如果一个关联自身还有属性(不限定属性个数),在面向对象模型中通常会定义一个专门的模型元素——即关联类——来代表并描述这个关联,这种关联我们称之为显式关联。而上一节介绍的“隶属”关联,我们在建立对象模型时不会为它定义专门的关联类,只是一个逻辑上的存在,这种关联我们称为隐式关联。

附加说明 显式关联的实例就是关联类的实例,即关联对象。

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

/// <summary>
///     表示教师
/// </summary>
public class Teacher
{
    /// <summary>
    ///     教师姓名
    /// </summary>
    private string _name;

    /// <summary>
    ///     教师ID
    /// </summary>
    private long _teacherId;

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

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

/// <summary>
///     “教学”关联类
/// </summary>
public class Teaching
{
    /// <summary>
    ///     班级
    /// </summary>
    private Class _class;

    /// <summary>
    ///     教师
    /// </summary>
    private Teacher _teacher;

    /// <summary>
    ///     是否是班主任
    /// </summary>
    private bool _isManage;

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

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

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

我们看到,在关联类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;
}

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

CREATE TABLE `teacher` (
  `TeacherId` bigint(20) NOT NULL AUTO_INCREMENT,
  `Name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL
  PRIMARY KEY (`TeacherId`)
) ENGINE=InnoDB 

CREATE TABLE `teaching` (
  `ClassId` bigint(20) DEFAULT NULL,
  `TeacherId` bigint(20) DEFAULT NULL,
  `IsManage` tinyint(4) DEFAULT NULL
) ENGINE=InnoDB

注册显式关联

在Obase中注册显式关联的方式与上节讲到的隐式关联类似,如以下代码所示(这里的Class是之前新手进阶中定义过的)。

//注册实体型
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>
    private string _name;

    /// <summary>
    ///     教室ID
    /// </summary>
    private long _roomId;


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

    /// <summary>
    ///     教室ID
    /// </summary>
    public long RoomId
    {
        get => _roomId;
        set => _roomId = value;
    }

    /// <summary>
    ///     转换为字符串表示形式
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"Room:{{RoomId-{_roomId},Name-\"{_name}\"}}";
    }
}
/// <summary>
///     走班制上课
/// </summary>
public class HavingClass
{
    /// <summary>
    ///     缺勤人数
    /// </summary>
    private int _absenceCount;

    /// <summary>
    ///     缺勤率 百分之多少
    /// </summary>
    private int _absenceRate;

    /// <summary>
    ///     班级
    /// </summary>
    private Class _class;

    /// <summary>
    ///     班级ID
    /// </summary>
    private long _classId;

    /// <summary>
    ///     教室
    /// </summary>
    private Room _room;

    /// <summary>
    ///     教室ID
    /// </summary>
    private long _roomId;

    /// <summary>
    ///     教师
    /// </summary>
    private Teacher _teacher;

    /// <summary>
    ///     教师ID
    /// </summary>
    private long _teacherId;

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

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

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

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

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

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

    /// <summary>
    ///     教室ID
    /// </summary>
    public long RoomId
    {
        get => _roomId;
        set => _roomId = value;
    }

    /// <summary>
    ///     教室
    /// </summary>
    public virtual Room Room
    {
        get => _room;
        set => _room = value;
    }


    /// <summary>
    ///     转换为字符串表示形式
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return
            $"Class:{{Class-{_class},Teacher-\"{_teacher}\",Room-\"{_room}\",AbsenceCount-\'{_absenceCount}\',AbsenceRate-\'{_absenceRate}%\'}}";
    }
}
/// <summary>
///     表示班级
/// </summary>
public class Class
{
    /// <summary>
    ///     班级id
    /// </summary>
    private long _classId;

    /// <summary>
    ///     班级名称
    /// </summary>
    private string _name;

    /// <summary>
    ///     班级的课程
    /// </summary>
    private List<HavingClass> _havingClasses;

    /// <summary>
    ///     班级id
    /// </summary>
    public long ClassId
    {
        get => _classId;
        set => _classId = value;
    }

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

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

    /// <summary>
    ///     转换为字符串表示形式
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"Class:{{ClassId-{_classId},Name-\"{_name}\"}}";
    }
}

这里的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);

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

多方隐式关联

仍然以上述“上课”关联为例,假如业务需求不考虑缺勤问题,那么就无须在领域模型中定义关联类,该关联即为隐式关联。 在Obase注册多方隐式关联时,其具体的配置与二方隐式关联有所区别。首先,需要定义一个承载该关联的类,该类的结构与上述关联类相似,只是不需要定义关联属性;然后,应将该关联配置为“显式”。这种配置方式称为隐式关联的显式化配置。 此外,基于多方隐式关联定义的关联引用其配置方式也有所不同。以下代码展示的是定义在Class类中的关联引用_havingClassRecords,它引用当前班级的历史上课记录,是一个Tuple< Teacher, Room >集合,每一元素代表一次上课,由任科老师和教室聚合而成。

/// <summary>
///     表示班级
/// </summary>
public class Class
{
    /// <summary>
    ///     班级id
    /// </summary>
    private long _classId;

    /// <summary>
    ///     班级名称
    /// </summary>
    private string _name;

    /// <summary>
    ///     班级的课程记录
    /// </summary>
    private List<Tuple<Teacher, Room>>  _havingClassRecords;

    /// <summary>
    ///     班级id
    /// </summary>
    public long ClassId
    {
        get => _classId;
        set => _classId = value;
    }

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

    /// <summary>
    ///     班级的课程
    /// </summary>
    public virtual List<Tuple<Teacher, Room>>  HavingClassRecords
    {
        get => _havingClassRecords;
        set => _havingClassRecords = value;
    }

        /// <summary>
    ///     添加记录
    /// </summary>
    /// <param name="havingClass"></param>
    public void AddHavingClassRecords(HavingClass havingClass)
    {
        if (_havingClassRecords == null)
            _havingClassRecords = new List<Tuple<Teacher, Room>>();
        if(havingClass.Class.ClassId == _classId)
            _havingClassRecords.Add(new Tuple<Teacher, Room>(havingClass.Teacher, havingClass.Room));
    }

    /// <summary>
    ///     获取记录
    /// </summary>
    /// <returns></returns>
    public List<HavingClass> GetHavingClassRecords()
    {
        return _havingClassRecords?.Select(p => new HavingClass()
        { ClassId = ClassId, Class = this, TeacherId = p.Item1.TeacherId, Teacher = p.Item1, RoomId = p.Item2.RoomId, Room = p.Item2 }).ToList();
    }

    /// <summary>
    ///     转换为字符串表示形式
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"Class:{{ClassId-{_classId},Name-\"{_name}\"}}";
    }
}
/// <summary>
///     走班制上课
/// </summary>
public class HavingClass
{
    /// <summary>
    ///     班级
    /// </summary>
    private Class _class;

    /// <summary>
    ///     班级ID
    /// </summary>
    private long _classId;

    /// <summary>
    ///     教室
    /// </summary>
    private Room _room;

    /// <summary>
    ///     教室ID
    /// </summary>
    private long _roomId;

    /// <summary>
    ///     教师
    /// </summary>
    private Teacher _teacher;

    /// <summary>
    ///     教师ID
    /// </summary>
    private long _teacherId;

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

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

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

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

    /// <summary>
    ///     教室ID
    /// </summary>
    public long RoomId
    {
        get => _roomId;
        set => _roomId = value;
    }

    /// <summary>
    ///     教室
    /// </summary>
    public virtual Room Room
    {
        get => _room;
        set => _room = value;
    }
}

这里仅列出修改后的HavingClass和Class类,其他类与显式多方关联相较无修改。 由于_havingClassRecords是隐式引用,它指向关联端而不是关联实例,但我们已将其关联实施了显式化配置,而显式引用指向的应该是关联实例,为了遵循这一原则,我们需要为该关联引用配置特殊的取值器和设值器。

//注册实体型
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>("HavingClassRecords", true);
associationReference.HasLeftEnd(p => p.Class);

//此处需要为这个关联引用设置设值器 因为这个关联的引用的关联型是HavingClass 但他的引用类型是Tuple<Teacher, Room> 需要进行转换
//设值器是相对于属性来说的 即调用属性的Set方法为属性设置值
//第一个参数是个委托 指示如何从HavingClass转为List<Tuple<Teacher, Room>> 这里使用了一个Class类定义的方法也可以使用Lambda表达式
//第二个是模式 对于多重的关联对象 为追加模式 否则为赋值模式
//这里使用了一个类内方法作为他的自定义取值器
    associationReference.HasValueSetter<HavingClass>((cla, havingClass) =>
    cla.AddHavingClassRecords(havingClass), eValueSettingMode.Appending)
.HasValueGetter(typeof(Class).GetMethod("GetHavingClassRecords"));
            

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

自关联

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

/// <summary>
///     表示一个区域
/// </summary>
public class Area
{
    /// <summary>
    ///     区域代码
    /// </summary>
    private string _code;

    /// <summary>
    ///     父级区域代码
    /// </summary>
    private string _parentCode;

    /// <summary>
    ///     父级区域
    /// </summary>
    private Area _parentArea;

    /// <summary>
    ///     子区域
    /// </summary>
    private List<Area> _subAreas;

    /// <summary>
    ///     友好区域
    /// </summary>
    private List<FriendlyArea> _friendlyAreas;

    /// <summary>
    ///     名字
    /// </summary>
    private string _name;

    /// <summary>
    ///     区域代码
    /// </summary>
    public string Code
    {
        get => _code;
        set => _code = value;
    }

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

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

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

    /// <summary>
    ///     友好区域
    /// </summary>
    public virtual List<FriendlyArea> FriendlyAreas
    {
        get => _friendlyAreas;
        set => _friendlyAreas = value;
    }

    /// <summary>
    ///     名字
    /// </summary>
    public string Name
    {
        get => _name;
        set => _name = value;
    }
}

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

/// <summary>
///     友好区域
/// </summary>
public class FriendlyArea
{
    /// <summary>
    ///     区域
    /// </summary>
    private Area _area;

    /// <summary>
    ///     友好区域
    /// </summary>
    private Area _friend;

    /// <summary>
    ///     友好关系开始时间
    /// </summary>
    private DateTime _startTime;

    /// <summary>
    ///     区域
    /// </summary>
    public virtual Area Area
    {
        get => _area;
        set => _area = value;
    }

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

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

对应的MySql表:

CREATE TABLE `area` (
  `Code` varchar(255) COLLATE utf8mb4_bin NOT NULL,
  `ParentCode` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `Name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`Code`)
) ENGINE=InnoDB DEFAULTCHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin;

CREATE TABLE `friendlyarea` (
  `AreaCode` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `FriendlyAreaCode` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
  `StartTime` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin;

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

//注册区域作为模型
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市列为了友好城市.其他的操作(修改、删除、查询)和之前的隐式关联和显示关联类似,这里不再赘述。

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

自定义关联型的隐式关联

有些时候,我们需要对具有相同关联端的关系配置不同的关联型,比如教师和班级间平时是“教学”关系,而在考试期间则是“监考”关系。一般而言,班级的监考教师不会是平时的任教教师,这时我们就需要为班级定义两个不同的关系“教学”和“监考”,那么我们定义的类如下:

/// <summary>
///     表示教师
/// </summary>
public class Teacher
{
    /// <summary>
    ///     教师姓名
    /// </summary>
    private string _name;

    /// <summary>
    ///     教师ID
    /// </summary>
    private long _teacherId;

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

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

    /// <summary>
    ///     转换为字符串表示形式
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"Teacher:{{TeacherId-{_teacherId},Name-\"{_name}\"}}";
    }
}
/// <summary>
///     表示班级
/// </summary>
public class Class
{
    /// <summary>
    ///     班级id
    /// </summary>
    private long _classId;

    /// <summary>
    ///     班级的监考教师
    /// </summary>
    private List<Teacher> _invigilating;


    /// <summary>
    ///     班级名称
    /// </summary>
    private string _name;

    /// <summary>
    ///     班级的任课教师
    /// </summary>
    private List<Teacher> _teachings;

    /// <summary>
    ///     班级id
    /// </summary>
    public long ClassId
    {
        get => _classId;
        set => _classId = value;
    }

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

    /// <summary>
    ///     班级的任课教师
    /// </summary>
    public virtual List<Teacher> Teachings
    {
        get => _teachings;
        set => _teachings = value;
    }

    /// <summary>
    ///     班级的监考教师
    /// </summary>
    public virtual List<Teacher> Invigilating
    {
        get => _invigilating;
        set => _invigilating = value;
    }

    /// <summary>
    ///     转换为字符串表示形式
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"Class:{{ClassId-{_classId},Name-\"{_name}\"}}";
    }
}

教师类和之前并无不同,班级类内有两个不同的关联引用。这里我们为这两个关联建立两张独立的关联表,如果是用伴随关联的话,要保证监考的教师和任教的教师在关联表中使用不同的字段作为映射字段,否则会污染数据。这里使用了独立的关联表来作为演示:

CREATE TABLE `teaching` (
  `ClassId` bigint(20) DEFAULT NULL,
  `TeacherId` bigint(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `invigilating` (
  `TeacherId` bigint(20) NOT NULL,
  `ClassId` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`TeacherId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

此类自定义关联型的隐式关联与一般的隐式关联操作是类似的,在查询时可以使用Include强制包含方法查询,也可以使用延迟加载。

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

//注册关联型 并且设置为隐式关联
modelBuilder.Association<Teaching>().HasVisible(false);
modelBuilder.Association<Invigilating>().HasVisible(false);

//为各自的关联引用设置 注意这里要设置左右端
classConfiguration.AssociationReference<Teaching>("Teachings", true)
    .HasLeftEnd(p => p.Class).HasRightEnd(p => p.Teacher);
classConfiguration.AssociationReference<Invigilating>("Invigilating", true)
    .HasLeftEnd(p => p.Class).HasRightEnd(p => p.Teacher);

以上就是配置代码,注意要将关联型配置为隐式以及为关联引用配置左端和右端。

var context = new CustomImplicitContext();

var cla = new Class
{
    Name = @"某某班1"
};

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

//添加任教和监考教师
cla.Teachings = new List<Teacher> {teacher1};
cla.Invigilating = new List<Teacher> {teacher2};

context.Classes.Attach(cla);
context.Teachers.Attach(teacher1);
context.Teachers.Attach(teacher2);

context.SaveChanges();
var context = new CustomImplicitContext();

var cla = context.Classes.FirstOrDefault();

if (cla?.Teachings != null)
    foreach (var teacher in cla?.Teachings)
        Console.WriteLine(teacher);

if (cla?.Invigilating != null)
    foreach (var teacher in cla?.Invigilating)
        Console.WriteLine(teacher);

这里仅列出如何添加和查询,其余方法读者们可以自己编写代码。

没有找到您需要的文档?

您还可以通过人工服务在线咨询,服务时间为每天上午9点至下午6点。

If you can't find required answer, get in touch with us online. We provide service from 9:00 to 18:00.

让编程成为一件快乐的事
现在开始