Skip to main content

配置关联型

在系统内,除了对象外,对象之间的关系也普遍存在,其中关联是最常见的关系之一。例如,在一个学生管理系统中,存在学生和班级两类对象,它们之间存在“隶属”关联。这个关联涉及两个参与方,即学生和班级,它们被称为关联端。基于关联关系,我们可以在关联端的对象内部定义一个指针,以便从关联的一端导航到另一端,这个指针被称为关联引用。在“隶属”关联中,学生作为关联端,内部定义了一个关联引用Class,指向该学生所属的班级;班级作为另一个关联端,内部也定义了一个关联引用Students,指向隶属于该班级的学生。由于一个班级有多个学生,所以Students是一个集合。通过这种关联关系,我们可以在Student对象内部通过关联引用Class找到它所属的班级,也可以在Class对象内部通过关联引用Students找到它所拥有的学生。这种关联关系方便了对象之间的导航,有助于获取相关联的信息。

示例模型

/// <summary>
/// 表示学生
/// </summary>
public class Student
{
/// <summary>
/// 学生id
/// </summary>
public long StudentId {get;set;}

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

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

/// <summary>
/// 就读班级
/// </summary>
public virtual Class Class {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<Student> Students {get;set;}
}

这两个关联引用是基于“隶属”关联而定义的。在实际应用中,我们通常会根据具体的业务需求来明确关联的方向,并只在源端定义关联引用。以学生管理系统为例,在实际业务中,我们通常不需要从班级对象导航到隶属于它的学生。因为一个班级有众多学生,如果要列出某一班级的学生,我们通常会基于学生的属性,如所属班级的ID(classId)进行查找,而不是先找到班级对象,再通过关联导航到学生。因此,在这种情况下,我们无需在班级对象内部定义指向学生的关联引用。这样的设计更符合实际业务需求,提高了查询效率和准确性。

附加说明

在大多数情况下,我们会为关联引用定义属性,如Student.Class和Class.Students,但这不是必须的,可以根据实际业务需求来决定是否定义这些属性。

与对象一样,关联也需要进行持久化。关联持久化的实质是保存两个对象的对应关系,因此我们只需要将关联各端的标识属性组合成一条记录进行存储。

以“学生小六属于高三(1)班”这一实例为例,我们只需将学生小六的ID(假设为16)和高三(1)班的ID(假设为54)组合成一条记录(16,54),并将其存储起来,即可实现对该关联实例的持久化。存储该记录的数据库表被称为关联映射表。

显然,上述“隶属”关联的映射表应包含两个字段:studentId和classId。其中,studentId是关联端Student的标识属性的映射字段,而classId则是关联端Class的标识属性的映射字段。

关联映射表的存储方式有两种:伴随存储或伴随映射,以及独立存储或独立映射。对于一对多或多对一关联,我们通常采用伴随存储的方式,此时关联映射表就是某一端的映射表。而对于多对多关联,我们通常采用独立存储的方式,此时需要一个独立的数据库表作为关联映射表。当然,一对多或多对一关联也可以采用独立映射,这取决于实际的存储设计需求。

以下是一个示例,展示了如何创建两个表:class和student。其中,student表既作为Student实体的映射表,也作为“隶属”关联的映射表。Class实体的标识属性映射到classId字段,而Student实体的标识属性则映射到studentId字段(它同时也是Student实体的标识属性的映射字段)。

在使用Obase进行关联存储时,您需要明确系统中存在的关联,并指定关联映射表以及关联端标识属性的映射字段(关联端映射)。这些配置可以通过重写对象上下文配置器(SqlContexConfiger)的CreateModel方法来完成。通过这种方式,您可以为系统中的对象和关联提供准确的映射配置,确保数据在数据库中的正确存储和检索。

/// <summary>
/// 使用指定的建模器创建对象数据模型。
/// </summary>
/// <param name="modelBuilder"></param>
protected override void CreateModel(ModelBuilder modelBuilder)
{
//首先 引入实体,即将班级和学生这两个类均配置为实体类型,指定了他们的主键,并且把主键设置为自增的.
//注册实体型“班级”,并指定它的对象键(自增)
var classCfg = modelBuilder.Entity<Class>();
classCfg.HasKeyAttribute(p => p.ClassId).HasKeyIsSelfIncreased(true);

//注册实体型“学生”,并指定它的对象键(自增)
var studentCfg = modelBuilder.Entity<Student>();
studentCfg.HasKeyAttribute(p => p.StudentId).HasKeyIsSelfIncreased(true);

//注册学生和班级间的“隶属”关联,并指定关联映射表
var studentClassAss = modelBuilder.Association<Student, Class>();
studentClassAss.ToTable("Student");

//分别配置关联的两端:指定关联端的实体型,配置关联端映射
studentClassAss.AssociationEnd(p => p.End1)
.HasEntityType<Student>()
.HasMapping(p => p.StudentId, "StudentId");
studentClassAss.AssociationEnd(p => p.End2)
.HasEntityType<Class>()
.HasMapping(p => p.ClassId, "ClassId");

//注册关联引用:班级 -> 学生
var refStudent = classCfg.AssociationReference<Student, Class>("Students",true);
refStudent.HasLeftEnd(p => p.End2);
refStudent.HasRightEnd(p => p.End1);

//注册关联引用:学生 -> 班级
var refClassCfg = studentCfg.AssociationReference<Student, Class>("Class", false);
refClassCfg.HasLeftEnd(p => p.End1);
refClassCfg.HasRightEnd(p => p.End2);
}

这里的配置看起来很复杂,但其实对于这种简单的隐式关联型,Obase可以自行配置,配置可以简化为如下的代码:

/// <summary>
/// 使用指定的建模器创建对象数据模型。
/// </summary>
/// <param name="modelBuilder"></param>
protected override void CreateModel(ModelBuilder modelBuilder)
{
//注册两个实体型 他们的键属性符合类名+id或者类名+Code
//所以可以直接有Obase推断得出他们的键属性
//其他属性除关联引用外 均为简单的Get/Set 同之前一样 可以直接自动配置
modelBuilder.Entity<Class>();
modelBuilder.Entity<Student>();
//配置二者间关系的隐式关联型
//之后他们的关联引用可由此关联型推断而出
var studentClassAss = modelBuilder.Association<Student, Class>();
studentClassAss.ToTable("Student");
}

新增关联实例

首先,我们先添加一个班级,此后,我们增加一名叫小6的学生,并将其分配至之前的班级里:

var context = new StudentAndClassContext();
//添加一个班级
var cla = new Class
{
Name = @"某某班"
};

context.Classes.Attach(cla);
//增加一个新学生
var student6 = new Student
{
Name = @"小6"
};

context.Students.Attach(student6);

//分配至之前的班级内
cla.Students = new List<Student> {student6};
context.SaveChanges();

context = new StudentAndClassContext();
//重新查出
cla = context.Classes.FirstOrDefault();
//展示
Console.WriteLine(cla);
var stu = cla?.Students.FirstOrDefault(p => p.Name == @"小6");
if (stu != null)
Console.WriteLine(stu);

此后重新查出班级,可以发现小6已经在此班级的学生集合内了. 当然,你也可以使用AddRange等方法来为集合添加元素.

加载关联引用

我们注意到,之前的新增关联实例中,在查询班级时也查询出了他的关联对象,学生。这其实是关联引用的加载,使用如下的代码即可加载关联的另一端:

//对像上下文
var context = new StudentAndClassContext();
//查询班级
var cla = context.Classes.FirstOrDefault();
if (cla != null)
foreach (var student in cla.Students)
{
Console.WriteLine($"{student.StudentId}号学生,名字是:{student.Name}");
}

就像调用对象内部的其他对象一样,我们可以通过关联引用获取到引用对象。

关于关联引用的加载,有一个重要的概念:延迟加载。在查询对象时,关联对象并不会在查询时一并加载。Obase的默认配置是在访问属性时进行额外查询并构造关联对象。以上例子就是延迟加载的示例。对于需要延迟加载的关联引用,我们将其定义为virtual,就像上文中的Class中的Students一样。有时,为了性能考虑(延迟加载需要额外访问数据持久层),我们会关闭或设置关联引用为virtual。但如果仍需加载关联对象,可以使用Include方法强制包含关联引用,直接获取对象及其关联对象。

这里我们定义一个不进行关联加载的上下文配置提供者以及对应的对象上下文:

//改为不延迟加载
classConfiguration.AssociationReference<Student, Class>("Students", true).HasEnableLazyLoading(false).HasRightEnd("End1").HasLeftEnd("End2");
/// <summary>
/// 学生与班级对象上下文
/// </summary>
public class StudentAndClassNoLazyLoadContext : ObjectContext<StudentAndClassNoLazyLoadConfger>
{
/// <summary>
/// 学生对象集合
/// </summary>
public ObjectSet<Student> Students { get; set; }

/// <summary>
/// 班级对象集合
/// </summary>
public ObjectSet<Class> Classes { get; set; }
}

那么,此时使用StudentAndClassNoLazyLoadContext查出的班级将不再延迟加载,只有在使用Include方法后才会一并加载关联对象,示例代码如下:

//对像上下文
var context = new StudentAndClassNoLazyLoadContext();
//查询班级
var cla = context.Classes.FirstOrDefault();

var stu = cla?.Students;
if (stu == null || cla.Students.Count == 0)
{
Console.WriteLine("No Load Association!");
}

//强制包含
cla = context.Classes.Include(p => p.Students).FirstOrDefault();

stu = cla?.Students;
if (stu != null)
{
foreach (var student in cla.Students)
{
Console.WriteLine($"{student.StudentId}号学生,名字是:{student.Name}");
}
}

删除关联实例

//对象上下文
var context = new StudentAndClassContext();

//查询出班级并强制包含学生
var cla = context.Classes.Include(p => p.Students).FirstOrDefault();
//查出要删除的学生
var student5 = context.Students.FirstOrDefault(p => p.Name == @"小5");
//移除
cla?.Students.Remove(student5);
//保存
context.SaveChanges();

移除也和一般的从集合中移除对象类似,但要注意的是需要通过Obase框架来代理这些对象.我们之前将班级至学生的关联引用配置为延迟加载(参见上面配置部分),这里需要用强制包含方法将此关联引用的对象包含至此上下文中(即使用Include方法).而后再查出要移除的学生对象,从集合中移除并保存即可.

当然,使用此种删除方式需要先查询出要删除的关联对象,使用起来效率较低。我们更常使用的是直接在对象中根据主键移除关联对像,代码如下:

//对象上下文 
var context = new StudentAndClassContext();

//查询出班级并强制包含学生
var cla = context.Classes.Include(p => p.Students).FirstOrDefault();
//此处是为了查出要移除的学生主键 在业务场景中 由其他部分传入
var student6 = context.Students.FirstOrDefault(p => p.Name == @"小6")?.StudentId;
//找到要移除的
var index = cla?.Students.FindIndex(p => p.StudentId == student6);
//移除
cla?.Students.RemoveAt(index.Value);
//保存
context.SaveChanges();

关联投影

关联投影是指使用Select和SelectMany方法将实体对象投影到某一关联引用,或者将关联对象投影到某一关联端。我们首先看一下Select和SelectMany方法的用法。

/// <summary>
/// 投影单个或多个的演示类
/// </summary>
public class SelectSingleAndMulti
{
/// <summary>
/// 单个
/// </summary>
public int Single { get; set; }

/// <summary>
/// 多个
/// </summary>
public List<int> Multi { get; set; }
}

其中的Single可以类比于我们之前的说的单一关系,Multi类比于多重关系。接下来,我们分别使用Select和SelectMany方法执行投影操作。

//构造用于投影的集合
var selectList = new List<SelectSingleAndMulti>();
//初始化一些值
for (var i = 1; i <= 10; i++)
{
selectList.Add(
new SelectSingleAndMulti()
{
Single = i,
Multi = new List<int>() { i, i+1, i+2 }
}
);
}

//对不同多重性的属性进行多重投影

//执行本行得到的singleSelect为一个列表,其中元素为1,2,3,4,5,6,7,8,9,10
var singleSelect = selectList.Select(p => p.Single).ToList();

//执行本行得到的multiSelect也为一个列表,但其中每个元素都为一个三个元素的列表即
// {1,2,3},{2,3,4},{3,4,5},{4,5,6},{5,6,7},{7,8,9},{9,10,11},{10,11,12}
var multiSelect = selectList.Select(p => p.Multi).ToList();

//执行本行得到的multiSelectMany的结果为一个列表,其元素为1,2,3,2,3,4,3,4,5,4,5,6,5,6,7,7,8,9,9,10,11,10,11,12
var multiSelectMany = selectList.SelectMany(p => p.Multi).ToList();

如果一个类的某一属性(Property)返回一个集合,在该类的实例集上运用Select投影到该属性,将得到一个以该集合为元素的集合,我们将这种投影称为多重投影。如果使用SelectMany方法执行多重投影,则会将集合元素进行串联得到一个新的集合,我们将SelectMany称为合并投影。

下面我们介绍如何在Obase中使用关联投影。

首先填充一下数据,为之前的班级再增加一个学生小7,现在我们的“某某班”内有两名学生“小6”和“小7”,(这里不再展开如何添加对象和关联对象了)。以下代码演示运用Select分别在Student实例集上投影到单值属性Class、在Class实例集上投影到集合属性Students。

//准备上下文
var selectContext = new StudentAndClassContext();

//Class是单值属性(Property),执行本行返回Class实例的集合
var clas = selectContext.Students.Select(p => p.Class).ToList();

//Students为集合属性(Property),执行本行得到一个由List<Student>构成的集合
var students = selectContext.Classes.Select(p => p.Students).ToList();

由于Class.Students是多重投影,我们可以实施合并投影,如下:

//准备上下文
var selectContext = new StudentAndClassContext();

//执行本行得到一个由Student实例构成的集合
var stus = selectContext.Classes.SelectMany(p => p.Students).ToList();