Тип сущности невозможно отследить, поскольку при добавлении нового объекта уже отслеживается другой экземпляр с тем же значением ключа

Соответствующие модели выглядят следующим образом:

Контрольный опрос:

public class Quiz
{
    // Primary key
    public Guid Id { get; set; }
    public string Title { get; set; }
    
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public QuizTypes QuizType { get; set; }
    
    public int TimeLimit { get; set; }
    public bool AverageTimeForQuestions { get; set; }
    
    // One-to-many relationship
    public List<Question> Questions { get; set; }
}

Журнал Викторины:

public class QuizLog
{
    public Guid Id { get; set; }
    public string? Title { get; set; }

    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public QuizTypes QuizType { get; set; }
    
    public int TimeLimit { get; set; }
    public List<QuestionLog>? Questions { get; set; } = new List<QuestionLog>();
}

Пользователь:

[Table("Us3r_Data")]
public partial class User
{
    public User() { }

    public User(string username, string password, Role role, IConfiguration config)
    {
        Id = Guid.NewGuid();
        this.CreationDate = DateTime.Now;
        this.Username = username;
        this.Password = PasswordHasher.HashPassword(password + config["Password:Seed"]);
        this.Role = role;
    }

    public Guid Id { get; set; }
    public Role Role { get; set; }
    public string Username { get; set; }
    public string FullName { get; set; }

    [JsonIgnore]
    public string Password { get; set; }
    
    public bool FinalTest { get; set; }
    
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public QuizTypes QuizType { get; set; }

    public DateTime? CreationDate { get; set; }

    [NotMapped]
    public bool IsAdmin { get; set; }

    [JsonIgnore]
    public List<UserLogs>? UserLog { get; set; }
}

Логи пользователя:

public class UserLogs
{
    public Guid Id { get; set; }

    public Guid UserId { get; set; }
    public User User { get; set; } = null!;
    public QuizLog QuizSnapshot { get; set; }
    public Quiz Quiz { get; set; }
    public DateTime SubmitDate { get; set; }
}

Тип Викторины:

public class QuizTypes
{
    public Guid Id [ get; set; }
    public string Name { get; set; }
}

Конечная точка выглядит следующим образом: выделена строка, вызывающая ошибку:

  [HttpPut("quizAnswer/{id}")]
        public async Task<IActionResult> QuizAnswer(Guid id, [FromBody] QuizLog quiz)
        {
            try
            {

                var localDb = _db;
                Guid userId = JwtAuthorization.GetUserId(Request.Headers.Authorization!, _conf);

                /*var user = await _db.Users.Include(j => j.UserLog).AsNoTracking().Include(qt => qt.QuizType).FirstOrDefaultAsync(i => i.Id == userId);*/
                var user = await localDb.Users.FindAsync(userId);
                /*var dbQuiz = await _db.Quiz.AsNoTracking().Include(qt => qt.QuizType).FirstOrDefaultAsync(i => i.Id == id);*/
                var dbQuiz = await localDb.Quiz.FindAsync(id);
                var tempQuiz = quiz;

                if (user == null || dbQuiz == null)
                {
                    await Console.Out.WriteLineAsync("no user or quiz found");
                    return NotFound();
                }
                if (!user.FinalTest)
                {
                    return Ok();
                }

                tempQuiz.Id = Guid.NewGuid();
                foreach (var item in tempQuiz.Questions!)
                {
                    item.Id = Guid.NewGuid();
                    foreach (var answer in item.Answers!)
                    {
                        answer.Id = Guid.NewGuid();
                    }
                }



                UserLogs log = new UserLogs();
                log.Id = Guid.NewGuid();
                log.QuizSnapshot = tempQuiz;
                log.Quiz = dbQuiz;
                log.User = user;
                log.SubmitDate = DateTime.UtcNow;
                user.FinalTest = false;



                await _db.UserLogs.AddAsync(log);
                await _db.SaveChangesAsync();
                return Ok();
            }
            catch (Exception ex)
            {
                await Console.Out.WriteLineAsync("\n" + ex.ToString() + "\n");
                return BadRequest("something went wrong");
            }
        }

Мой DbContext:

public class QuizDatabase : DbContext
{
    public QuizDatabase(DbContextOptions options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<Quiz>().HasMany(i => i.Questions).WithOne().OnDelete(DeleteBehavior.Restrict);
        modelBuilder.Entity<Question>().HasMany(i => i.Answers).WithOne().OnDelete(DeleteBehavior.Restrict);
        modelBuilder.Entity<User>().HasMany(i => i.UserLog).WithOne(j => j.User).HasForeignKey(l => l.UserId).IsRequired();
        modelBuilder.Entity<QuizLog>().HasMany(i => i.Questions).WithOne().OnDelete(DeleteBehavior.Restrict);
        modelBuilder.Entity<QuestionLog>().HasMany(i => i.Answers).WithOne().OnDelete(DeleteBehavior.Restrict);
        modelBuilder.Entity<Quiz>().HasOne(i => i.QuizType).WithMany();
        modelBuilder.Entity<QuizLog>().HasOne(i => i.QuizType).WithMany();
        modelBuilder.Entity<User>().HasOne(q => q.QuizType).WithMany();
    }

    public DbSet<User> Users { get; set; }
    public DbSet<UserLogs> UserLogs { get; set; }
    public DbSet<Quiz> Quiz { get; set; }
    public DbSet<Question> Questions { get; set; }
    public DbSet<Answer> Answers { get; set; }
    public DbSet<QuizTypes> QuizTypes { get; set; }
    public DbSet<AnswerLog> AnswerLogs { get; set; }
    public DbSet<QuestionLog> QuestionLogs { get; set; }
    public DbSet<QuizLog> QuizLogs { get; set; }
}

Полное исключение выглядит примерно так:

System.InvalidOperationException: The instance of entity type 'QuizTypes' cannot be tracked because another instance with the key value '{Id: [guid]aaa}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey, Nullable`1 fallbackState)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey)
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.SetEntityState(InternalEntityEntry entry, EntityState entityState)
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Add(TEntity entity)
   at SOP_QUIZ.Controllers.QuizController.QuizAnswer(Guid id, QuizLog quiz) in [...] /Controllers/QuizController.cs:line 308

Всего есть 3 QuizTypes для UserLog:

  • Quiz -> aaa
  • QuizSnapshot -> aaa
  • User -> bbb (разные)

Что я пробовал

  1. AsNoTracking():
    Я попробовал использовать .AsNoTracking() для объектов, которые извлекаю из базы данных, с помощью этих строк:

    var user = await _db.Users.Include(j => j.UserLog).Include(qt => qt.QuizType).AsNoTracking().FirstOrDefaultAsync(i => i.Id == userId);
    var dbQuiz = await _db.Quiz.Include(qt => qt.QuizType).AsNoTracking().FirstOrDefaultAsync(i => i.Id == id);
    

    Я предполагал, что это приведет к удалению отслеживания, но понял, что вызов Add() в конце снова начинает отслеживание, что привело к той же проблеме.

  2. Аннотации внешнего ключа:
    Я пробовал использовать аннотации внешнего ключа в User, Quiz и QuizLog, добавляя новое свойство, например public Guid QuizTypeId { get; set; }, но это не решило проблему.

  3. Прикрепление существующих объектов:
    Я попробовал прикрепить существующие объекты QuizTypes перед сохранением, надеясь, что это обеспечит использование только одного экземпляра для всех трех объектов, но это тоже не помогло.

  4. Очистка трекера изменений:
    Я позвонил _db.ChangeTracker.Clear() прямо перед тем, как вызвать _db.UserLogs.Add(log);, и убедился, что до прояснения никаких объектов не отслеживалось. Однако это также не решило проблему.

Любая помощь приветствуется.

Кажется, что вы делаете жизнь намного сложнее, чем она должна быть. С помощью EF вы извлекаете объект из БД, изменяете его свойства и вызываете сохранение в контексте. Ничего из этого возиться с Entity, а также отсоединением/присоединением, возиться с состоянием объекта и т. д. Кстати, вы можете использовать FindAsync вместо этого «посмотреть локально, а затем получить из базы данных, если он не локальный».

flackoverstow 27.08.2024 13:24

Не храните контексты в течение длительного времени, не сериализуйте объекты БД в пользовательский интерфейс, а затем (когда вы вернете их обратно измененными пользователем) попытайтесь повторно восстановить объекты БД, прикрепить их к средству отслеживания изменений и убедить EF, что это объект, который он должен использовать, чтобы что-то сделать с БД. Создайте разные классы моделей для вашего пользовательского интерфейса, попросите EF выполнить поиск в БД, скопируйте данные из объекта пользовательского интерфейса в объект БД, который вы получаете от EF (возможно, с использованием автоматического сопоставителя), и сохраните .. Чем больше вы пытаетесь контролировать EF, тем сложнее становится ваша жизнь

flackoverstow 27.08.2024 13:30

Код, который я отправил, был последней попыткой, но я, конечно, начал с самого простого плана, с вызовами базы данных, проверяя, что они не равны нулю, а затем добавляя новый объект. Я вернулся к этому, заменил вызовы базы данных на findAsync, но он выдает «Дубликат записи «GUID» для ключа «PRIMARY». Позвольте мне обновить свой код в моем сообщении, чтобы отразить мои изменения.

Quan 27.08.2024 14:15

Если var p = await context.People.FindAsync(123); p.Name = "John Smith"; await context.SaveChangesAsync(); выдает дублирующее исключение PK, ваши проблемы в другом месте.

flackoverstow 27.08.2024 14:23

Не используйте AsNoTracking(), если обновляете enitites.

Svyatoslav Danyliv 27.08.2024 14:29

Добавьте свойства внешнего ключа QuizTypeId помимо QuizType и установите их вместо QuizType.

Gert Arnold 27.08.2024 14:37
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

При таком обходе графов отдельных сущностей эта проблема будет возникать довольно часто. Любые ссылки на существующие данные должны быть проверены в кеше отслеживания перед присоединением или вставкой строки. В вашем случае это выделение QuizType, поскольку на QuizType ссылаются практически все остальные объекты, используемые здесь. Я бы избегал переназначения ссылок, поскольку это делает код более запутанным для чтения, также важно именование, поэтому, если вы получаете QuizLog, называйте его quizLog, а не quiz, если есть сущность Quiz.

public async Task<IActionResult> QuizAnswer(Guid id, [FromBody] QuizLog quizLog)
{
    try
    {
        Guid userId = JwtAuthorization.GetUserId(Request.Headers.Authorization!, _conf);

        var user = await _db.Users
             .Include(u => u.UserLog)
             .Include(u => u.QuizType)
             .FirstOrDefaultAsync(u => u.Id == userId);
        var quiz = await _db.Quiz
             .Include(q => q.QuizType)
             .FirstOrDefaultAsync(i => i.Id == id);

        if (user == null || quiz == null)
        {
            await Console.Out.WriteLineAsync("no user or quiz found");
            return NotFound();
        }

        if (!user.FinalTest)
        {
            return Ok();
        }

        // This is what you need to do for any and all references in a
        // detached entity. Check the .Local tracking cache for an 
        // existing tracked reference. If found, replace the reference
        // with the tracked one. If not found, Attach.
        var existingQuizType = _db.QuizTypes.Local
           .FirstOrDefault(qt = qt.Id == quizLog.QuizType.Id);
        if (existingQuizType != null)
           quizLog.QuizType = existingQuizType;
        else
           _db.Attach(quizLog.QuizType;

        quizLog.Id = Guid.NewGuid(); // Consider using Identity columns with new IDs set in DB. Default GUID is terrible for PKs /w clustered indexes. None of this code should be necessary when set up correctly.
        foreach (var item in quizLog.Questions)
        {
            item.Id = Guid.NewGuid(); // Identity
            foreach (var answer in item.Answers)
            {
                answer.Id = Guid.NewGuid(); // Identity
            }
        }

        UserLogs log = new UserLogs
        {
            Id = Guid.NewGuid();
            QuizSnapshot = quizLog;
            Quiz = quiz;
            User = user;
            SubmitDate = DateTime.UtcNow;
            FinalTest = false;
        };


        await _db.UserLogs.AddAsync(log);
        await _db.SaveChangesAsync();
        return Ok();
    }
    catch (Exception ex)
    {
        await Console.Out.WriteLineAsync("\n" + ex.ToString() + "\n");
        return BadRequest("something went wrong");
    }
}

Возможно, потребуются дополнительные изменения, но я бы рассмотрел возможность правильной реализации столбцов идентификаторов в схеме базы данных, чтобы избавиться от всех этих настроек идентификаторов. Идентификаторы GUID — плохой выбор для типичного кластерного индекса на ПК. Особенно лучше будет последовательный GUID, гораздо лучше — последовательный числовой столбец идентификаторов. Если вы используете GUID (последовательный или по умолчанию), я рекомендую запланировать регулярное задание по обслуживанию индекса в базе данных, чтобы регулярно реорганизовывать индекс и уменьшать фрагментацию.

Спасибо за ответ, это действительно было то, что мне нужно, но мне не хватало. Я рассмотрю предложенные вами изменения GUID/Identity, но пока они работают безупречно. Спасибо за ответ и понимание.

Quan 28.08.2024 08:21

Другие вопросы по теме