关联映射

前面章节我们介绍了EFCore中单表的模型定义和增删改查,这篇笔记我们继续学习如何定义多表之间的关联关系。为了简单起见,本篇笔记我们定义数据模型的方式都是使用FluentAPI,关于如何使用数据注解定义关联模型可以参考微软官方文档。

一对多关联

一对多是最常见的关联关系,我们这里通过例子介绍如何定义一对多关联关系。下面例子中我们定义了数据模型ClassRoomStudent,它们具有一对多关系,一个教室关联多个学生。

ClassRoom.cs

namespace Gacfox.Demo.DemoNetCore;

public class ClassRoom
{
    public long ClassRoomId { get; set; }
    public string ClassRoomName { get; set; }
    public List<Student> Students { get; set; } = new List<Student>();
}

ClassRoomEntityConfig.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Gacfox.Demo.DemoNetCore;

public class ClassRoomEntityConfig : IEntityTypeConfiguration<ClassRoom>
{
    public void Configure(EntityTypeBuilder<ClassRoom> builder)
    {
        builder.ToTable("t_classroom");
        builder.HasKey(e => e.ClassRoomId);
        builder.Property(e => e.ClassRoomId)
            .ValueGeneratedOnAdd()
            .HasColumnName("classroom_id");
        builder.Property(e => e.ClassRoomName)
            .HasColumnName("classroom_name")
            .IsRequired();
    }
}

ClassRoom模型中,我们定义了Students属性表示该教室下的学生列表。

Student.cs

namespace Gacfox.Demo.DemoNetCore;

public class Student
{
    public long StudentId { get; set; }
    public string StudentName { get; set; }
    public int Age { get; set; }
    public DateTime CreateTime { get; set; }
    public long ClassRoomId { get; set; }
    public ClassRoom ClassRoom { get; set; }
}

StudentEntityConfig.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Gacfox.Demo.DemoNetCore;

public class StudentEntityConfig : IEntityTypeConfiguration<Student>
{
    public void Configure(EntityTypeBuilder<Student> builder)
    {
        builder.ToTable("t_student");
        builder.HasKey(e => e.StudentId);
        builder.Property(e => e.StudentId)
            .ValueGeneratedOnAdd()
            .HasColumnName("student_id");
        builder.Property(e => e.StudentName)
            .HasColumnName("student_name")
            .IsRequired();
        builder.Property(e => e.Age)
            .HasColumnName("age")
            .IsRequired();
        builder.Property(e => e.CreateTime)
            .HasColumnName("create_time")
            .IsRequired();
        builder.Property(e => e.ClassRoomId)
            .HasColumnName("classroom_id");
        builder.HasOne(e => e.ClassRoom)
            .WithMany(e => e.Students)
            .HasForeignKey(e => e.ClassRoomId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

Student模型中,我们定义了ClassRoom属性表示该学生归属的教室,此外我们还定义了ClassRoomId属性表示该学生关联的教室ID外键,这里注意该属性是可以省略不写的,即使不写EFCore也会帮我们在数据库中自动创建外键字段,但我这里习惯将其明确写出,这样如果有“从学生查询教室ID”的需求就可以不必把教室实体类查询出来,此外明确写出外键字段也方便我们对外键字段自定义列名。

数据模型配置中,我们使用了一组.HasOne().HasMany()的链式调用,这些就是EFCore中定义模型关联的API,代码中我们对HasOne()方法指定了模型中的ClassRoom属性,对HasMany()方法指定了关联模型中的Students属性,这样就实现了一对多关联关系。该处链式调用我们还使用了HasForeignKey()方法指定了我们之前定义的外键字段,如果之前没有明确定义外键字段则不必在此指定,此外我们还调用了OnDelete()方法指明了外键的删除级联操作。

定义一对多关系的额外补充:

  1. 一对多关系可以在任意一方定义:此处我们是在Student的模型映射配置中指定一对多关联关系,实际上我们也可以在ClassRoom模型配置中指定关联关系,此时就应该使用.HasMany().WithOne()链式调用,不过我们还是推荐在一对多中一的一方指定关联关系,这样更符合一般人的思维逻辑。
  2. 可以定义双向一对多和单向一对多:上面我们可以看到我们定义的数据模型是双向关联的,EFCore中也支持单向关联,假如上面代码中ClassRoom模型没有定义Students属性,此时就是一对多单向关联,我们在StudentEntityConfig.cs中进行模型配置时可以使用builder.HasOne(e => e.ClassRoom).WithMany()指定,这里WithMany()方法可以不传任何参数。

Program.cs

namespace Gacfox.Demo.DemoNetCore;

public class Program
{
    public static void Main()
    {
        using (MyDbContext myDbContext = new MyDbContext())
        {
            ClassRoom room = new ClassRoom { ClassRoomName = "一年一班" };
            Student s1 = new Student { StudentName = "汤姆", Age = 18, CreateTime = DateTime.Now };
            Student s2 = new Student { StudentName = "杰瑞", Age = 18, CreateTime = DateTime.Now };
            room.Students.Add(s1);
            room.Students.Add(s2);
            myDbContext.ClassRooms.Add(room);
            myDbContext.Students.Add(s1);
            myDbContext.Students.Add(s2);
            myDbContext.SaveChanges();
        }
    }
}

上面代码演示了向一对多关联的两张表中插入数据的代码,我们创建了1个ClassRoom对象,然后又创建了2个Student对象并将其关联在一起。注意虽然模型中我们定义的是双向关联,但实际代码中我们插入数据时没必要双向指定,只要指定一个方向的关联关系,EFCore就知道我们要实现的操作是什么了。

代码的最后我们保存了数据,实际上myDbContext.Students.Add(s1)myDbContext.Students.Add(s2)甚至也可以省略不写,因为EFCore能够自动解析关联关系,代码中我们已经设置了关联,因此EFCore可以自动保存被关联的Student对象,但这里我还是习惯将其明确写出,这样代码可读性更好。

一对多自关联

自关联是一种特殊的一对多关系,自关联最常见的地方是系统菜单,一个菜单可能包含若干子菜单,这显然是一种自关联关系。

Menu.cs

namespace Gacfox.Demo.DemoNetCore;

public class Menu
{
    public long MenuId { get; set; }
    public string MenuName { get; set; }
    public long? ParentMenuId { get; set; }
    public Menu ParentMenu { get; set; }
    public List<Menu> ChildMenus { get; set; } = new List<Menu>();
}

MenuEntityConfig.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Gacfox.Demo.DemoNetCore;

internal class MenuEntityConfig : IEntityTypeConfiguration<Menu>
{
    public void Configure(EntityTypeBuilder<Menu> builder)
    {
        builder.ToTable("t_menu");
        builder.HasKey(e => e.MenuId);
        builder.Property(e => e.MenuId)
            .ValueGeneratedOnAdd()
            .HasColumnName("menu_id");
        builder.Property(e => e.MenuName)
            .IsRequired()
            .HasColumnName("menu_name")
            .HasMaxLength(20);
        builder.Property(e => e.ParentMenuId)
            .HasColumnName("parent_menu_id")
            .IsRequired(false);
        builder.HasOne(e => e.ParentMenu)
            .WithMany(e => e.ChildMenus)
            .HasForeignKey(e => e.ParentMenuId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

Program.cs

namespace Gacfox.Demo.DemoNetCore;

public class Program
{
    public static void Main()
    {
        using (MyDbContext myDbContext = new MyDbContext())
        {
            Menu m1 = new Menu { MenuName = "用户管理" };
            Menu m2 = new Menu { MenuName = "系统管理" };

            Menu m11 = new Menu { MenuName = "新增用户" };
            Menu m12 = new Menu { MenuName = "导入用户" };
            Menu m21 = new Menu { MenuName = "数据字典" };

            m11.ParentMenu = m1;
            m12.ParentMenu = m1;
            m21.ParentMenu = m2;

            myDbContext.Menus.Add(m1);
            myDbContext.Menus.Add(m2);
            myDbContext.Menus.Add(m11);
            myDbContext.Menus.Add(m12);
            myDbContext.Menus.Add(m21);

            myDbContext.SaveChanges();
        }
    }
}

我们可以看到,定义一对多自关联和普通的一对多没有太大区别,我们正常定义数据模型的关联关系即可,只不过一对多的两侧都是同一个实体类。不过这里注意由于我们的外键ParentMenuId可能为空(对于根节点菜单来说),因此我们这里将其C#类型定义为long?可空类型,并在配置类中指定了IsRequired(false)

一对一关联

一对一关联定义的例子如下。

IdCard.cs

namespace Gacfox.Demo.DemoNetCore;

public class IdCard
{
    public long CardId { get; set; }
    public string CardCode { get; set; }
    public Person Person { get; set; }
}

IdCardEntityConfig.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Gacfox.Demo.DemoNetCore;

public class IdCardEntityConfig : IEntityTypeConfiguration<IdCard>
{
    public void Configure(EntityTypeBuilder<IdCard> builder)
    {
        builder.ToTable("t_idcard");
        builder.HasKey(e => e.CardId);
        builder.Property(e => e.CardId)
            .ValueGeneratedOnAdd()
            .HasColumnName("card_id");
        builder.Property(e => e.CardCode)
            .HasColumnName("card_code")
            .IsRequired();
    }
}

Person.cs

namespace Gacfox.Demo.DemoNetCore;

public class Person
{
    public long PersonId { get; set; }
    public string PersonName { get; set; }
    public long CardId { get; set; }
    public IdCard IdCard { get; set; }
}

PersonEntityConfig.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Gacfox.Demo.DemoNetCore;

public class PersonEntityConfig : IEntityTypeConfiguration<Person>
{
    public void Configure(EntityTypeBuilder<Person> builder)
    {
        builder.ToTable("t_person");
        builder.HasKey(e => e.PersonId);
        builder.Property(e => e.PersonId)
            .ValueGeneratedOnAdd()
            .HasColumnName("person_id");
        builder.Property(e => e.PersonName)
            .HasColumnName("person_name")
            .IsRequired();
        builder.Property(e => e.CardId)
            .HasColumnName("card_id")
            .IsRequired();
        builder.HasOne(e => e.IdCard)
            .WithOne(e => e.Person)
            .HasForeignKey<Person>(e => e.CardId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}

Program.cs

namespace Gacfox.Demo.DemoNetCore;

public class Program
{
    public static void Main()
    {
        using (MyDbContext myDbContext = new MyDbContext())
        {
            IdCard idCard = new IdCard { CardCode = "123456" };
            Person person = new Person { PersonName = "李雷" };
            person.IdCard = idCard;
            myDbContext.IdCards.Add(idCard);
            myDbContext.People.Add(person);
            myDbContext.SaveChanges();
        }
    }
}

我们可以看到其实一对一和一对多没有什么太大的区别,我们使用.HasOne().WithOne()写法定义即可。不过这里要注意,之前一对多我们可以省略外键字段不写,EFCore会帮我们自动创建,但一对一关系中我们必须明确写出外键,这是因为一对一关系中外键字段可以在两张表任意一侧,EFCore不知道要将外键字段自动创建到哪一侧,因此我们必须手动指定。

多对多关联

多对多关系比较特殊的是我们需要使用一张中间表来表达关联关系,下面是一个例子。

namespace Gacfox.Demo.DemoNetCore;

public class Author
{
    public long AuthorId { get; set; }
    public string AuthorName { get; set; }
    public List<Book> Books { get; set; } = new List<Book>();
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Gacfox.Demo.DemoNetCore;

public class AuthorEntityConfig : IEntityTypeConfiguration<Author>
{
    public void Configure(EntityTypeBuilder<Author> builder)
    {
        builder.ToTable("t_author");
        builder.HasKey(e => e.AuthorId);
        builder.Property(e => e.AuthorId)
            .HasColumnName("author_id");
        builder.Property(e => e.AuthorName)
            .HasColumnName("author_name")
            .IsRequired();
    }
}
namespace Gacfox.Demo.DemoNetCore;

public class Book
{
    public long BookId { get; set; }
    public string BookName { get; set; }
    public List<Author> Authors { get; set; } = new List<Author>();
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Gacfox.Demo.DemoNetCore;

public class BookEntityConfig : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder.ToTable("t_book");
        builder.HasKey(e => e.BookId);
        builder.Property(e => e.BookId)
            .HasColumnName("book_id");
        builder.Property(e => e.BookName)
            .HasColumnName("book_name")
            .IsRequired();
        builder.HasMany(e => e.Authors)
            .WithMany(e => e.Books)
            .UsingEntity<Dictionary<string, object>>(
            "t_author_book",
            join => join
            .HasOne<Author>()
            .WithMany()
            .HasForeignKey("author_id"),
            join => join
            .HasOne<Book>()
            .WithMany()
            .HasForeignKey("book_id"),
            join => join.HasKey("author_id", "book_id")
            );
    }
}

代码中我们定义了数据模型AuthorBook的双向多对多关系,其中关联关系我们定义在Book这一侧,实际上我们可以定义在任意一侧。BookEntityConfig类中,我们使用了.HasMany().WithMany()的方式定义多对多关系,EFCore中我们可以不显式的定义关联表实体类,这里我们使用UsingEntity()方法来对关联表进行配置,该方法有很多重载,我们使用的是最复杂的一个,我们这里对关联表的表名、字段名、主键都进行了配置,有关其他重载的使用我们可以参考官方文档。

namespace Gacfox.Demo.DemoNetCore;

public class Program
{
    public static void Main()
    {
        using (MyDbContext myDbContext = new MyDbContext())
        {
            Author a1 = new Author { AuthorName = "赵日天" };
            Author a2 = new Author { AuthorName = "王后雄" };
            Book b1 = new Book { BookName = "一万个为什么" };
            Book b2 = new Book { BookName = "母猪的产后护理" };
            Book b3 = new Book { BookName = "MySQL从删库到跑路" };
            a1.Books.Add(b1);
            a1.Books.Add(b2);
            a2.Books.Add(b2);
            a2.Books.Add(b3);
            myDbContext.Authors.Add(a1);
            myDbContext.Authors.Add(a2);
            myDbContext.Books.Add(b1);
            myDbContext.Books.Add(b2);
            myDbContext.Books.Add(b3);
            myDbContext.SaveChanges();
        }
    }
}

上面代码中我们向多对多关联表中插入了数据,执行后我们可以看到t_authort_bookt_author_book3张表都插入了数据。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap