EF Core中如何正确地设置两张表之间的关联关系

数据库


假设现在我们在SQL Server数据库中有下面两张表:

Person表,代表的是一个人:

CREATE TABLE [dbo].[Person](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [PersonCode] [nvarchar](20) NULL,
    [Name] [nvarchar](50) NULL,
    [Age] [int] NULL,
 CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
 CONSTRAINT [IX_Person] UNIQUE NONCLUSTERED 
(
    [PersonCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

其主键是ID,而且主键是自增列。Person表还有个PersonCode列是唯一键,然后Name和Age列用来描述一个人的名字和年龄。

Book表,代表的是一本书:

CREATE TABLE [dbo].[Book](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [BookCode] [nvarchar](20) NULL,
    [PersonCode] [nvarchar](20) NULL,
    [BookName] [nvarchar](50) NULL,
    [ISBN] [nvarchar](20) NULL,
 CONSTRAINT [PK_Book] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
 CONSTRAINT [IX_Book] UNIQUE NONCLUSTERED 
(
    [BookCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

其主键是ID,而且主键也是自增列。Book表的BookCode列是唯一键,Book表的PersonCode列引用Person表的PersonCode列值,所以Book表的PersonCode列实际上是外键,但是我们并没有在数据库中设置两张表之间的外键关系,我们将稍后在EF Core中的实体之间设置外键关系,来演示就算在数据库中没有设置外键,EF Core也可以设置实体之间的外键关系。 所以Person表和Book表实际上是一对多关系,通过两张表的PersonCode列,一个Person对应多个Book,表示一个人可以拥有多本书。

实体


新建一个.NET Core控制台项目,现在我们在EF Core中建立Person表和Book表的实体:

Person实体,对应数据库的Person表,其属性Book是一个ICollection<Book>类型的Book实体集合,表示一个Person实体包含多个Book实体:

public partial class Person
{
    public int Id { get; set; }
    public string PersonCode { get; set; }
    public string Name { get; set; }
    public int? Age { get; set; }

    //通过Person实体的Book属性,可以找到多个Book实体,说明Person表是一对多关系中的主表
    public virtual ICollection<Book> Book { get; set; }
}

Book实体,对应数据库的Book表,其属性Person是一个Person实体,表示一个Book实体只能找到一个Person实体:

public partial class Book
{
    public int Id { get; set; }
    public string BookCode { get; set; }
    public string PersonCode { get; set; }
    public string BookName { get; set; }
    public string Isbn { get; set; }

    //通过Book实体的Person属性,可以找到一个Person实体,说明Book表是一对多关系中的从表
    public virtual Person Person { get; set; }
}

然后是继承DbContext的TestDBContext类,其中最重要的地方是OnModelCreating方法中设置Person实体和Book实体一对多关系的Fluent API,每一行都写明了注释:

public partial class TestDBContext : DbContext
{
    public TestDBContext()
    {
    }

    public TestDBContext(DbContextOptions<TestDBContext> options)
        : base(options)
    {
    }

    public virtual DbSet<Book> Book { get; set; }
    public virtual DbSet<Person> Person { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlServer("Server=localhost;User Id=sa;Password=Dtt!123456;Database=TestDB");
        }
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>(entity =>
        {
            entity.HasKey(e => e.BookCode);//设置Book实体的BookCode属性为EF Core实体的Key属性

            entity.HasIndex(e => e.BookCode)
                .HasName("IX_Book")
                .IsUnique();

            entity.Property(e => e.Id).ValueGeneratedOnAdd();//设置Book实体的Id属性为插入数据到数据库Book表时自动生成,因为Book表的ID列为自增列
            entity.Property(e => e.Id).HasColumnName("ID");

            entity.Property(e => e.BookCode).HasMaxLength(20);

            entity.Property(e => e.BookName).HasMaxLength(50);

            entity.Property(e => e.Isbn)
                .HasColumnName("ISBN")
                .HasMaxLength(20);

            entity.Property(e => e.PersonCode).HasMaxLength(20);
        });

        modelBuilder.Entity<Person>(entity =>
        {
            entity.HasKey(e => e.PersonCode);//设置Person实体的PersonCode属性为EF Core实体的Key属性

            entity.HasIndex(e => e.PersonCode)
                .HasName("IX_Person")
                .IsUnique();

            entity.Property(e => e.Id).ValueGeneratedOnAdd();//设置Person实体的Id属性为插入数据到数据库Person表时自动生成,因为Person表的ID列为自增列
            entity.Property(e => e.Id).HasColumnName("ID");

            entity.Property(e => e.Name).HasMaxLength(50);

            entity.Property(e => e.PersonCode).HasMaxLength(20);

            //设置Person实体和Book实体之间的一对多关系,尽管我们并没有在数据库中建立Person表和Book表之间的一对多外键关系,但是我们可以用EF Core的Fluent API在实体层面设置外键关系
            entity.HasMany(p => p.Book)//设置Person实体通过属性Book可以找到多个Book实体,表示Person表是一对多关系中的主表
            .WithOne(b => b.Person)//设置Book实体通过属性Person可以找到一个Person实体,表示Book表是一对多关系中的从表
            .HasPrincipalKey(p => p.PersonCode)//设置Person表的PersonCode列为一对多关系中的主表键
            .HasForeignKey(b => b.PersonCode)//设置Book表的PersonCode列为一对多关系中的从表外键
            .OnDelete(DeleteBehavior.ClientSetNull);//设置一对多关系的级联删除效果为DeleteBehavior.ClientSetNull

        });
    }
}

示例代码


现在我们来设想下面一个场景:

假设数据库中的Person表有一行数据如下:

数据库中的Book表有三行数据如下:

可以看到Book表三行数据的PersonCode列都为NULL,那么我们怎么在EF Core中更改Book表三行数据的PersonCode列为Person表的PersonCode列值呢?也就是说将Book表三行数据的PersonCode列都改为Person表的值P001,从而表示James这个人拥有三本书。

本例的示例代码都写在了.NET Core控制台项目的Program类中,这里先将代码全部贴出来:

class Program
{
    /// <summary>
    /// 初始化Person表和Book表的数据,没有设置Book表的外键列PersonCode的值
    /// </summary>
    static void InitData()
    {
        //初始化数据库数据
        using (var dbContext = new TestDBContext())
        {
            var james = new Person() { PersonCode = "P001", Name = "James", Age = 30 };

            dbContext.Person.Add(james);

            var chineseBook = new Book() { BookCode = "B001", Isbn = "001", BookName = "Chinese" };//没有设置Book表中外键列PersonCode的值
            var japaneseBook = new Book() { BookCode = "B002", Isbn = "001", BookName = "Japanese" };//没有设置Book表中外键列PersonCode的值
            var englishBook = new Book() { BookCode = "B003", Isbn = "001", BookName = "English" };//没有设置Book表中外键列PersonCode的值

            //插入三条数据到Book表
            dbContext.Book.Add(chineseBook);
            dbContext.Book.Add(japaneseBook);
            dbContext.Book.Add(englishBook);

            dbContext.SaveChanges();
        }
    }

    /// <summary>
    /// 删除Person表和Book表的所有数据
    /// </summary>
    static void DeleteAllData()
    {
        using (var dbContext = new TestDBContext())
        {
            dbContext.Database.ExecuteSqlCommand("DELETE FROM [dbo].[Book]");
            dbContext.Database.ExecuteSqlCommand("DELETE FROM [dbo].[Person]");
        }
    }

    /// <summary>
    /// 不正确地设置Person表和Book表的关联关系,这种方法会让EF Core错误地生成INSERT语句,而不是UPDATE语句
    /// </summary>
    static void SetRelationshipIncorrectly()
    {
        using (var dbContext = new TestDBContext())
        {
            var james = dbContext.Person.First(e => e.Name == "James");//首先通过DbContext从数据库中查询出要建立关联关系的Person表实体

            var chineseBook = new Book() { BookCode = "B001" };//只构造Book实体的Key属性即可,根据BookCode值"B001"来构造Chinese Book
            var japaneseBook = new Book() { BookCode = "B002" };//只构造Book实体的Key属性即可,根据BookCode值"B002"来构造Japanese Book
            var englishBook = new Book() { BookCode = "B003" };//只构造Book实体的Key属性即可,根据BookCode值"B003"来构造English Book

            Console.WriteLine($"Before adding, chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//可以看到由于此时Book实体chineseBook没有被DbContext跟踪,所以状态是Detached
            Console.WriteLine($"Before adding, japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//可以看到由于此时Book实体japaneseBook没有被DbContext跟踪,所以状态是Detached
            Console.WriteLine($"Before adding, englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//可以看到由于此时Book实体englishBook没有被DbContext跟踪,所以状态是Detached

            Console.WriteLine();
                
            james.Book = new List<Book>();//由于我们在上面调用dbContext.Person.First(e => e.Name == "James")时,没有用EF Core中Eager Loading的Include方法来加载Book实体集合,所以这里要用List类来构造一个Book实体集合,否则james.Book为null

            james.Book.Add(chineseBook);//添加chineseBook到Person类的Book实体集合
            Console.WriteLine("chineseBook was added into Person.Book collection");

            james.Book.Add(japaneseBook);//添加japaneseBook到Person类的Book实体集合
            Console.WriteLine("japaneseBook was added into Person.Book collection");

            james.Book.Add(englishBook);//添加englishBook到Person类的Book实体集合
            Console.WriteLine("englishBook was added into Person.Book collection");

            Console.WriteLine();

            Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//调用DbContext.Entry()方法后,DbContext发现一个原本状态是Detached的Book实体chineseBook被加入到Person.Book集合中了,所以此时chineseBook的实体状态变为了Added
            Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//调用DbContext.Entry()方法后,DbContext发现一个原本状态是Detached的Book实体japaneseBook被加入到Person.Book集合中了,所以此时japaneseBook的实体状态变为了Added
            Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//调用DbContext.Entry()方法后,DbContext发现一个原本状态是Detached的Book实体englishBook被加入到Person.Book集合中了,所以此时englishBook的实体状态变为了Added

            dbContext.SaveChanges();//由于此时chineseBook、japaneseBook和englishBook的EntityState都是Added,所以此时DbContext.SaveChanges方法调用后,EF Core生成的是INSERT语句,将chineseBook、japaneseBook和englishBook插入数据库表Book,导致插入了重复值到唯一键列BookCode,所以数据库报错
        }
    }

    /// <summary>
    /// 正确地设置Person表和Book表的关联关系,这种方法会让EF Core正确地生成UPDATE语句,在数据库中设置Book表的PersonCode列数据
    /// </summary>
    static void SetRelationshipCorrectly()
    {
        using (var dbContext = new TestDBContext())
        {
            var james = dbContext.Person.First(e => e.Name == "James");//首先通过DbContext从数据库中查询出要建立关联关系的Person表实体

            var chineseBook = new Book() { BookCode = "B001" };//只构造Book实体的Key属性即可,根据BookCode值"B001"来构造Chinese Book
            var japaneseBook = new Book() { BookCode = "B002" };//只构造Book实体的Key属性即可,根据BookCode值"B002"来构造Japanese Book
            var englishBook = new Book() { BookCode = "B003" };//只构造Book实体的Key属性即可,根据BookCode值"B003"来构造English Book

            dbContext.Attach(chineseBook);//将chineseBook关联到DbContext,开始跟踪
            dbContext.Attach(japaneseBook);//将japaneseBook关联到DbContext,开始跟踪
            dbContext.Attach(englishBook);//将englishBook关联到DbContext,开始跟踪

            Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//由于上面chineseBook被Attach到DbContext开始跟踪了,所以此时chineseBook的实体状态是Unchanged
            Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//由于上面japaneseBook被Attach到DbContext开始跟踪了,所以此时japaneseBook的实体状态是Unchanged
            Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//由于上面englishBook被Attach到DbContext开始跟踪了,所以此时englishBook的实体状态是Unchanged

            Console.WriteLine();

            james.Book = new List<Book>();//由于我们在上面调用dbContext.Person.First(e => e.Name == "James")时,没有用EF Core中Eager Loading的Include方法来加载Book实体集合,所以这里要用List类来构造一个Book实体集合,否则james.Book为null

            james.Book.Add(chineseBook);//添加chineseBook到Person类的Book实体集合
            Console.WriteLine("chineseBook was added into Person.Book collection");

            james.Book.Add(japaneseBook);//添加japaneseBook到Person类的Book实体集合
            Console.WriteLine("japaneseBook was added into Person.Book collection");

            james.Book.Add(englishBook);//添加englishBook到Person类的Book实体集合
            Console.WriteLine("englishBook was added into Person.Book collection");

            Console.WriteLine();

            Console.WriteLine($"Berfore querying DbContext.Entry(chineseBook), chineseBook.PersonCode is :{chineseBook.PersonCode ?? "null"}");//此时由于我们还没有调用DbContext.Entry()方法,所以DbContext还无法察觉到chineseBook已经被添加到Person类的Book实体集合了,所以chineseBook.PersonCode为null
            Console.WriteLine($"After querying DbContext.Entry(chineseBook), chineseBook entity state is :{dbContext.Entry(chineseBook).State.ToString()}");//调用DbContext.Entry()方法后,DbContext发现一个原本状态是Unchanged的Book实体chineseBook被加入到Person.Book集合中了,所以此时chineseBook的实体状态变为了Modified
            Console.WriteLine($"After querying DbContext.Entry(chineseBook),  chineseBook.PersonCode is :{chineseBook.PersonCode}");//由于上面我们调用DbContext.Entry(chineseBook)使得DbContext得知了chineseBook被加入到Person.Book集合中了,所以DbContext还将Book实体的外键属性PersonCode也进行了赋值,为P001

            Console.WriteLine();

            Console.WriteLine($"Berfore querying DbContext.Entry(japaneseBook),japaneseBook.PersonCode is :{japaneseBook.PersonCode ?? "null"}");//很有意思的是我们上面在chineseBook上调用DbContext.Entry()方法后,japaneseBook的PersonCode属性也不为null了,变为了P001,说明调用一次DbContext.Entry()方法后,会引发DbContext重新检查所有被跟踪实体的状态
            Console.WriteLine($"After querying DbContext.Entry(japaneseBook), japaneseBook entity state is :{dbContext.Entry(japaneseBook).State.ToString()}");//在上面为chineseBook调用DbContext.Entry()方法时,DbContext同时发现了原本状态是Unchanged的Book实体japaneseBook,也被加入到了Person.Book集合中,所以japaneseBook的实体状态也变为了Modified
            Console.WriteLine($"After querying DbContext.Entry(japaneseBook),  japaneseBook.PersonCode is :{japaneseBook.PersonCode}");//在上面为chineseBook调用DbContext.Entry()方法时,DbContext得知了japaneseBook也被加入到了Person.Book集合中,所以DbContext将japaneseBook的PersonCode属性也赋值为P001了

            Console.WriteLine();

            Console.WriteLine($"Berfore querying DbContext.Entry(englishBook),englishBook.PersonCode is :{englishBook.PersonCode ?? "null"}");//在上面为chineseBook调用DbContext.Entry()方法时,DbContext得知了englishBook也被加入到了Person.Book集合中,所以DbContext将englishBook的PersonCode属性也赋值为P001了
            Console.WriteLine($"After querying DbContext.Entry(englishBook), englishBook entity state is :{dbContext.Entry(englishBook).State.ToString()}");//在上面为chineseBook调用DbContext.Entry()方法时,DbContext同时发现了原本状态是Unchanged的Book实体englishBook,也被加入到了Person.Book集合中,所以englishBook的实体状态也变为了Modified
            Console.WriteLine($"After querying DbContext.Entry(englishBook),  englishBook.PersonCode is :{englishBook.PersonCode}");//在上面为chineseBook调用DbContext.Entry()方法时,DbContext得知了englishBook也被加入到了Person.Book集合中,所以DbContext将englishBook的PersonCode属性也赋值为P001了

            dbContext.SaveChanges();//由于此时chineseBook、japaneseBook和englishBook的EntityState都是Modified,所以此时DbContext.SaveChanges方法调用后,EF Core生成的是UPDATE语句,通过更新数据库Book表的PersonCode列,将Chinese、Japanese和English三行Book数据同Person表的数据成功关联了起来
        }
    }


    static void Main(string[] args)
    {
        DeleteAllData();//调用DeleteAllData方法删除Person表和Book表的所有数据,防止有脏数据
        InitData();//初始化Person表和Book表的数据

        SetRelationshipCorrectly();//正确地的设置Person表和Book表的关联关系
        SetRelationshipIncorrectly();//不正确地的设置Person表和Book表的关联关系,该方法会抛出异常错误

        Console.WriteLine("Press any key to quit...");
        Console.ReadKey();
    }
}

示例代码中的DeleteAllData方法,是清表语句,用来删除Person表和Book表的所有数据,防止有脏数据。

InitData方法用来初始化Person表和Book表的数据,Person表插入了一行数据,Book表插入了三行数据且PersonCode列都为NULL,调用InitData方法后数据库Person表和Book表的数据就和上面示例代码前的两个截图相同了。

测试SetRelationshipIncorrectly方法

先将示例代码的Main方法改为如下:

static void Main(string[] args)
{
    DeleteAllData();//调用DeleteAllData方法删除Person表和Book表的所有数据,防止有脏数据
    InitData();//初始化Person表和Book表的数据

    //SetRelationshipCorrectly();//正确地的设置Person表和Book表的关联关系
    SetRelationshipIncorrectly();//不正确地的设置Person表和Book表的关联关系,该方法会抛出异常错误

    Console.WriteLine("Press any key to quit...");
    Console.ReadKey();
}

SetRelationshipIncorrectly方法用来演示怎么错误地设置Person表和Book表的关联关系,可以看到由于我们在其中新建的三个Book实体

var chineseBook = new Book() { BookCode = "B001" };//只构造Book实体的Key属性即可,根据BookCode值"B001"来构造Chinese Book
var japaneseBook = new Book() { BookCode = "B002" };//只构造Book实体的Key属性即可,根据BookCode值"B002"来构造Japanese Book
var englishBook = new Book() { BookCode = "B003" };//只构造Book实体的Key属性即可,根据BookCode值"B003"来构造English Book

最终在调用DbContext.SaveChanges方法时其实体状态都是Added,所以调用DbContext.SaveChanges方法时,EF Core在数据库中生成的是INSERT语句,尝试将这三个实体数据插入数据库Book表,由于调用InitData方法后,数据库Book表中已经有相同PersonCode列值的数据了,Book表的PersonCode列又是唯一键,所以DbContext.SaveChanges方法抛出异常。

我们可以从EF Core的后台日志中,查看到调用DbContext.SaveChanges方法时生成的是INSERT语句:

=============================== EF Core log started ===============================
SaveChanges starting for 'TestDBContext'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
DetectChanges starting for 'TestDBContext'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
DetectChanges completed for 'TestDBContext'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Opening connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Opened connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Beginning transaction with isolation level 'ReadCommitted'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executing update commands individually as the number of batchable commands (3) is smaller than the minimum batch size (4).
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Executing DbCommand [Parameters=[@p0='?' (Size = 20), @p1='?' (Size = 50), @p2='?' (Size = 20), @p3='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Book] ([BookCode], [BookName], [ISBN], [PersonCode])
VALUES (@p0, @p1, @p2, @p3);
SELECT [ID]
FROM [Book]
WHERE @@ROWCOUNT = 1 AND [BookCode] = @p0;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Failed executing DbCommand (8ms) [Parameters=[@p0='?' (Size = 20), @p1='?' (Size = 50), @p2='?' (Size = 20), @p3='?' (Size = 20)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Book] ([BookCode], [BookName], [ISBN], [PersonCode])
VALUES (@p0, @p1, @p2, @p3);
SELECT [ID]
FROM [Book]
WHERE @@ROWCOUNT = 1 AND [BookCode] = @p0;
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Disposing transaction.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Closing connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================
=============================== EF Core log started ===============================
Closed connection to database 'TestDB' on server 'localhost'.
=============================== EF Core log finished ===============================

在SetRelationshipIncorrectly方法中我们还输出了chineseBook、japaneseBook和englishBook三个Book实体的EntityState,可以看到将chineseBook、japaneseBook和englishBook三个Book实体添加到Person类的Book实体集合后,EntityState发生了相应的变化。

猜你喜欢

转载自www.cnblogs.com/OpenCoder/p/9820064.html