【问题标题】:Many-to-Many EF Core already being tracked - C# Discord Bot多对多 EF Core 已被跟踪 - 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) 来自这些行会的所有用户当前都在数据库中。不过很好看,绝对是我需要考虑的事情,干杯!

标签: c# mysql entity-framework entity-framework-core discord.net


【解决方案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 每次都希望将实体中的所有字段发送到数据库,而不仅仅是发送已更改的内容。这意味着对可以更改的内容和不应该更改的内容进行更多控制。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-19
    • 2020-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多