多对多 EF Core 已被跟踪 - C# Discord Bot
Posted
技术标签:
【中文标题】多对多 EF Core 已被跟踪 - C# Discord Bot【英文标题】:Many-to-Many EF Core already being tracked - C# Discord Bot 【发布时间】:2021-09-15 12:16:26 【问题描述】:所以我使用 Entity Framework Core 来构建公会(Discord 服务器的另一个名称)和用户的数据库,以及 Discord.NET 库。每个公会有很多用户,每个用户可以加入多个公会。第一次使用 EF,我遇到了一些问题。这两个类是:
public class Guild
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id get; set;
public ulong Snowflake get; set;
public DateTimeOffset CreatedAt get; set;
public string Name get; set;
public ICollection<User> Users get; set;
public class User
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id get; set;
public ulong Snowflake get; set;
public string Username get; set;
public ushort DiscriminatorValue get; set;
public string AvatarId get; set;
public ICollection<Guild> Guilds get; set;
public DateTimeOffset CreatedAt get; set;
目标是拥有 3 个表:Guild、Users 和 GuildUsers。这是我目前获取公会的功能:
using var context = new AutomataContext();
var discordGuilds = this.client.Guilds.ToList();
var dbGuilds = context.Guilds;
List<Guild> internalGuilds = discordGuilds.Select(g => new Guild
Snowflake = g.Id,
Name = g.Name,
CreatedAt = g.CreatedAt,
Users = g.Users.Select(gu => new User
Id = context.Users.AsNoTracking().FirstOrDefault(u => u.Snowflake == gu.Id)?.Id ?? default(int),
).ToList(),
).ToList();
// Upsert Guilds to db set.
foreach (var guild in internalGuilds)
var existingDbGuild = dbGuilds.AsNoTracking().FirstOrDefault(g => g.Snowflake == guild.Snowflake);
if (existingDbGuild != null)
guild.Id = existingDbGuild.Id;
dbGuilds.Update(guild); // Hits the second Update here and crashes
else
dbGuilds.Add(guild);
await context.SaveChangesAsync();
我应该注意,“雪花”是 discord 使用的唯一 ID,但我想为每个表保留自己的唯一 ID。
高级概述,公会被收集到 Discord.NET 模型中。然后将这些转换为 internalGuilds(我的公会类,其中包括用户列表)。这些中的每一个都循环并插入到数据库中。
问题出现在第二个公会循环中,在“更新”中抛出了一个错误,表明用户 ID 已经被跟踪(在公会内部)。那么嵌套 ID 已经被跟踪了?不知道这里发生了什么,任何帮助将不胜感激。谢谢。
【问题讨论】:
数据库中不存在的 Discord 公会用户应该怎么办? 好点,目前我相信它会尝试将 ID 设置为默认 int 即 0,并且会在两个单独的“不在数据库中的用户”上导致相同的异常。然而,导致此初始异常的原因并非 1) 已被跟踪的 Id 是现有用户,2) 来自这些行会的所有用户当前都在数据库中。不过很好看,绝对是我需要考虑的事情,干杯! 【参考方案1】:这个异常很可能发生,因为您在加载用户时没有跟踪然后循环并可能尝试更新或插入公会/w 相同的用户引用,尤其是使用 Update
方法。
我建议删除AsNoTracking
的使用。在读取大量数据时,通过AsNoTracking
处理分离的实体引用更多的是一种性能调整。您可以通过雪花预取所有用户引用:
using (var context = new AutomataContext())
var discordGuilds = this.client.Guilds.ToList();
// Get the user snowflakes from the guilds, and pre-fetch them.
var userSnowflakes = discordGuilds.SelectMany(g => g.Users.Select(u => u.Id)).ToList();
var users = await context.Users
.Where(x => userSnowflakes.Contains(x.Snowflake))
.ToListAsync();
// We need to add references for any New user snowflakes.
var existingSnowflakes = users.Select(x => x.Snowflake).ToList();
// If more detail is needed for new user records, it will need to be fetched from the passed in Guild.User.
var newUsers = userSnowflakes.Except(existingSnowFlakes)
.Select(x => new User SnowflakeId = x ).ToList();
if(newUsers.Any())
users.AddRange(newUsers);
List<Guild> internalGuilds = discordGuilds.Select(g => new Guild
Snowflake = g.Id,
Name = g.Name,
CreatedAt = g.CreatedAt,
Users = g.Users
.Select(gu => users.Single(u => u.Snowflake == gu.Id))
.ToList(),
).ToList(),
// Upsert Guilds to db set.
foreach (var guild in internalGuilds)
var existingGuildId = context.Guilds
.Where(x => x.Snowflake == guild.Snowflake)
.Select(x => x.Id)
.SingleOrDefault();
if (existingGuildId != 0)
guild.Id = existingGuildId;
dbGuilds.Update(guild);
else
dbGuilds.Add(guild);
await context.SaveChangesAsync();
这应该有助于确保现有用户的用户引用指向相同的实例,无论是现有用户还是首次引用时将与 DbContext 关联的新用户引用。
最终我不建议将Update
用于“Upsert”场景,因为无论如何都需要获取 Db 记录,更新已获取实例上的值或插入新值。 Update
每次都希望将实体中的所有字段发送到数据库,而不仅仅是发送已更改的内容。这意味着对可以更改的内容和不应该更改的内容进行更多控制。
【讨论】:
以上是关于多对多 EF Core 已被跟踪 - C# Discord Bot的主要内容,如果未能解决你的问题,请参考以下文章