【问题标题】:HasOne relationship cannot be set to null inside ForEach不能在 ForEach 中将 HasOne 关系设置为 null
【发布时间】:2022-12-01 16:53:48
【问题描述】:

假设有一个模型有条目。每个条目都有一种货币。我想像这样更新货币:

var model = await _dbContext.Models
    .Include(x => x.Entries) 
    .FirstOrDefaultAsync(x => x.Id.ToString().Equals(command.Id), cancellationToken);

model.Name = command.Name;

command.Entries.ForEach(x =>
{
    var entry = _dbContext.Entries
        .Find(Guid.Parse(x.Id));

    var currency = _dbContext.Currencies
        .FirstOrDefault(y => y.Id.Equals(x.CurrencyId));

    entry.Currency = currency;
});

await _dbContext.SaveChangesAsync(cancellationToken);

以下是货币关系的定义方式:

modelBuilder.Entity<Entry>()
    .HasOne<Currency>(x => x.Currency)
    .WithMany();

我正在调试可以为货币分配一个值但不能为空的问题。例如。

command.Entries.ForEach(x =>
{
    var entry = _dbContext.Entries
        .Find(Guid.Parse(x.Id));

    entry.Currency = null;
});

不会将货币设置为 null 但这有效:

command.Entries.ForEach(x =>
{
    var entry = _dbContext.Entries
        .Find(Guid.Parse(x.Id));

    var currency = ...

    entry.Currency = currency;
});

货币已正确更新。现在,当我在 ForEach 之外进行更新时,它会起作用:

var entry = _dbContext.Entries.Find(Guid.Parse(".."));

entry.Currency = null;

await _dbContext.SaveChangesAsync(cancellationToken);

所以 ForEach 似乎是问题所在。任何想法为什么?奇怪的是,货币可以更新为一个值,但不能在 ForEach 内部设置为 null。

【问题讨论】:

  • 旁注:这是一种非常低效的更新条目的方法。您通过数据库中的单个查询获取每个条目。最好一次全部取回。货币也一样。
  • ForEach 不是 LINQ 命令,也没有转换为 SQL。这是一个 List<T> 方法,除了foreach(var item in thatList) 会做的lambda 是一个单独的范围。 entry 在 lambda 之外不存在
  • 代码是非常慢,因为它一个一个地加载实体。您可以将 ForEach 替换为 _dbContext.Entries.Where(e=&gt;listOfIds.Contains(e.Id)) 以生成 ID in (@id1, @id2, @id3,....) 子句。如果您使用Inlude(e=&gt;e.Currency),则无需单独加载Currency。即使是现在,您也可以显式加载它

标签: c# entity-framework-core


【解决方案1】:

ForEach 不是 LINQ 命令,也没有转换为 SQL。这是一个 List 方法,它所做的只是 foreach(var item in thatList) 会做的事情。真正的区别在于 lambda 是一个独立于代码其余部分的作用域。 entry 是那里的孤儿。 _dbContext 本身就是一个捕获变量。

另一个问题是代码非常慢,因为它一个一个地加载实体。 ForEach 应替换为 _dbContext.Entries.Where(e=&gt;listOfIds.Contains(e.Id))。这将在 SQL 中生成 ID in (@id1, @id2, @id3,....) 子句。

问题的代码可以替换为:

// Set up the parameters
var commandId=Guid.Parse(command.Id);
var entryIds=command.Entries.Select(c=>Guid.Parse(c.Id)).ToList();

//Make the changes

var model = await _dbContext.Models
    .FirstOrDefaultAsync(x => x.Id == commandId, cancellationToken);

model.Name = command.Name;


var entries=_dbContext.Entries.Where(e=>entryIds.Contains(e.Id));

foreach(var entry in entries)
{
    entry.Currency = null;
};

//Save the changes
await _dbContext.SaveChangesAsync(cancellationToken);

x =&gt; x.Id.ToString().Equals(command.Id) 被替换为 x.Id == commandId,因为转换为字符串会阻止服务器使用任何涵盖 Id 的索引。索引是根据列的值创建的,不能在过滤像cast(id as varchar(16))这样的函数结果时使用

即使该代码也可以替换为:

var model = await _dbContext.Models.Find(commandId);

英孚核心 7

在 EF Core 7 中,Bulk updates 可用于生成最终的 UPDATE 语句,而无需首先加载对象。问题的操作可能只是 SQL 中的两个 UPDATE 语句:

UPDATE Models
SET Name=@name
WHERE ID=@commandId;

UPDATE Entries
SET Currency=NULL
Where ID in (@id1, @id2,....);

在 EF Core 7 中,这可以使用 ExecuteUpdateAsync 方法完成:

await  _dbContext.Models
    .Where(m => x.Id == commandId)
    .ExecuteUpdateAsync(s => s
        .SetProperty(b => b.Name, b => name));

await _dbContext.Entries
    .Where(e=>entryIds.Contains(e.Id))
    .ExecuteUpdateAsync(s => s
        .SetProperty(b => b.Currency, b => null));

缺点是 ExecuteUpdate 立即执行,如果需要回滚多个更改,则需要显式事务

【讨论】:

  • 关于匿名方法的范围或 entry 被孤立,我没有关注你。闭包范围将包括任何引用的标识符,它们将正常运行。因此,如果您将保存放在 OP 的原始 .ForEach(...); 之后,它就可以正常工作,而且我相信您也说过很多,显然 foreach 循环是更好的风格。
  • 感谢您指出性能问题和可以进行的改进。非常好!但是我仍然不确定为什么不能在循环(ForEach 和 foreach)内将货币设置为 null,甚至不确定如何调试它。
  • @AluanHaddad 这不是风格问题。 will work just fine 恰恰是因为它是在声明变量的同一范围内的单个操作。它还破坏了事务逻辑,因为现在每个修改都是通过单独的命令、单独的连接立即执行的。
猜你喜欢
  • 1970-01-01
  • 2020-05-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-07
  • 1970-01-01
  • 2010-09-13
  • 1970-01-01
相关资源
最近更新 更多