EF 核心 |添加相关实体 |数据库操作预计影响1行,实际影响0行

问题描述 投票:0回答:1

无法弄清楚为什么我会收到以下错误:

{
  "title": "Server Error",
  "status": 500,
  "detail": "The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions."
}

这是完整的例外:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException
  HResult=0x80131500
  Message=The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
  Source=Npgsql.EntityFrameworkCore.PostgreSQL
  StackTrace:
   at Npgsql.EntityFrameworkCore.PostgreSQL.Update.Internal.NpgsqlModificationCommandBatch.<ThrowAggregateUpdateConcurrencyExceptionAsync>d__10.MoveNext()
   at Npgsql.EntityFrameworkCore.PostgreSQL.Update.Internal.NpgsqlModificationCommandBatch.<Consume>d__7.MoveNext()
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.<ExecuteAsync>d__50.MoveNext()
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.<ExecuteAsync>d__50.MoveNext()
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.<ExecuteAsync>d__9.MoveNext()
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.<ExecuteAsync>d__9.MoveNext()
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.<ExecuteAsync>d__9.MoveNext()
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__111.MoveNext()
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__115.MoveNext()
   at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.<ExecuteAsync>d__7`2.MoveNext()
   at Microsoft.EntityFrameworkCore.DbContext.<SaveChangesAsync>d__63.MoveNext()
   at Persistence.IKATDbContext.<SaveEntitiesAsync>d__43.MoveNext() in C:\Users\<user>\source\repos\ikat\Persistence\IKATDbContext.cs:line 54

  This exception was originally thrown at this call stack:
    [External Code]
    Persistence.IKATDbContext.SaveEntitiesAsync(System.Threading.CancellationToken) in IKATDbContext.cs

我是本地环境中唯一进行测试的。我按照文档工作这里

代码似乎也到位,但我肯定遗漏了一些东西。请注意,我通过删除验证等简化了代码。

我有两个实体:

public class Tutor : Entity, IAggregateRoot
{
    public Guid Id { get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public ICollection<Availability> Availabilities { get; private set; }

    public Tutor(string firstName, string lastName)
    {
        this.Id = Guid.NewGuid();
        this.FirstName = firstName;
        this.LastName = lastName;
        this.Availabilities = [];
    }

    public static Tutor Create(string firstName, string lastName)
    {    
        return new Tutor(firstName, lastName);
    }

    public Availability AddAvailability(DateTime startDateTime, DateTime endDateTime, bool recurring)
    {
        Availability availability = new(this.Id, startDateTime, endDateTime, recurring);
        this.Availabilities.Add(availability);
        return availability;
    }
}

并且:

public class Availability : Entity
{
    public Guid Id { get; private set; }
    public Guid TutorId { get; private set; }
    public Tutor Tutor { get; private set; }
    public DateTime StartDateTime { get; private set; }
    public DateTime EndDateTime { get; private set; }
    public bool Recurring { get; private set; }

    public Availability(Guid tutorId, DateTime startDateTime, DateTime endDateTime, bool recurring)
    {
        this.Id = Guid.NewGuid();
        this.TutorId = tutorId;
        this.StartDateTime = startDateTime;
        this.EndDateTime = endDateTime;
        this.Recurring = recurring;
    }
}

这些是实体配置:

public class TutorEntityConfiguration : IEntityTypeConfiguration<Tutor>
{
    public void Configure(EntityTypeBuilder<Tutor> builder)
    {
        builder.ToTable("Tutors");

        builder.HasKey(t => t.Id);

        builder.Property(t => t.FirstName)
            .IsRequired();

        builder.Property(t => t.LastName)
            .IsRequired();

        builder.HasMany(t => t.Availabilities)
            .WithOne(a => a.Tutor)
            .HasForeignKey(a => a.TutorId);
    }
}

并且:

public class AvailabilityEntityConfiguration : IEntityTypeConfiguration<Availability>
{
    public void Configure(EntityTypeBuilder<Availability> builder)
    {
        builder.ToTable("Availabilities");

        builder.HasKey(a => a.Id);

        builder.Property(a => a.StartDateTime)
            .IsRequired();

        builder.Property(a => a.EndDateTime)
            .IsRequired();

        builder.Property(a => a.Recurring)
            .IsRequired();

        builder.HasOne(a => a.Tutor)
            .WithMany(t => t.Availabilities)
            .HasForeignKey(a => a.TutorId)
            .IsRequired();
    }
}

这些是我正在使用的

TutorRepository
方法:

public void Update(Tutor tutor)
{
    this.context.Entry(tutor).State = EntityState.Modified;
}

public async Task<Tutor> GetTutorByIdAsync(Guid tutorId, CancellationToken cancellationToken)
{
    return await this.context.Tutors
                             .Include(t => t.Availabilities)
                             .FirstOrDefaultAsync(t => t.Id == tutorId, cancellationToken);
}

最后是 CommandHandler:

public async Task<Guid> Handle(CreateAvailabilityCommand request, CancellationToken cancellationToken)
{
    Tutor tutor = await this.tutorRepository.GetTutorByIdAsync(request.TutorId, cancellationToken) ?? throw new Exception("Tutor not found");
    tutor.AddAvailability(request.StartTime, request.EndTime, false);

    this.tutorRepository.Update(tutor);
    _ = await this.tutorRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
    return tutor.Id;
}
c# .net entity-framework-core repository-pattern
1个回答
0
投票

问题几乎肯定源于您映射关系的方式以及过度执行的步骤,这些步骤可能最终会在尝试插入数据时使 EF 感到困惑。 EF 将尝试遵循诸如期望名为“Id”或“{TEntity}Id”的列作为标识列之类的约定,如果您打算在代码中设置 PK 值,这可能会有点冲突。 (不推荐)存储库模式可能也不会给您带来任何好处。

过度配置关系也会让工作变得糟糕。在处理一对多等关系时,请从一侧或另一侧进行配置,而不是两者都进行配置。

对于实体配置:

public class Tutor : Entity, IAggregateRoot
{
    public Guid Id { get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public ICollection<Availability> Availabilities { get; private set; } = new [];

    private Tutor(string firstName, string lastName)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }

    protected Tutor () { }

    public static Tutor Create(string firstName, string lastName)
    {    
        return new Tutor(firstName, lastName);
    }

    public Availability AddAvailability(DateTime startDateTime, DateTime endDateTime, bool recurring)
    {
        Availability availability = Availability.Create(startDateTime, endDateTime, recurring);
        this.Availabilities.Add(availability);
        return availability;
    }
}

像上面这样的东西对于工厂方法和更多 DDD 方法来聚合根来说是可以的。您应该避免在代码中分配 PK 甚至 FK。我会扩展静态工厂方法,而不是公开构造函数加工厂方法。您应该有一个受保护的默认构造函数,以使 EF 在读取数据时保持满意。它可以使用简单的值构造函数,但如果您有一个采用任何相关实体的构造函数(您可能需要一些聚合),它会犹豫不决

从那里简化实体配置:

public class TutorEntityConfiguration : IEntityTypeConfiguration<Tutor>
{
    public void Configure(EntityTypeBuilder<Tutor> builder)
    {
        builder.ToTable("Tutors");

        builder.HasKey(t => t.Id)
             .DatabaseGenerated(DatabaseGeneratedOption.Identity);

        builder.Property(t => t.FirstName)
            .IsRequired();

        builder.Property(t => t.LastName)
            .IsRequired();

        builder.HasMany(t => t.Availabilities)
            .WithOne(a => a.Tutor)
            .HasForeignKey(a => a.TutorId);
    }
}

这很好,尽管我会将 Id 标记为 DatabaseGenerate Identity,其中数据库设置为默认值

NewId
或更好,
NewSequentialId
假设是 SQL Server。如果您想使用客户端分配 (Guid.New()),那么我会将其设置为
DatabaseGeneratedOption.None
以确保 EF 在创建时将其视为期望和 ID。

然后在可用性配置中删除:

    //builder.HasOne(a => a.Tutor)
    //    .WithMany(t => t.Availabilities)
    //    .HasForeignKey(a => a.TutorId)
    //    .IsRequired();

该关系是在另一端配置的,而不是在两端配置的。

现在说到保存,我真的强烈建议重新考虑添加存储库作为 EF 的抽象。您想要的最简单的形式:

var tutor = await _context.Tutors
    .Include(x => x.Availabilities)
    .SingleAsync(x => x.Id == tutorId);
tutor.AddAvailability(request.StartTime, request.EndTime, false);
await _context.SaveChangesAsync();

就是这样,简单直接。 EF 的

DbContext
已经是一个工作单元,并且
DbSet<TEntity>
已经用作存储库。 EF 将自动连接 FK 和导航属性。尝试显式设置它们可能会让事情变得糟糕。

如果您确实想使用存储库,则应该出于正确的原因,例如集中公共域规则(如租赁、授权和/或软删除等)或更轻松地促进单元测试,而不是抽象来自您的业务逻辑/域的 EF。我用于存储库的模式是让它们公开

IQueryable<TEntity>
,而不是返回
IEnumeratble<TEntity>
甚至
TEntity
。这提供了许多优点,例如轻量级且易于模拟的存储库层,以及支持投影、自定义过滤、排序、分页等的完全灵活性。此外,我还要求存储库充当工厂方法容器。这样,“CreateTutor”不仅会创建 Tutor 实体,还会将其添加到 Tutor 的
Dbset
中,准备好由工作单元提交。 (就我而言,Medhime 的 DbContextScope UoW 模式,由 Zejii.DbContext 适应 EF Core)我绝对会避免诸如通用存储库模式之类的东西。

© www.soinside.com 2019 - 2024. All rights reserved.