【问题标题】:Can't update principal entity in one-to-one relationship when dependent entity is set in the same call在同一调用中设置依赖实体时,无法以一对一关系更新主体实体
【发布时间】:2021-02-06 06:47:54
【问题描述】:

上下文

我有一个项目,我正在使用实体框架核心。该项目将拥有可以注册的用户,当然,之后可以登录。他们还可以AFTERWARDS编辑他们的个人资料,这就是问题所在。当尝试编辑用户设置(数据,他在注册时不输入,登录)时,他还可以设置一个地址,这是用户的依赖实体。当尝试添加地址时,它也失败了

乐观并发失败,对象已被修改

流程

首先,用户只需提供电子邮件地址和密码即可注册。一封电子邮件被发送给他以确认他的帐户。当他这样做时,他能够登录。登录时,他检索一个 JWT,他可以使用它来执行他的请求。目前唯一的其他控制器方法是编辑他的个人资料设置,他可以在其中添加数据,例如

  • 名字(字符串)
  • 姓氏(字符串)
  • 电话号码(字符串)
  • 地址(地址)
    • 街道
    • 户型
    • 城市
    • 邮政编码

问题及相关代码

在详细介绍之前,我想向您展示我是如何处理这种关系的

用户.cs

public sealed class User : IdentityUser<Guid>
{
    // Other fields...
    [ProtectedPersonalData]
    public Address Address { get; set; }
}

地址.cs

public sealed class Address
{
    // Other fields...

    public Guid AddressId { get; set; }
    public Guid UserId { get; set; }
    public User User { get; set; }

    public Address()
    {
        AddressId = Guid.NewGuid();
    }
}

DbContext.cs

public sealed class DbContext : IdentityDbContext<User, Role, Guid>
{
    public DbSet<User> Users { get; set; }
    public DbSet<Address> Addresses { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseMySql(
            "server=localhost;database=db;user=root;password=example"
        );
    }
    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<User>()
            .HasOne(u => u.Address)
            .WithOne(a => a.User)
            .HasForeignKey<Address>(a => a.UserId);
        base.OnModelCreating(builder);
    }
}

当我现在发出 PUT 请求以编辑配置文件时,我正在发送此数据

{
  "firstname": "Austin",
  "lastname": "Powers",
  "Address": {
    "Street": "Mr. Evil Way",
    "HouseNumber": "20",
    "ZipCode": "10115"
  },
  "PhoneNumber": "+492148 48484"
}

我的控制器,看起来像这样

public async Task<ActionResult<User>> EditProfile(EditProfileResource editProfileResource)
{
    User user = await _userManager.GetUserAsync(User);
    // _mapper is an automapper instance
    user = _mapper.Map(editProfileResource, user);
    user.ProfileSetup = true;
    IdentityResult result = await _userManager.UpdateAsync(user);
    if (result.Succeeded)
    {
        return NoContent();
    }

    return Problem(result.Errors.First().Description, null, StatusCodes.Status400BadRequest);
}

那么接下来会发生什么。

  1. 控制器自动将请求正文转换为EditProfileResource,它看起来像这样并有一个地址资源
public sealed class EditProfileResource
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public AddressResource Address { get; set; }
    public string PhoneNumber { get; set; }
}

// Separate file of course
public sealed class AddressResource
{
    public string Street { get; set; }
    public string HouseNumber { get; set; }
    public string ZipCode { get; set; }
    public string City { get; set; }
}

所以我的editProfileResource 拥有所有正确的数据集!问题?当我现在将用户保存在内容为IdentityResult result = await _userManager.UpdateAsync(user); 的行中时,我收到了错误Optimistic concurrency failure, object has been modified。当我从EditProfileResource 中删除AddressResource 时,它可以正常工作,但我也想编辑地址。

但是为了正确理解,我将展示代码是如何实现的以及其中的内容,这可能会导致错误,但我不知道修复会是什么

User user = await _userManager.GetUserAsync(User);
-> 在这里,我们有用户对象,存储在数据库中,没有名字,没有姓氏,没有地址

user = _mapper.Map(editProfileResource, user);
-> 这现在改变了用户对象,所以它现在有一个名字、姓氏,甚至一个地址对象。地址对象还具有具有正确 guid 和用户对象的用户对象。所以一切都是正确的。

IdentityResult result = await _userManager.UpdateAsync(user);
-> 这样会报错,可能是因为User中的Address也有User对象吧?

经过测试的解决方案

所以我接下来要做的是尝试,改变储蓄机制。我没有做IdentityResult result = await _userManager.UpdateAsync(user);,而是做了

await using (DbContext context = new DbContext())
{
    context.Addresses.Add(user.Address);
    await context.SaveChangesAsync();
}

但是,由于外键约束,这失败了?

无法添加或更新子行:外键约束失败(db.Addresses, CONSTRAINT FK_Addresses_AspNetUsers_UserId FOREIGN KEY (UserId) REFERENCES AspNetUsers (Id) ON DELETE CASCADE)

所以我在没有context.Addresses.Add(user.Address);的情况下尝试了它

这可行,但由于我没有将它添加到上下文中,因此数据库中没有地址。我究竟做错了什么? :/

什么是正确的方法,同时编辑两个实体并将它们都保存到数据库中?

更新

更新 1

我尝试将其拆分为两种方法,以确保它与尝试一次更新多个数据集无关

[HttpPut("userdata")]
public async Task<ActionResult<User>> EditProfile(EditProfileResource editProfileResource)
{
    User user = await _userManager.GetUserAsync(User);
    _mapper.Map(editProfileResource, user);
    user.ProfileSetup = user.ProfileSetup ? user.ProfileSetup : user.Address != null;

    IdentityResult result = await _userManager.UpdateAsync(user);
    if (result.Succeeded)
    {
         return NoContent();
    }

    return Problem(result.Errors.First().Description, null, StatusCodes.Status400BadRequest);
}

[HttpPut("address")]
public async Task<ActionResult> EditAddress(AddressResource addressResource)
{
    User user = await _userManager.GetUserAsync(User);
    Address address = _mapper.Map<AddressResource, Address>(addressResource);
    address.User = user;
    address.UserId = user.Id;

    await using (DbContext context = new DbContext())
    {
        context.Addresses.Add(address);
        await context.SaveChangesAsync();
    }

    return NoContent();
}

再一次,编辑用户是可行的,但是当我想保存地址时,由于已经存在密钥,它会失败。怎么可能?

【问题讨论】:

  • 我会说这是 db 类设计中的一个缺陷。该地址不应与其他类别的用户分开。如何为多个用户重复使用一个地址?
  • 不止一个人可以住在同一个地址。地址应该在另一个表中。
  • 补充一下 Mich 所说的:即使我们不考虑地址,每个关系都会发生此错误。地址只是其中之一。例如,如果我有一个照片,这是一个不同的类,因为一个用户可以有一张照片,另一个实体可以有一张照片,而且我不希望每个表都有 3 列照片,那么同样的问题会发生
  • 您能否确认您是否配置了表用户或地址的任何触发器?
  • @UmairZafar 我可以确认我没有使用任何触发器

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


【解决方案1】:

好的,看来问题与组合使用 UserManagerDbContext 的组合有关。

解决方案如下所示

public async Task<ActionResult<User>> EditProfile(ProfileSetupResource editProfileResource)
{
    Guid userId = Guid.Parse(_userManager.GetUserId(User));

    await using (DbContext context = new DbContext())
    {
        User user = await context.Users.FindAsync(userId);
        
        if (user.ProfileSetup)
        {
            return BadRequest("Profile already set up.");
        }

        user = _mapper.Map(editProfileResource, user);
        user.ProfileSetup = true;

        context.Users.Update(user);
        await context.Addresses.AddAsync(user.Address);
        await context.SaveChangesAsync();
    }
    return NoContent();
}

问题是,在前面的示例中,DbContext 注意到用户已更改,但是,更改不是由 DbContext 管理,而是由 UserManager 管理,因为这是代码

// User changes managed by UserManager
User user = await _userManager.GetUserAsync(User);
user = _mapper.Map(editProfileResource, user);

// [other code...]

await using (DbContext context = new DbContext())
{
    // Address changes managed by DbContext
    context.Addresses.Add(address);
    await context.SaveChangesAsync();
}

所以基本上,就像我说的,解决方案是让DbContext 管理一切。 UserManager 应该只被使用,如果只有用户将被改变,而不是其他数据。由于用户管理器也在幕后使用DbContext,这是一场“战争”,其中一方获胜,双方都有变化,而另一方DbContext 没有。所以是的。这就是我的解决方案。

【讨论】:

    猜你喜欢
    • 2019-11-11
    • 2023-03-07
    • 2017-10-14
    • 2020-05-31
    • 1970-01-01
    • 2019-01-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多