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

欢迎查阅
OBASE帮助文档

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

关联

系统内不仅存在对象,还普遍存在着对象之间的关系,关联即是一种关系。比如一个学生管理系统,存在学生和班级两类对象,还存在学生与班级之间的“隶属”关联。这一关联有两个参与方,学生和班级,它们称为关联端。基于关联关系,我们可以在作为端的对象(端对象)内部定义一个指针,以便从关联的一端导航到另一端,这个指针称为关联引用。在上述“隶属”关联中,Student是关联端,其内部定义了一个关联引用_class,它指向该学生所属的班级;Class也是一个关联端,其内部也定义了一个关联引用_students,它指向隶属于该班级的学生,由于一个班级有多个学生,所以它是一个集合。

/// <summary>
///     表示学生
/// </summary>
public class Student
{
    /// <summary>
    ///     就读班级
    /// </summary>
    private Class _class;

    /// <summary>
    ///     学校ID
    /// </summary>
    private long _classId;

    /// <summary>
    ///     学生名称
    /// </summary>
    private string _name;


    /// <summary>
    ///     学生id
    /// </summary>
    private long _studentId;

    /// <summary>
    ///     学生id
    /// </summary>
    public long StudentId
    {
        get => _studentId;
        set => _studentId = value;
    }

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

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

    /// <summary>
    ///     就读班级
    /// </summary>
    public virtual Class Class
    {
        get => _class;
        set => _class = value;
    }
}
/// <summary>
///     表示班级
/// </summary>
public class Class
{
    /// <summary>
    ///     班级id
    /// </summary>
    private long _classId;

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


    /// <summary>
    ///     学生
    /// </summary>
    private List<Student> _students;

    /// <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<Student> Students
    {
        get => _students;
        set => _students = value;
    }
}

这两个关联引用都是基于“隶属”关联定义的。在实际场景中通常不需要为每个端都定义关联引用,一般会根据实际的业务需要明确关联的方向,因此只需要在源端定义关联引用。例如上述学生管理系统,实际业务一般不需要从班级导航到隶属于它的学生,因为学生众多,如果要列出某一班级的学生,一般会基于学生的属性classId(所属班级的ID)进行查找,而不是先查找到班级对象,再导航到学生。这种情况下,我们就不需要在班级内定义指向学生的关联引用。

附加说明 通常情况下,我们会为关联引用定义属性(Property),如Student.Class和Class.Students,但这并非Obase的强制性要求,您可以根据业务需求决定是否定义。

与对象一样,关联也需要持久化。作为一种关系,关联持久化的实质是保存两个对象的对应关系,因此只需要将关联各端的标识属性组合成一条记录存储就行了。以上述学生与班级间的“隶属”关联为例,假定该关联有一个实例“学生小六属于高三(1)班”,取小六的ID(假定为16)和高三(1)班的ID(假定为54)组合成一条记录(16,54),存储这条记录就实现了对该关联实例的持久化。存储该记录的数据库表称为关联映射表。显然,上述“隶属”关联的映射表应该有两个字段studentId和classId,其中,studentId是关联端Student的标识属性的映射字段,classId是关联端Class的标识属性的映射字段。 可以用关联某一端的映射表作为关联映射表,这种存储关联的方式称为伴随存储或伴随映射,一对多或多对一关联通常采用这种方式。也可以用独立的数据库表作为关联映射表,这种方式称为独立存储或独立映射,多对多关联一般应当采用这种方式。当然,一对多或多对一关联也可以采用独立映射,这取决于您自己的存储设计。 下列代表创建了两个表class和student,分别作为类型Class和Student的映射表.student同时作为“隶属”关联的映射表,关联端Class的标识属性映射到字段classId,关联端Student的标识属性映射到字段studentId(它同时也是实体型Student的标识属性的映射字段)。

CREATE TABLE `class` (
`ClassId` bigint(20) NOT NULL AUTO_INCREMENT,
`Name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL
PRIMARY KEY (`ClassId`)
) ENGINE=InnoDB AUTO_INCREMENT=95 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

CREATE TABLE `student` (
`StudentId` bigint(20) NOT NULL AUTO_INCREMENT,
`Name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
`ClassId` bigint(20) DEFAULT NULL,
PRIMARY KEY (`StudentId`)
) ENGINE=InnoDB AUTO_INCREMENT=506 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

使用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();

没有找到您需要的文档?

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

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

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