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
(разные)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()
в конце снова начинает отслеживание, что привело к той же проблеме.
Аннотации внешнего ключа:
Я пробовал использовать аннотации внешнего ключа в User
, Quiz
и QuizLog
, добавляя новое свойство, например public Guid QuizTypeId { get; set; }
, но это не решило проблему.
Прикрепление существующих объектов:
Я попробовал прикрепить существующие объекты QuizTypes
перед сохранением, надеясь, что это обеспечит использование только одного экземпляра для всех трех объектов, но это тоже не помогло.
Очистка трекера изменений:
Я позвонил _db.ChangeTracker.Clear()
прямо перед тем, как вызвать _db.UserLogs.Add(log);
, и убедился, что до прояснения никаких объектов не отслеживалось. Однако это также не решило проблему.
Не храните контексты в течение длительного времени, не сериализуйте объекты БД в пользовательский интерфейс, а затем (когда вы вернете их обратно измененными пользователем) попытайтесь повторно восстановить объекты БД, прикрепить их к средству отслеживания изменений и убедить EF, что это объект, который он должен использовать, чтобы что-то сделать с БД. Создайте разные классы моделей для вашего пользовательского интерфейса, попросите EF выполнить поиск в БД, скопируйте данные из объекта пользовательского интерфейса в объект БД, который вы получаете от EF (возможно, с использованием автоматического сопоставителя), и сохраните .. Чем больше вы пытаетесь контролировать EF, тем сложнее становится ваша жизнь
Код, который я отправил, был последней попыткой, но я, конечно, начал с самого простого плана, с вызовами базы данных, проверяя, что они не равны нулю, а затем добавляя новый объект. Я вернулся к этому, заменил вызовы базы данных на findAsync, но он выдает «Дубликат записи «GUID» для ключа «PRIMARY». Позвольте мне обновить свой код в моем сообщении, чтобы отразить мои изменения.
Если var p = await context.People.FindAsync(123); p.Name = "John Smith"; await context.SaveChangesAsync();
выдает дублирующее исключение PK, ваши проблемы в другом месте.
Не используйте AsNoTracking()
, если обновляете enitites.
Добавьте свойства внешнего ключа QuizTypeId
помимо QuizType
и установите их вместо QuizType
.
При таком обходе графов отдельных сущностей эта проблема будет возникать довольно часто. Любые ссылки на существующие данные должны быть проверены в кеше отслеживания перед присоединением или вставкой строки. В вашем случае это выделение 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, но пока они работают безупречно. Спасибо за ответ и понимание.
Кажется, что вы делаете жизнь намного сложнее, чем она должна быть. С помощью EF вы извлекаете объект из БД, изменяете его свойства и вызываете сохранение в контексте. Ничего из этого возиться с Entity, а также отсоединением/присоединением, возиться с состоянием объекта и т. д. Кстати, вы можете использовать FindAsync вместо этого «посмотреть локально, а затем получить из базы данных, если он не локальный».