.NET 6 EF在更新时删除DB记录



实体/模型有一个子对象,在ADD(POST)操作期间,我只想在数据库中更新父对象,我只需将子对象设置为null。父对象可以很好地添加到数据库中,子对象不会接触数据库。

然而,当我执行UPDATE(PUT)并将相同的子对象设置为null时,父对象实际上是从数据库中删除的,而子对象在数据库中没有被触摸?

型号代码:

namespace PROJ.API.Models
{
public partial class Todo
{
public Todo()
{
}
public long TdoId { get; set; }
public string TdoDescription { get; set; } = null!;
public long PtyId { get; set; }
public virtual Priority? Priority { get; set; }
}
public partial class Priority
{
public Priority()
{
}
public long PtyId { get; set; }
public byte PtyLevel { get; set; }
public string PtyDescription { get; set; } = null!;
}
}

实体代码:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace PROJ.API.Entities
{
public class Todo
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long TdoId { get; set; }
public string TdoDescription { get; set; } = null!;
public long PtyId { get; set; }
[ForeignKey("PtyId")]
public virtual Priority? Priority { get; set; }
}
public class Priority
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long PtyId { get; set; }
public byte PtyLevel { get; set; }
public string PtyDescription { get; set; } = null!;
}
}

存储库代码:

public async Task<Todo?> GetTodoAsync(long tdoId)
{
var todo = await _context.Todo.Where(c => c.TdoId == tdoId)
.Include(x => x.Priority)
.FirstOrDefaultAsync();
return todo;
}

控制器代码:

[HttpPut()] // UPDATE
public async Task<ActionResult> UpdateTodoAsync(Todo todo)
{
var eTodo = await _myRepository.GetTodoAsync(todo.TdoId);
if (todo.Priority == null || todo.Priority.PtyId == 0)
{
var priority = await _myRepository.GetPriorityAsync(todo.PtyId);
if (priority != null)
{
_mapper.Map(priority, todo.Priority);
}
}
_mapper.Map(todo, eTodo);
await _myRepository.SaveChangesAsync();
return NoContent();
}

我的理解是,将子对象设置为null会告诉EF不要在数据库中对其执行任何操作。TODO.PtyId是用SQL数据库中的FK到PRIORITY.PtyId设置的,但我没有在上下文(OnModelCreating)中定义它;思考;我需要Fluent API方法。

有没有想过我做错了什么和/或为什么当我将子对象设置为NULL时,UPDATE实际上删除了一条记录?正如我之前提到的,使用相同null方法的ADD工作得很好。

有几件事。

在您的示例和命名约定中,您应该通过属性或fluent声明明确指定FK。EF的惯例是将FK名称建立在";类型";关系的名称,而不是属性名称。例如,如果你有:

public virtual Priority? Pty { get; set; }

EF将查找名为Priority_ID或PriorityID的FK,而不是PtyID。EF Core的这种行为可能已经改变了,老实说,我还没有深入研究EF惯例是否可以被信任来解决这个问题。

最后,这是一个典型的问题示例,当您将关注点与实体混合并使用分离的实体作为视图模型时,就会出现这些问题。它还概述了存储库实现中的一个问题。在您的情况下,您正在分离一个实体,然后在将其传递回服务器进行更新时,从数据状态加载该实体,并使用Automapper跨其复制值。

第一个问题是,当至少在本例中您不需要或不想要相关实体时,您的存储库会自动无条件地渴望加载该相关实体。当EF急切地加载一个关系,然后将该相关实体设置为#null时,代理会记录此操作,EF会将其解释为";删除此关系";。如果相关实体未加载/关联,并且在保存该顶级实体时保留为#null,则不会删除任何内容。无论哪种方式,如果您不想保存对相关实体的更改,都需要避免将其设置为#null。解决方案是首先不加载相关实体,忽略相关实体之间的映射,或者将这些实体标记为"未更改"以避免对其进行持久更改。

未加载相关实体:这可以通过添加参数来指示应该热切加载的内容,或者考虑对存储库方法采用IQueryable来解决:

自变量:

public async Task<Todo?> GetTodoAsync(long tdoId, bool includeRelated = true)
{
var query = _context.Todo.Where(c => c.TdoId == tdoId);
if (includeRelated)
{
query = query.Include(c => c.Pty);
}

return query.FirstOrDefaultAsync();
}

在简单的情况下,这还不算太糟,但在更复杂的实体中,这可能是一种痛苦,尤其是如果你想选择性地包括亲属。这样,当您从数据状态加载eToDo时,您可以告诉它不要急于加载优先级。这并不是万无一失的,因为如果DbContext实例之前加载了与该Todo关联的Priority,那么仍然有可能关联Priority。被跟踪的实体将被关联,即使您没有明确地急于加载它们。为了安全起见,这应该与下面的自动映射器更改结合起来。(不包括映射更改)这仍然是一个有价值的更改,因为您可以避免无条件地急于加载每次读取的资源/性能成本。

可查询:

public IQueryable<Todo> GetTodoById(long tdoId)
{
var query = _context.Todo.Where(c => c.TdoId == tdoId);
return query;
}

IQueryable为您的消费者提供了更大的灵活性来处理将要返回的数据,但它确实需要围绕工作单元模式进行思考,将DbContext的范围移到工作单元中,以便消费者负责该范围,而不是在单个存储库级别。这种方法的优点是,如果需要,可以在存储库之间共享工作单元(DbContext范围),并且使用这种模式,您的消费者可以控制以下内容:

  • 使用SelectCountAny等进行投影
  • async与同步操作
  • 评估是否急于加载相关实体

因此,以这种模式为例,控制器或服务代码的功能更像:

[HttpPut()] // UPDATE
public async Task<ActionResult> UpdateTodoAsync(Todo todo)
{
using (var contextScope = _contextScopeFactory.Create())
{
var eTodo = await _myRepository.GetTodoById(todo.TdoId)
.SingleAsync();
_mapper.Map(todo, eTodo);
await contextScope.SaveChangesAsync();
return NoContent();
}
}

contextScope/\ucontextScopeFactory是Medhi El Gueddari为EF6创建的名为DbContextScope的UoW模式,该模式具有覆盖EF Core的多个分叉。我喜欢这种模式,因为它为Repository提供了一个对定位器的依赖,以从Scope解析DbContext,而不是传递DbContext实例,从而使该Scope完全控制SaveChanges()是否被提交。利用IQueryable可以实现投影,因此当使用Automapper的ProjectTo读取要发送到视图的数据以将其投影到ViewModel时,它可以帮助避免这一问题,而不是将实体发送到视图,后者作为反序列化的、通常不完整的外壳返回控制器。

不包括映射更改:

这涉及到调整映射,当将一个Todo映射到另一个时,您可以使用该映射来排除对相关实体的更改之间的复制。如果映射忽略Todo.Pty,那么您可以仅在Todo字段之间从一个实例映射到DB实例,并保存DbInstance,而不必跟踪更改,从而触发Pty或关系中的任何更改。

标记为未更改:

假设您的存储库正在管理DbContext的范围,那么您可能需要添加一个方法来隔离对该顶级实体的更改。由于Repository正在确定DbContext的范围,这意味着某种形式的笨拙方法,因为我们需要传递实体来调整跟踪。

// eTodo.Pty = null; don't do
_myRepository.IgnoreRelatedChanges(eTodo);
await _myRepository.SaveChangesAsync();

然后。。。

public void IgnoreRelatedChanges(Todo todo)
{
_context.Entry(todo.Pty).State = EntityState.Unchanged;
}

这种方法的问题在于它很笨拙,而且容易出现错误/异常。

在任何情况下,这都应该为您提供一些选项来考虑解决您的问题,并可能考虑更新您的存储库模式。

最新更新