У меня возникла проблема с сохранением значения отношения «один ко многим» в базе данных. Постоянно пишет, что GroupId
отслеживается дважды, но у меня даже в репозитории отслеживания нет.
Если вы каким-либо образом можете помочь оптимизировать код, я буду очень признателен, но если нет или не можете, это тоже нормально.
DepartmentController
:
[HttpGet]
public async Task<IActionResult> Details (string id)
{
var department = await _repository.GetByIdAsync(id);
if (department != null)
{
var deparment = new EditDepartmentViewModel()
{
DepartmentId = department.DepartmentId,
DepartmentName = department.DepartmentName,
PeopleInDepartment = _repository.GetNamesByDepartment(id),
};
return View(deparment);
}
return RedirectToAction("Index");
}
[HttpPost]
public async Task<IActionResult> Details(EditDepartmentViewModel model)
{
model.DepartmentId = await _repository.GetIdByNameAsync(model.DepartmentName);
model.Persons = _repository.PersonsInDep(model.DepartmentId);
var userId = _contextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userRepository.GetByIdAsync(userId);
var department = await _repository.GetByIdAsync(model.DepartmentId);
if (department != null)
{
department.DepartmentName = model.DepartmentName;
department.Persons = model.Persons;
if (!string.IsNullOrWhiteSpace(model.PeopleInDepartment))
{
var names = model.PeopleInDepartment.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries);
var alreadyin = _repository.GetNamesByDepartment(model.DepartmentId);
var alinasList = alreadyin.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries);
if (names.Count() >= alinasList.Count())
{
foreach (var name in names)
{
if (alreadyin.Contains(name) == false)
{
var person = new Person
{
PersonId = Guid.NewGuid().ToString(),
Name = name.Trim(),
DepartmentId = department.DepartmentId,
GroupPerDay = new List<PersonInGroupPerDay>(),
};
var dg = new DayGroup()
{
Id = Guid.NewGuid().ToString(),
Group = await _groupRepository.GetByDepartment(department),
};
var p = new PersonInGroupPerDay()
{
Id = Guid.NewGuid().ToString(),
Person = person,
DayGroup = dg,
Groups = new List<Group>(),
};
p.Groups.Add(dg.Group);
person.GroupPerDay.Add(p);
department.Persons.Add(person);
}
_repository.Update(department);
}
}
else
{
foreach(var name in alinasList)
{
if (model.PeopleInDepartment.Contains(name) == false)
{
var person = await _repository.GetByNameAsync(name);
if (person != null)
{
var ps = await _repository.GetByPerson(person);
foreach (var p in ps)
{
_repository.DeletePIGPD(p);
}
_repository.DeletePerson(person);
}
}
_repository.Update(department);
}
}
}
return RedirectToAction("Index");
}
return RedirectToAction("Index");
}
DepartmentRepository
:
public bool Save()
{
var saved = _context.SaveChanges();
return saved > 0;
}
public bool Update(Department department)
{
_context.Update(department);
return Save();
}
public async Task<Department> GetByIdAsync(string id)
{
var dep = new Department();
dep.DepartmentId = id;
dep.DepartmentName = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.DepartmentName).FirstOrDefaultAsync();
dep.Persons = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(p => p.Persons).FirstOrDefaultAsync();
dep.Users = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Users).FirstOrDefaultAsync();
dep.DayDepartments = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.DayDepartments).FirstOrDefaultAsync();
dep.Group = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Group).FirstOrDefaultAsync();
dep.Days = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Days).FirstOrDefaultAsync();
dep.Months = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Months).FirstOrDefaultAsync();
return dep;
}
Детали отдела:
@model QLAnCaRedo.Web.ViewModels.EditDepartmentViewModel
<div class = "container mb-3">
<div class = "row pt-4">
<div class = "col-6">
<h2 class = "text-primaary">
Edit phòng ban
</h2>
</div>
</div>
</div>
<form method = "post" action = "Details">
<div class = "mb-3">
<label for = "" class = "form-label">Tên phòng ban</label>
<input type = "text" class = "form-control" asp-for = "DepartmentName" />
</div>
<div class = "mb-3">
<label for = "" class = "form-label">Người trong phòng ban</label>
<input type = "text" class = "form-control" asp-for = "@Model.PeopleInDepartment" />
</div>
<button type = "submit" class = "btn btn-primary">Submit</button>
<a type = "button" class = "btn btn-danger" asp-controller = "Department" asp-action = "Delete" asp-route-id = "@Model.DepartmentId">
Delete
</a>
Модели
public class Department
{
[Key]
public string DepartmentId { get; set; }
public string DepartmentName { get; set; }
public List<Person> Persons { get; set; }
public Group Group { get; set; }
}
public class Group
{
[Key]
public string GroupId { get; set; }
[Required]
public AppUser AppUser { get; set; }
public Department Department { get; set; }
[Required]
public List<DayGroup> DayGroups { get; set; }
}
public class DayGroup
{
public string Id { get; set; }
public DateTime SubcriptionDate { get; set; }
public Group Group { get; set; }
public List<PersonInGroupPerDay> PersonInGroups { get; set; }
}
public class PersonInGroupPerDay
{
public string Id { get; set; }
public DayGroup DayGroup { get; set; }
public Person Person { get; set; }
[DefaultValue(0)]
public int Set1 { get; set; }
[DefaultValue(0)]
public int Set2 { get; set; }
[DefaultValue(0)]
public int Set3 { get; set; }
public List<Group> Groups { get; set; }
}
public class EditDepartmentViewModel
{
public string DepartmentId { get; set; }
public string DepartmentName { get; set; }
public string PeopleInDepartment { get; set; }
public List<Person> Persons { get; set; }
}
Хорошо, помимо стандартного заявления об отказе от ответственности «Не пишите репозиторий, EF DbSet
уже является репозиторием», вы делаете что-то очень, очень неправильно, и мне искренне любопытно, где вы сталкивались с чем-то вроде такого примера??
Для начала этот код должен идти:
public async Task<Department> GetByIdAsync(string id)
{
var dep = new Department();
dep.DepartmentId = id;
dep.DepartmentName = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.DepartmentName).FirstOrDefaultAsync();
dep.Persons = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(p => p.Persons).FirstOrDefaultAsync();
dep.Users = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Users).FirstOrDefaultAsync();
dep.DayDepartments = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.DayDepartments).FirstOrDefaultAsync();
dep.Group = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Group).FirstOrDefaultAsync();
dep.Days = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Days).FirstOrDefaultAsync();
dep.Months = await _context.Departments.AsNoTracking().Where(d => d.DepartmentId == id).Select(d => d.Months).FirstOrDefaultAsync();
return dep;
}
В этом есть много неправильного. Сначала он делает 7 обращений к базе данных для заполнения 1 отдела. Во-вторых, он вернет отдел с указанным идентификатором и ничего больше, если отдел не найден.
Вот что вы должны вернуть:
public async Task<Department> GetByIdAsync(string id)
{
var department = await _context.Departments
.AsNoTracking()
.Single(d => d.DepartmentId == id);
return department;
}
Если есть связанные объекты, которые нужно включить, загрузите их, добавив .Include(x => x.{Insert Related Navigation Property})
. При этом извлекается отсоединенная сущность, но если вы получаете сущность с намерением ее обновить, то вместо этого я бы удалил .AsNoTracking()
, чтобы возвращаемую сущность можно было изменить, а средство отслеживания изменений создало подходящий SQL при вызове SaveChanges
в DbContext. МНОГО путаницы и сложностей возникнет при попытке обернуть функциональность EF DbContext
/DbSet
в класс репозитория, поэтому я настоятельно рекомендую удалить репозиторий и сначала заставить все работать, используя DbContext
и отделы DbSet
, прежде чем пытаться что-либо абстрагировать.
Если по какой-либо причине вы хотите исключить поля и т. д. при чтении объектов, которые необходимо перемещать, вы можете использовать один .Select()
, чтобы заполнить новую модель для возврата. Я не рекомендую использовать для этого сущность Department, если у вас есть DTO/ViewModel отдела для заполнения:
public async Task<DepartmentViewModel> GetByIdAsync(string id)
{
var departmentVM = await _context.Departments
.Where(d => d.DepartmentId == id)
.Select(d => new DepartmentViewModel
{
DepartmentId = d.DepartmentId,
// .. populate remaining fields...
}).Single();
return departmentVM;
}
Не заполняйте сущность «Отдел» частично, чтобы она служила моделью представления или результатом DTO транспорта данных. Проблема с выполнением чего-то подобного заключается в том, что методы, ожидающие сущность Department, всегда должны ожидать правильную, отслеживаемую сущность, не рискуя получить какой-то неотслеживаемый, частично заполненный заполнитель.
Суть ошибки: EF предназначен для работы со ссылками. Когда вы создаете сущность, связанную с другими сущностями, существует два основных типа ссылок. Ассоциации и дети. Ассоциации — это связи между этой сущностью и другими существующими записями в базе данных. Дети — это предметы, которые, по сути, будут жить или умирать в зависимости от своего Родителя. Что-то вроде «Группы», скорее всего, будет ассоциативным отношением, что означает, что когда вы добавляете или обновляете отдел и связываете его с группой, вам необходимо убедиться, что EF работает с единственной ссылкой на то, что, как он знает, является существующей записью. , а не что-то, что он собирается рассматривать как новую группу.
Например, если вы хотите создать отдел, связанный с записью группы с идентификатором 10, вам не нужно делать что-то вроде:
department.Group = new Group { GroupId = 10; }
или даже:
var group = _context.Groups.AsNoTracking().Single(g => g.GroupId == 10);
department.Group = group;
Вместо этого вы используете:
var group = _context.Groups.Single(g => g.GroupId == 10);
department.Group = group;
Ключевое отличие состоит в том, что последний пример извлекает отслеживаемую ссылку на группу, где AsNoTracking()
вернет неотслеживаемый новый экземпляр. EF будет рассматривать это как «новую» группу, если она будет добавлена в Департамент, а не как ассоциацию с существующей строкой.
Если у вас уже есть ссылка на группу, которая может или не может отслеживаться/прикрепляться, вам необходимо проверить и заменить эту ссылку, если DbContext уже отслеживает эту группу:
var trackedGroup = _context.Groups.Local.FirstOrDefault(g => g.GroupId = group.GroupId);
if (trackedGroup == null)
{
_context.Attach(group);
trackedGroup = group;
}
department.Group = trackedGroup;
Это проверка .Local
, которая ищет только локальный кэш отслеживания, но не попадает в базу данных. Если мы не найдем отслеживаемую копию, мы будем отслеживать отсоединенную копию (группу). Таким образом отдел привязывается к отслеживаемой ссылке.
Другие элементы, которые необходимо исправить, — это удалить все свойства навигации по коллекции настроек кода. Например:
public List<Person> Persons { get; set; }
Использовать:
public List<Person> Persons { get; protected set; } = new [];
Защитите установщик, удалив любой код, который пытается его установить. Список лиц будет инициализирован и готов к использованию в любом новом отделе, который вы создадите. Код никогда не должен вызывать установщик свойств навигации по коллекции, поскольку это нарушит отслеживание изменений в EF. Если вы хотите заменить коллекцию в сущности, это не так просто, как выгрузить коллекцию и воссоздать ее заново. Вам нужно выяснить, какие элементы удалить, а какие добавить, чтобы система отслеживания изменений знала, что нужно удалять строки, которые необходимо удалить, и вставьте строки, которые необходимо добавить. Сеттеры должны быть доступны только для ссылок на отдельные объекты, например public Group Group { get; set; }
.
Надеюсь, это поможет вам начать отслеживать проблемы с правильным связыванием сущностей при вставке и обновлении данных. Вероятно, есть еще несколько областей, которые будут вызывать проблемы, но попробуйте эти изменения и уменьшите объем кода, который вы пытаетесь протестировать, и тестируйте постепенно, чтобы облегчить обнаружение проблем раньше.
Он продолжает говорить, что GroupId отслеживается дважды, бесполезно. Какое полное и точное сообщение об ошибке вы получаете. Если вам нужна помощь, вам нужно предоставить реальную информацию, а не какой-то бессмысленный шум. Сообщение об ошибке находится на экране прямо перед вами, поэтому нет оправдания тому, что вы не указали его в своем сообщении.