Я создал простую иерархию «один ко многим», используя подход «сначала код». Все объекты в иерархии имеют идентификатор, который является идентификатором Guid, и временные метки для CreatedOn и LastUpdate. Они содержатся в классе BaseEntity. У каждой сущности могут быть и другие свойства, но они не важны применительно к данной проблеме. Каждый объект в иерархии дополнительно имеет внешний ключ для объекта, который является его родителем, а также набор дочерних объектов.
Например:
public class BaseEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime LastUpdate { get; set; }
}
public class RootDirectory : BaseEntity
{
public string RootName { get; set; }
public ICollection<SubDirectory> SubDirectories { get; set; }
}
Класс подкаталога выглядит следующим образом:
public class Subdirectory : BaseEntity
{
public string SubDirectoryName { get; set; }
public Guid? RootId { get; set; }
public RootDirectory Root { get; set; }
public ICollection<File> Files { get; set; }
}
Тогда класс File выглядит следующим образом:
public class File : BaseEntity
{
public string FileName { get; set; }
public Guid? SubDirectoryId { get; set; }
public SubDirectory SubDirectory { get; set; }
}
Это создает соответствующие отношения внешнего ключа, которые я ожидал. Все операции отлично работают для этой системы, кроме LastUpdate.
LastUpdate обновляется, как и ожидалось, когда я использую следующий общий метод в BaseRepo:
public virtual int Update(TEntity entity, bool persist = true)
{
Table.Update(entity);
return persist ? SaveChanges() : 0;
}
Например, когда я выполняю это для файла, LastUpdate изменяется, как и ожидалось, на время вызова обновления. Однако при обновлении файла подкаталог дополнительно не обновляется, и, в свою очередь, корневой каталог не обновляется.
По сути, есть ли в EF способ заставить родителей обновляться, если обновляется ребенок? За неимением лучших терминов, обратное каскадное обновление? (Каскадное удаление удаляет все дочерние элементы для каждого объекта при удалении на более высоком уровне, могу ли я обновить на более низком уровне и обновить всех вышестоящих родителей?)
Обратите внимание, что это очень важно, так как я хочу использовать этот BaseRepo в других иерархиях сущностей, отличных от этой, и эти иерархии могут иметь другое количество уровней и другие подобные различия.
Я полагаю, что это должно быть возможно, поскольку репозитории на GitHub и DevOps делают это, когда файл изменяется или добавляется. Файл, который был изменен, имеет последнее изменение, но то же самое относится и ко всем его родительским папкам, поскольку они знают, что что-то в них обновлено.
Дайте мне знать, если что-то не ясно. Как я уже сказал, все остальное у меня работает плавно с EF, и это всего лишь небольшая проблема, о которой я не смог найти никакой документации или сообщений, поскольку большинство людей, похоже, хотят идти в другом направлении, обновляя дочерние элементы, когда родитель обновляется.
Насколько я знаю, Update()
будет обновлять только этот конкретный объект в этой таблице. Если вам нужно обновить другие объекты, вам нужно получить эти объекты, отследить их (например, прикрепив их к Context
) и рекурсивно обновить их.
Спасибо за ваши ответы. Конечно, это облом, но это объясняет, почему я не смог найти способ сделать это. Думаю, мне придется делать это рекурсивно, чего я надеялся избежать. Увы, ушная сера.
См. ответ от pjs для отличного примера того, как сделать его достаточно универсальным (вам все еще нужно найти, какая сущность была обновлена, но она позволяет использовать один обработчик, а это что-то.
Похоже, что для этого нет текущего готового решения, поэтому на данный момент я реализую рекурсивное обновление, где я вручную просматриваю и обновляю всех родителей объектов, которые были обновлены или добавлены. Если это изменится в будущем, не стесняйтесь размещать новый ответ, говоря об этом.
Это не так автоматически, как хотелось бы, но я добавляю в контекст обработчик событий SavingChanges. Обработчик получает список добавленных или обновленных сущностей и передает их процедуре обработки. Вы можете увидеть специальный код для случаев, когда атрибут UpdatedDateTime существует в родительском объекте. Я думаю, вы, вероятно, могли бы автоматизировать большую часть этого, рекурсивно ища родительские сущности, но у меня есть только несколько случаев, поэтому было проще включить конкретный код для этих случаев.
/// <summary>
/// SavingChanges event handler to update UpdatedDateTime and UpdatedBy properties.
/// </summary>
/// <param name = "sender">The ConferenceDbContext whose changes are about to be saved.</param>
/// <param name = "e">Standard SavingChanges event arg object, not used here.</param>
private void AssignUpdatedByAndTime(object? sender, SavingChangesEventArgs e)
{
//Get added or modified entities.
List<EntityEntry> changedEntities = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
.ToList();
//Assign UpdatedDateTime and UpdatedBy properties if they exist.
foreach (EntityEntry entityEntry in changedEntities)
{
//Some subcategory entities have the updated date/by attributes in the parent entity.
EntityEntry? parentEntry = null;
string entityTypeName = entityEntry.Metadata.Name; //Full class name, e.g., ConferenceEF.Models.Meeting
entityTypeName = entityTypeName.Split('.').Last();
switch (entityTypeName)
{
case nameof(Paper):
case nameof(FlashPresentation):
parentEntry = entityEntry.Reference(nameof(SessionItem)).TargetEntry;
break;
default:
break;
}
AssignUpdatedByUserAndTime(parentEntry ?? entityEntry);
}
}
private void AssignUpdatedByUserAndTime(EntityEntry entityEntry)
{
if (entityEntry.Entity is BaseEntityWithUpdatedAndRowVersion
|| entityEntry.Entity is PaperRating)
{
PropertyEntry updatedDateTime = entityEntry.Property("UpdatedDateTime");
DateTimeOffset? currentValue = (DateTimeOffset?)updatedDateTime.CurrentValue;
//Avoid possible loops by only updating time when it has changed by at least 1 minute.
//Is this necessary?
if (!currentValue.HasValue || currentValue < DateTimeOffset.Now.AddMinutes(-1))
updatedDateTime.CurrentValue = DateTimeOffset.Now;
if (entityEntry.Entity is BaseEntityWithUpdatedAndRowVersion
|| entityEntry.Properties.Any(p => p.Metadata.Name == "UpdatedBy"))
{
PropertyEntry updatedBy = entityEntry.Property("UpdatedBy");
string? newValue = CurrentUserName; //ClaimsPrincipal.Current?.Identity?.Name;
if (newValue == null && !updatedBy.Metadata.IsColumnNullable())
newValue = string.Empty;
if (updatedBy.CurrentValue?.ToString() != newValue)
updatedBy.CurrentValue = newValue;
}
}
}
Спасибо за пример. Я на самом деле реализовал что-то почти идентичное этому. Я отмечу, что вы можете избежать Split(), просто используя entityEntry.Entity.GetType().Name, которое я использовал в качестве моей переменной case case, но это просто избегает необходимости анализировать имя из полного имени типа. Поскольку у меня это работает, я считаю это решением, хотя, очевидно, я хотел бы, чтобы EF Core имел что-то для этого без рекурсивного просмотра родительских сущностей.
EF ничего не предлагает из коробки. Это требует пользовательского кода.