处理 EntityFramework Core 中的重复键违规

Ste*_*ord 6 c# entity-framework entity-framework-core .net-core

根据其他问题(此处和此处),可以通过捕获抛出的异常并检查它的InnerException.

当DbContext.SaveChanges()使用一组重复的数据调用时,会抛出一个异常,但它是一个相当标准的InvalidOperationException,它InnerException是null.

如何检测Entity Framework Core 中的重复键违规?

更新更多上下文(双关语)

我试图捕获/检测的特定违规是在通过多对多关系连接的两个实体(团队和用户)之间添加链接时。

System.InvalidOperationException:无法跟踪实体类型“TeamUser”的实例,因为已在跟踪具有相同键的此类型的另一个实例。添加新实体时,对于大多数键类型,如果未设置键(即,如果键属性为其类型分配了默认值),将创建一个唯一的临时键值。如果您为新实体显式设置键值,请确保它们不会与现有实体或为其他新实体生成的临时值发生冲突。附加现有实体时,请确保只有一个具有给定键值的实体实例附加到上下文。

用户实体类:

public class User
{
    [Key]
    public string Name { get; set; }

    public ICollection<TeamUser> TeamUsers { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

团队实体类:

public class Team
{
    [Key]
    public string Id { get; set; }

    [Required]
    public string Name { get; set; }

    public ICollection<Template> Templates { get; set; }
    public ICollection<Checklist> Checklists { get; set; }

    public ICollection<TeamUser> TeamUsers { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

TeamUser 实体类:

public class TeamUser
{
    public string TeamId { get; set; }
    public Team Team { get; set; }

    public string UserName { get; set; }
    public User User { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我的DbContext子类配置了 Teams 和 Users 之间的多对多关系:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var teamUserEntity = modelBuilder.Entity<TeamUser>();

    teamUserEntity
        .HasKey(tu => new { tu.TeamId, tu.UserName });

    teamUserEntity
        .HasOne(tu => tu.Team)
        .WithMany(t => t.TeamUsers)
            .HasForeignKey(tu => tu.TeamId);

    teamUserEntity
        .HasOne(tu => tu.User)
        .WithMany(u => u.TeamUsers)
        .HasForeignKey(tu => tu.UserName);
}
Run Code Online (Sandbox Code Playgroud)

EF Core 生成的TeamUser表如下:

CREATE TABLE "TeamUser" (
    "TeamId" TEXT NOT NULL,
    "UserName" TEXT NOT NULL,
    CONSTRAINT "PK_TeamUser" PRIMARY KEY ("TeamId", "UserName"),
    CONSTRAINT "FK_TeamUser_Teams_TeamId" FOREIGN KEY ("TeamId") REFERENCES "Teams" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_TeamUser_Users_UserName" FOREIGN KEY ("UserName") REFERENCES "Users" ("Name") ON DELETE CASCADE
);
CREATE INDEX "IX_TeamUser_UserName" ON "TeamUser" ("UserName");
Run Code Online (Sandbox Code Playgroud)

Gio*_*rgi 7

为了处理这样的情况,我编写了一个开源库:EntityFramework.Exceptions它允许您捕获如下类型的异常:

using (var demoContext = new DemoContext())
{
    demoContext.Products.Add(new Product
    {
        Name = "a",
        Price = 1
    });

    demoContext.Products.Add(new Product
    {
        Name = "a",
        Price = 1
    });

    try
    {
        demoContext.SaveChanges();
    }
    catch (UniqueConstraintException e)
    {
        //Handle exception here
    }
}
Run Code Online (Sandbox Code Playgroud)

您所要做的就是从 Nuget 安装它并在您的OnConfiguring方法中调用它:

class DemoContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<ProductSale> ProductSale { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseExceptionProcessor();
    }
}
Run Code Online (Sandbox Code Playgroud)


kim*_*udi 3

您无法检测重复键违规的原因是您使用 dbcontext 的单个实例来保存重复数据。让我用一个示例控制器来解释一下:

MyController.cs

public class MyController : Controller
{
    private readonly MyDbContext _context;

    public MyController(MyDbContext context)
    {
        _context = context;
    }

    public IActionResult AddFirst()
    {
        var user = new User
        {
            Name = "Alice"
        };
        _context.Users.Add(user);

        var team = new Team
        {
            Id = "uniqueteamid",
            Name = "A Team"
        };
        _context.Teams.Add(team);

        var teamuser1 = new TeamUser()
        {
            User = user,
            Team = team
        };
        _context.TeamUsers.Add(teamuser1);

        _context.SaveChanges();

        return View();
    }

    public IActionResult AddSecond()
    {
        var teamuser2 = new TeamUser()
        {
            UserName = "Alice",
            TeamId = "uniqueteamid"
        };
        _context.TeamUsers.Add(teamuser2);

        _context.SaveChanges();

        return View();
    }

    public IActionResult AddFirstAndSecond()
    {
        var user = new User
        {
            Name = "Bob"
        };
        _context.Users.Add(user);

        var team = new Team
        {
            Id = "anotherteamid",
            Name = "B Team"
        };
        _context.Teams.Add(team);

        var teamuser1 = new TeamUser()
        {
            User = user,
            Team = team
        };
        _context.TeamUsers.Add(teamuser1);

        var teamuser2 = new TeamUser()
        {
            User = user,
            Team = team
        };
        _context.TeamUsers.Add(teamuser2);

        _context.SaveChanges();

        return View();
    }

    public IActionResult AddFirstAndSecondAgain()
    {
        var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
        optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=aspnet-WebApplication1;Trusted_Connection=True;MultipleActiveResultSets=true");

        using (var context = new MyDbContext(optionsBuilder.Options))
        {
            var user = new User
            {
                Name = "Cat"
            };
            context.Users.Add(user);
            context.SaveChanges();
        }

        using (var context = new MyDbContext(optionsBuilder.Options))
        {
            var team = new Team
            {
                Id = "andanotherteamid",
                Name = "C Team"
            };
            context.Teams.Add(team);
            context.SaveChanges();
        }

        using (var context = new MyDbContext(optionsBuilder.Options))
        {
            var teamuser1 = new TeamUser()
            {
                UserName = "Cat",
                TeamId = "andanotherteamid"
            };
            context.TeamUsers.Add(teamuser1);
            context.SaveChanges();
        }

        using (var context = new MyDbContext(optionsBuilder.Options))
        {
            var teamuser2 = new TeamUser()
            {
                UserName = "Cat",
                TeamId = "andanotherteamid"
            };
            context.TeamUsers.Add(teamuser2);
            context.SaveChanges();
        }

        return View();
    }
}
Run Code Online (Sandbox Code Playgroud)

在该控制器中,有 4 个操作方法:AddFirst、AddSecond、AddFirstAndSecond、 和AddFirstAndSecondAgain。

情况1(AddFirst和AddSecond):

假设AddFirst首先被调用。这将创建一个新用户、一个新团队和一个团队用户。现在,如果AddSecond之后调用,这将尝试添加重复的 TeamUser 并抛出重复的键违规异常。原因是第二次插入重复 TeamUser 的调用使用与第一次插入 TeamUser 的调用不同的 dbcontext 实例。

情况2 ( AddFirstAndSecond):

假设你打电话给AddFirstAndSecond. 这将抛出无效操作异常。为什么?因为您使用 dbcontext 的单个实例来添加第一个 TeamUser 和第二个重复的 TeamUser。Entity Framework Core 已在跟踪第一个 TeamUser,因此无法跟踪第二个重复的 TeamUser。

情况3(AddFirstAndSecondAgain):

如果您确实需要在单个操作方法中添加重复的 TeamUser,则在添加每个 TeamUser 时需要使用不同的 dbcontext 实例。看看AddFirstAndSecondAgain动作方法。这还会引发重复键冲突异常,因为您使用 dbcontext 的不同实例来添加第一个和第二个重复的 TeamUser。