多对多 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的主要内容,如果未能解决你的问题,请参考以下文章

渴望加载多对多 - EF Core [关闭]

EF Core 多对多关系

EF Core 多对多关系上的 LINQ 查询

定义引用同一个表的多对多关系(EF7/core)

EF Core 多对多配置

EF Core多对多查询加入3个表