【问题标题】:Update is not working in OwnsMany Value Object relationship EF Core 5更新在 OwnsMany 值对象关系 EF Core 5 中不起作用
【发布时间】:2021-11-29 16:13:08
【问题描述】:

我有以下课程(为简单起见减少了)。

public class User : Entity
{
    public List<Skill> Skills { get; set; }
}

public class Skill : ValueObject
{
    public string Name { get; private set; }
}

我的UserConfiguration 类如下:

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder
            .OwnsMany(user => user.Skills);
    }
}

这里我说 EF 我的 User 类拥有一些 Skills 并生成下表:

我使用工作单元模式,因此在我的存储库中,我对 Update 方法执行以下操作:

public virtual void Update(T entityToUpdate)
{
    _context.Entry(entityToUpdate).State = EntityState.Modified;
}

更新后我调用我的Commit 方法,它会保存更改:

public void Commit()
{
    _dbContext.SaveChanges();
}

据我所知,在获取User 实体时,我正确接收了所有属性(包括Skills),但在更新时更新的是除Skills 之外的每个属性。

我尝试手动设置Skill's表的主键和外键以这种方式更改UserConfiguration类(结果相同,技能未更新):

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder
            .OwnsMany(user => user.Skills, skill =>
            {
                skill.WithOwner().HasForeignKey("UserId");
                skill.Property<int>("Id");
                skill.HasKey("id");
            });
    }
}

我还尝试将我的Update 方法更改为:

public virtual void Update(T entityToUpdate)
    {
        _dbSet.Update(entityToUpdate);
    }

但我收到以下异常:Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException : Database operation expected to affect 1 row(s) but actually affected 0 row(s).

注意 Skill 是一个 ValueObject(它有一个影子 ID)。我实际上认为应该有问题。

非常感谢您的帮助。

编辑:由于@pejman 的回答对我不起作用,我将把我的 UnitOfWork、Repository 和 Context 放在这里(为简单起见减少了),因为这是我唯一没有做过的事情,例如 @ pejman,也许问题就在那里,但我看不到。

Repository:

class BaseRepository<T> : IRepository<T> where T : Entity
{
    private readonly Context _context;
    private readonly DbSet<T> _dbSet;

    public BaseRepository(Context context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public virtual T GetById(Guid id) => _dbSet.AsNoTracking().FirstOrDefault(x => x.Id == id);

    public virtual void Update(T entityToUpdate)
    {
        _context.Entry(entityToUpdate).State = EntityState.Modified;
    }
}

Unit of work:

public class UnitOfWork : IUnitOfWork
{
    private const int SqlObjectAlreadyExistsErrorNumber = 2627;
    private readonly Context _dbContext;
    private BaseRepository<User> _users;

    public UnitOfWork(Context dbContext)
    {
        _dbContext = dbContext;
    }

    public IRepository<User> Users
    {
        get
        {
            return _users ??= new BaseRepository<User>(_dbContext);
        }
    }

    public void Commit()
    {
        _dbContext.SaveChanges();
    }
}

Context:

public class Context : DbContext
{
    public Context(DbContextOptions<DeribeContext> options)
        : base(options)
    {
    }

    public DbSet<User> Users { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(DeribeContext).Assembly);
        base.OnModelCreating(modelBuilder);
    }
}

【问题讨论】:

  • 您是否尝试过在不附加实体的情况下进行更新?
  • @SvyatoslavDanyliv 是的,我试过了,我得到了这个例外:Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException : Database operation expected to affect 1 row(s) but actually affected 0 row(s). 我将用它更新问题。
  • 您还应该提供您在代码中提到的其他类。我们无法根据假设回答。
  • @GertArnold 你到底是指哪些类?感谢您的帮助。
  • 你的类是从其他类派生的。

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


【解决方案1】:

更新断开实体的集合导航属性的支持非常有限。首先,它们不被视为实体数据的一部分。其次,对于在断开连接的实体中没有找到的现有子实体应该如何处理还不是很清楚。

所有这一切都是因为 EF Core 没有关于原始数据库状态的信息,因为它们主要支持(也是唯一可靠的)加载流(在数据库上下文更改跟踪器中)、修改、保存。

理论上,拥有的实体类型的集合应该比常规实体的集合更容易处理。由于它们被认为是所有者的一部分(至少在加载时),因此它们可能在更新期间被这样处理,即先删除所有退出的然后插入(添加)所有传入的替换。

实际上(当前的 EF Core)在断开连接的情况下更新它们几乎是不可能的。它们不仅不会自动处理,而且由于您无法定义拥有的实体类型的 DbSet(因此使用 Set&lt;T&gt;() 方法),因此没有其他方法可以告诉 EF Core 通过实际从数据库中加载它们来删除它们并清除集合。这与显式/延迟加载不起作用的事实一起,留下了从数据库加载所有者实体的唯一选择。

一旦你这样做了,那么最好也应用修改后的属性(使用CurrentValues.SetValues 方法),而不是使用使用Update 方法时发生的所有“强制更新”(或将State 设置为EntityState.Modified)。

将它应用到您的示例模型将是这样的

var dbUser = _context.Set<User>().FirstOrDefault(e => e.Id == user.Id);
_context.Entry(dbUser).CurrentValues.SetValues(user);
dbUser.Skills = user.Skills;

由于唯一的自定义代码是 dbUser.Skills = user.Skills;,因此可以使用 EF Core 提供的元数据和更改跟踪 API 将其推广到任何拥有实体类型的集合:


public virtual void Update(T entityToUpdate)
{
    // Load existing entity from database into change tracker
    var entity = _dbSet.AsTracking()
        .FirstOrDefault(x => x.Id == entityToUpdate.Id);
    if (entity is null)
        throw new KeyNotFoundException();
    var entry = _context.Entry(entity);
    // Copy (replace) primitive properties
    entry.CurrentValues.SetValues(entityToUpdate);
    // Copy (replace) owned collections
    var ownedCollections = entry.Collections
        .Where(c => c.Metadata.TargetEntityType.IsOwned());
    foreach (var collection in ownedCollections)
    {
        collection.CurrentValue = (IEnumerable)collection.Metadata
            .GetGetter().GetClrValue(entityToUpdate);
    }
}

【讨论】:

  • 感谢 Ivan,它的工作原理就像一个魅力 :)
【解决方案2】:

试试这个,它工作正常:

 public class User : Entity
{
    private List<Skill> _skills;
    public IReadOnlyList<Skill> Skills => _skills.AsReadOnly();// use this way 
                                                                 //for more encapsulation

    public void AssignSkills(List<Skill> skills) 
    {
        this._skills = skills;
    }
}

Skill 类:

public class Skill
{
    public Skill(string name, string desc)
    {
        this.Name = name;
        Description = desc;
    }

    protected Skill() { }
    public string Name { get; private set; }
    public string Description { get; private set; }
}
 
public class Entity
{
    public int Id { get; set; }
}

对于Mapping

 public class UserMapping : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.Property(a => a.Id).ValueGeneratedNever();
        builder.OwnsMany(a => a.Skills, map =>
        {
            map.ToTable("Skills").HasKey("Id");
            map.Property<int>("Id");
            map.WithOwner().HasForeignKey("UserId");
            map.UsePropertyAccessMode(PropertyAccessMode.Field);
        });
    }
}

MyContext:

public class MyDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=users;Integrated Security=true");
        base.OnConfiguring(optionsBuilder);
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(UserMapping).Assembly);
        base.OnModelCreating(modelBuilder);
    }
}

最后,我尝试向用户添加技能列表,然后更新列表,它工作正常:

 static async Task Main(string[] args)
    {
        using (var dbContext = new MyDbContext())
        {
            var user = new User();
            var skills = new List<Skill>()
            {
                new Skill("first skill","desc1"),
                new Skill("sec skill","desc2"),
            };
            user.AssignSkills(skills);

            dbContext.Users.Add(user);
            dbContext.SaveChanges();
        }

        using (var readContext = new MyDbContext())
        {
            var user = readContext.Users.First();
            var skills = new List<Skill>()
            {
                new Skill("first skill","desc1"),
                new Skill("sec skill","desc_change"),
                new Skill("third skill","desc_New"),
            };
            user.AssignSkills(skills);
            await readContext.SaveChangesAsync();
        }

【讨论】:

  • 我尝试做同样的事情,但使用 DbContext 的方式仍然无法正常工作。也许问题就在那里。我将使用我的 UnitOfWork + Repository 编辑问题。
  • @QuicoLlinaresLlorens 为什么在您的基础仓库中使用 .AsNoTracking()?这导致ef 不再跟踪你的对象
  • 我认为这会在获取对象时停止跟踪它,但后来我在更新时跟踪它。我这样做是因为我说的更新时无法跟踪。
  • 如果我删除了 AsNoTracking,我将无法进行更新,也无法更新任何属性。
猜你喜欢
  • 1970-01-01
  • 2021-07-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-30
  • 1970-01-01
  • 2015-09-17
  • 2021-06-27
相关资源
最近更新 更多