How to resolve the update problem tracking instance in many-to-many in EFCore 7.0.10

Introduction

There is a scene that 2 classes are related by a join-table-like class. For example:

A class defines some basic properties, one of which is a set of Project, which means an object may refers to many Project

/*AssemblyLanguage.cs*/
public class AssemblyLanguage{
   public int ID {get;set;}
   public string Mnemonic {get;set;}
   public List<Project> Projects {get;set;}
}

/*Project.cs*/
   public class Project
   {
       public int ID { get; set; }
       
       public string Name { get; set; }
       
       public int ParentID { get; set; }

       /*-----self-reference, not the point in the text----*/
       public Project Parent { get; set; }
   	public List<Project> Children { get; set; }
       /*-------------------------------------------------*/
       public List<AssemblyLanguage> Nemonics { get; set; }
   }

The definition of DbContext, in which Entity Framework Core migrates and manipulates data from objects to database, is below.

public class AssemblyLangTranslatorDbContext : DbContext
{
        private string? DbPath = string.Empty;

        public AssemblyLangTranslatorDbContext()
        {
            DbPath = AppConfigurtaionServices.Configuration.GetSection("ConnectionStrings").GetSection("AssemblyLangTranslatorContext").Value;
        }


        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            options.UseSqlServer(DbPath);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Project>().Property(p=>p.Name).HasMaxLength(16);
            modelBuilder.Entity<Project>().HasData(new Project() {ID=1, Name = "项目", ParentID=1 });
            modelBuilder.Entity<Project>().HasMany(p => p.Mnemonics).WithMany(n => n.Projects)
                 .UsingEntity(
                    "AssemblyLanguageProject",
                    l => l.HasOne(typeof(AssemblyLanguage)).WithMany().HasForeignKey("AssemblyLanguageId").HasPrincipalKey(nameof(AssemblyLanguage.ID)).OnDelete(DeleteBehavior.Restrict),
                    r => r.HasOne(typeof(Project)).WithMany().HasForeignKey("ProjectId").HasPrincipalKey(nameof(Project.ID)).OnDelete(DeleteBehavior.Restrict),
                    j => j.HasKey("AssemblyLanguageId", "ProjectId"));

            modelBuilder.Entity<AssemblyLanguage>().Property(al => al.Nemonic).HasMaxLength(32);

        }

        public DbSet<Project> Projects { get; set; } = default!;!;

        public DbSet<AssemblyLanguage> AssemblyLanguages { get; set; } = default!;
}

As we can see, the name of the join-table defined by UsingEntity method is AssemblyLanguageProject, which combines the ProjectId and AssemblyLanguageId as PrimaryKey and specifies the foreign keys respectively on them.

在这里插入图片描述

By migration-tools from EntityFrameworkCore.Design package, database could be automatically built by two simple commands in PowerShell or terminal as mentioned at https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=vs

PS D:\src\cs\M2MUpdateSample\M2MUpdateSample\LibM2MUpdateSample> dotnet ef migrations add InitialCreate
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'

PS D:\src\cs\M2MUpdateSample\M2MUpdateSample\LibM2MUpdateSample> dotnet ef database update
Build started...
Build succeeded.
Applying migration '20230901010942_InitialCreate'.
Done.

Basic Db Operation

Insert operations will be implemented in their own class. For example, “jdk” added into table PROJECTS unit test is below.

/*Project.cs*/
[Test]
public void TestInsert()
{
    Project project = new Project();
    project.Name = "jdk";
    project.ParentID = 1;
    int result = project.Insert();

    Assert.AreEqual(1, result);
}

Assuming after several steps, some mnemonics have already been inserted into table AssemblyLanguage, and the manipulation, which will be introduced next, is to connect the ‘Project’ and ‘AssemblyLanguage’
在这里插入图片描述

figure 1. Table Projects

在这里插入图片描述

figure 2. Table AssemblyLanguages

To update the relations between projects and AssemblyLanguages, what firstly should be done is getting list of projects in AssemblyLanguage class. Certainly, it can be easily implemented by EFCore interfaces.

using (var context = new AssemblyLangTranslatorDbContext())
{
    var item = context.AssemblyLanguages.Include(al => al.Projects).First(r => r.ID == this.ID);
}

Next, Update is coming

using (var context = new AssemblyLangTranslatorDbContext())
{
	var item = context.AssemblyLanguages.Include(al => al.Projects).First(r => r.ID == this.ID);
    ...
    item.ID = this.ID;
    item.Mnemonic = this.Mnemonic;

    item.Projects = this.Projects;
    context.Update(item);
    result = context.SaveChanges();
}

An exception occurs.

Unexpected result and exception

Firstly, paying attention the pattern of assignment of List. Previous statement, this.Projects is assigned incautiously to the property of item without thinking the tracking state of item and its belongings-Projects, which from the Include(al=>al.Projects) .

So, Projects assigning operation is changed to one-by-one.

foreach (var project in Projects)
{
    item.Projects.Add(project);
}

exception still occurs. To find out what causes the exception, options.EnableSensitiveDataLogging();, which enables to output the information about application data, was add to the OnConfiguring method.

protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    options.UseSqlServer(DbPath);
    options.EnableSensitiveDataLogging();
}

and restart the unit test again, the details of exception outputs.

SqlException: The UPDATE statement conflicted with the FOREIGN KEY SAME TABLE constraint "FK_Projects_Projects_ParentID". The conflict occurred in database "AssemblyLangProjectDb", table "dbo.Projects", column 'ID'.

Reason

Obviously , the exception is about the ParentID property of Project class, but the root of the problem is related to entity tracking, which comes from context. Every object’s operation for database must be from DbContext, which also means the object should be an entity tracked by the context, or attach to the database .

Resolution

Create an element from context, and add them to the Projects property of AssemblyLanguage, Everything is OK. Code as below.

foreach (var project in Projects)
{
    var pItem = context.Projects.Find(project.ID);
    item.Projects.Add(pItem);
}

Conclusion

In summary, every operation related to database should connects to the DbContext.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值