Как вы обновляете свойство LastUpdate родительских сущностей при добавлении или изменении дочернего элемента на любом уровне иерархии под ним в Entity Framework?

Я создал простую иерархию «один ко многим», используя подход «сначала код». Все объекты в иерархии имеют идентификатор, который является идентификатором 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, и это всего лишь небольшая проблема, о которой я не смог найти никакой документации или сообщений, поскольку большинство людей, похоже, хотят идти в другом направлении, обновляя дочерние элементы, когда родитель обновляется.

EF ничего не предлагает из коробки. Это требует пользовательского кода.

Gert Arnold 13.04.2023 20:36

Насколько я знаю, Update() будет обновлять только этот конкретный объект в этой таблице. Если вам нужно обновить другие объекты, вам нужно получить эти объекты, отследить их (например, прикрепив их к Context) и рекурсивно обновить их.

Codingwiz 13.04.2023 21:22

Спасибо за ваши ответы. Конечно, это облом, но это объясняет, почему я не смог найти способ сделать это. Думаю, мне придется делать это рекурсивно, чего я надеялся избежать. Увы, ушная сера.

Pile 13.04.2023 21:47

См. ответ от pjs для отличного примера того, как сделать его достаточно универсальным (вам все еще нужно найти, какая сущность была обновлена, но она позволяет использовать один обработчик, а это что-то.

Pile 14.04.2023 20:50
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
52
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

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

Это не так автоматически, как хотелось бы, но я добавляю в контекст обработчик событий 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 имел что-то для этого без рекурсивного просмотра родительских сущностей.

Pile 14.04.2023 20:49

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