【问题标题】:Handle duplicate key violations in EntityFramework Core处理 EntityFramework Core 中的重复键违规
【发布时间】:2017-05-28 16:39:50
【问题描述】:

根据其他问题(herehere),可以通过捕获抛出的异常并检查它的 InnerException 来捕获 Entity Framework 6 中的唯一键违规。

当使用重复的数据集调用DbContext.SaveChanges() 时,会引发异常,但这是一个相当标准的InvalidOperationException,它的InnerExceptionnull

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

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

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

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

用户实体类:

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

    public ICollection<TeamUser> TeamUsers { get; set; }
}

团队实体类:

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; }
}

TeamUser 实体类:

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

    public string UserName { get; set; }
    public User User { get; set; }
}

我的DbContext 子类配置团队和用户之间的多对多关系:

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);
}

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");

【问题讨论】:

  • 您是否在数据库上下文中设置了唯一约束?
  • 我的意思是这样的:entity.HasAlternateKey(p => new { p.ProductName }).HasName("U_ProductName");
  • @H.Herzl 请检查更新的问题,我已经添加了相关的实体类和OnModelCreating 方法。
  • 对不起,我没有很好地解释,因为我理解你的问题,你需要捕捉一个独特的约束违规;所以我的建议是在 DbContext 中为您的唯一约束添加配置并尝试操作以查看异常详细信息,这有意义吗?
  • @H.Herzl 据我所知,我已经正确设置了唯一约束。我添加了相关的表模式,它显示主键是TeamIdUserName 的复合键。问题不在于防止重复,而是检测系统何时尝试插入重复。

标签: c# entity-framework .net-core entity-framework-core


【解决方案1】:

为了处理这样的情况,我编写了一个开源库: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
    }
}

您所要做的就是从 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();
    }
}

【讨论】:

  • 这个图书馆很棒
【解决方案2】:

您无法检测到重复键违规的原因是因为您使用 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();
    }
}

在此控制器中,有 4 种操作方法:AddFirstAddSecondAddFirstAndSecondAddFirstAndSecondAgain

案例 1(AddFirstAddSecond):

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

案例 2 (AddFirstAndSecond):

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

案例 3 (AddFirstAndSecondAgain):

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

【讨论】:

  • 抱歉,但我并没有真正按照你的建议去做。能不能看一下我推送到 Github 上的sample project 多指点一下?
  • 当您使用 Add 方法向 DbContext 添加内容时,上下文开始跟踪它(以监控在调用 SaveChanges 时是否需要保存内容)。如果您尝试将同一实例多次添加到上下文中,则 EF 会引发无法跟踪新实体的异常。这就像在单个 DbContext 实例中的跟踪实体内发生的唯一约束违规。作为指针,您应该查找异常发生的位置以及在同一上下文实例中添加两次的实体是什么。这将帮助您找到添加了两次的内容。
猜你喜欢
  • 2021-09-03
  • 1970-01-01
  • 2015-03-05
  • 2022-07-04
  • 1970-01-01
  • 2017-03-14
  • 2018-02-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多