1. 程式人生 > >EF Core中如何正確地設定兩張表之間的關聯關係

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發生了相應的變化。