EF Core: обновление графа объектов дублирует дочерние объекты - PullRequest
2 голосов
/ 07 января 2020

У нас довольно сложная модель предметной области, и мы используем Entityframework Core в качестве ORM. Обновления всегда выполняются на объектах root. Если нам нужно добавить или обновить дочерний объект, мы загружаем сущность root, изменяем дочерние объекты и затем сохраняем сущность root. Аналогично этой части документа: https://docs.microsoft.com/en-us/ef/core/saving/disconnected-entities#mix -of-new-and-существующих-entity Мы используем GUID в качестве идентификаторов для сущностей, а идентификаторы генерируются базой данных на вставках!

Это работает довольно хорошо, но есть проблема, которую я не могу решить:

  • Я хочу добавить новый элемент (типа GeneralElementTemplate) в root сущность типа StructureTemplate
  • Я загружаю сущность StructureTemplate из БД со всеми подчиненными сущностями (в сущности root уже есть один элемент -> см. Скриншот № 1)
  • Я создаю новый элемент (с именем elementTemplate)
  • Я добавляю новый элемент в коллекцию Elements в сущности root (теперь две сущности находятся в коллекции Elements -> см. скриншот # 2)
  • Я вызываю SaveChanges в DBContext
  • Все отлично сохраняется
  • Но теперь в коллекции Elements объекта root есть ТРИ сущности! Новая добавленная сущность дважды в коллекции (см. Снимок экрана № 3)!?
  • В базе данных (SQL Сервер) все вставляется / обновляется, как и ожидалось. После операции объект root имеет два элемента (а не три) ...

        GeneralElementTemplate elementTemplate = new GeneralElementTemplate(ElementTemplateType.Line);
    
        StructureTemplate structureTemplate = DbContext.StructureTemplates
                                            .Include(x => x.Elements).ThenInclude(e => e.Attributes)
                                            .Include(x => x.Elements).ThenInclude(e => e.Groups)
                                            .Include(x => x.Elements).ThenInclude(e => e.Materials)
                                            .Include(x => x.Elements).ThenInclude(e => e.Points)
                                            .Include(x => x.Elements).ThenInclude(e => e.Sections)
                                            .Where(b => b.Id == structureTemplateId)
                                            .SingleOrDefault();
    
        if (structureTemplate == null)
        {
            return NotFound();
        }
    
        structureTemplate.AddElementTemplate(elementTemplate);
    
        DbContext.SaveChanges();
    

Я уже пытался создать небольшой пример проекта, чтобы продемонстрировать это поведение, но с Пример проекта все работает отлично. Может кто-нибудь объяснить, что происходит?

Реализация StructureTemplate:

public class StructureTemplate : Document<StructureTemplate>
{
    private HashSet<GeneralElementTemplate> _elements = new HashSet<GeneralElementTemplate>();

    private HashSet<StructureTemplateTag> _structureTemplateTags = new HashSet<StructureTemplateTag>();

    public StructureTemplate(
        DocumentHeader header,
        uint versionNumber = InitialLabel,
        IEnumerable<GeneralElementTemplate> elements = null)
        : base(header, versionNumber)
    {
        _elements = (elements != null) ? new HashSet<GeneralElementTemplate>(elements) : new HashSet<GeneralElementTemplate>();
    }

    /// <summary>
    /// EF Core ctor
    /// </summary>
    protected StructureTemplate()
    {
    }

    public IReadOnlyCollection<GeneralElementTemplate> Elements => _elements;

    public IReadOnlyCollection<StructureTemplateTag> StructureTemplateTags => _structureTemplateTags;

    public override IReadOnlyCollection<Tag> Tags => _structureTemplateTags.Select(x => x.Tag).ToList();

    public void AddElementTemplate(GeneralElementTemplate elementTemplate)
    {
        CheckUnlocked();

        _elements.Add(elementTemplate);
    }

    public override void AddTag(Tag tag) => _structureTemplateTags.Add(new StructureTemplateTag(this, tag));

    public void RemoveElementTemplate(Guid elementTemplateId)
    {
        CheckUnlocked();

        var elementTemplate = Elements.FirstOrDefault(x => x.Id == elementTemplateId);
        _elements.Remove(elementTemplate);
    }

    public override void RemoveTag(Tag tag)
    {
        var existingEntity = _structureTemplateTags.SingleOrDefault(x => x.TagId == tag.Id);
        _structureTemplateTags.Remove(existingEntity);
    }

    public void SetPartTemplateId(Guid? partTemplateId)
    {
        CheckUnlocked();

        PartTemplateId = partTemplateId;
    }
}

Реализация GeneralElementTemplate:

publi c class GeneralElementTemplate: Entity {private HashSet _attributes = new HashSet (); private HashSet _groups = new HashSet (); private HashSet _materials = new HashSet (); private HashSet _points = new HashSet (); private HashSet _sections = new HashSet ();

    public GeneralElementTemplate(
        ElementTemplateType type,
        IEnumerable<NamedPointReference> points = null,
        IEnumerable<NamedSectionReference> sections = null,
        IEnumerable<NamedMaterialReference> materials = null,
        IEnumerable<NamedGroupReference> groups = null,
        IEnumerable<NamedAttributeReference> attributes = null)
        : base()
    {
        Type = type;
        _points = points != null ? new HashSet<NamedPointReference>(points) : new HashSet<NamedPointReference>();
        _sections = sections != null ? new HashSet<NamedSectionReference>(sections) : new HashSet<NamedSectionReference>();
        _materials = materials != null ? new HashSet<NamedMaterialReference>(materials) : new HashSet<NamedMaterialReference>();
        _groups = groups != null ? new HashSet<NamedGroupReference>(groups) : new HashSet<NamedGroupReference>();
        _attributes = attributes != null ? new HashSet<NamedAttributeReference>(attributes) : new HashSet<NamedAttributeReference>();
    }

    /// <summary>
    /// EF Core ctor
    /// </summary>
    protected GeneralElementTemplate()
    {
    }

    public IReadOnlyCollection<NamedAttributeReference> Attributes => _attributes;

    public IReadOnlyCollection<NamedGroupReference> Groups => _groups;

    public IReadOnlyCollection<NamedMaterialReference> Materials => _materials;

    public IReadOnlyCollection<NamedPointReference> Points => _points;

    public IReadOnlyCollection<NamedSectionReference> Sections => _sections;

    public ElementTemplateType Type { get; private set; }

    public virtual GeneralElementTemplate Reincarnate()
    {
        return new GeneralElementTemplate(
            Type,
            Points,
            Sections,
            Materials,
            Groups,
            Attributes);
    }
}

Конфигурация типа объекта для StructureTemplate:

public class StructureTemplateTypeConfiguration : IEntityTypeConfiguration<StructureTemplate>
{
    public void Configure(EntityTypeBuilder<StructureTemplate> builder)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }

        builder
            .Property(e => e.Id)
            .ValueGeneratedOnAdd();

        builder
            .OwnsOne(e => e.Header, headerBuilder =>
            {
                headerBuilder
                    .Property(e => e.Name)
                    .HasConversion<string>(x => x, x => EntityName.ToEntityName(x))
                    .HasMaxLength(EntityName.NameMaxLength)
                    .IsUnicode(false);

                headerBuilder
                    .Property(e => e.Descriptions)
                    .HasConversion(
                        d => JsonConvert.SerializeObject(d.ToStringDictionary()),
                        d => d == null
                        ? TranslationDictionary.Empty
                        : JsonConvert.DeserializeObject<Dictionary<EntityLang, string>>(d).ToTranslationDictionary())
                    .HasMaxLength((int)TranslatedEntry.EntryMaxLength * (Enum.GetValues(typeof(EntityLang)).Length + 1));
            });

        builder
            .Property(e => e.VersionNumber);

        builder
            .HasMany(e => e.Elements)
            .WithOne();
        builder.Metadata.FindNavigation(nameof(StructureTemplate.Elements)).SetPropertyAccessMode(PropertyAccessMode.Field);


        // TAGS
        builder
            .Ignore(e => e.Tags);
        builder
            .HasMany(e => e.StructureTemplateTags);
        builder.Metadata
            .FindNavigation(nameof(StructureTemplate.StructureTemplateTags))
            .SetPropertyAccessMode(PropertyAccessMode.Field);
    }
}

Конфигурация типа объекта для StructureTemplateElement:

public class StructureElementTemplateTypeConfiguration : IEntityTypeConfiguration<GeneralElementTemplate>
{
    public void Configure(EntityTypeBuilder<GeneralElementTemplate> builder)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }

        builder.ToTable("StructureTemplateElements");

        builder
            .Property(e => e.Id)
            .ValueGeneratedOnAdd();

        builder
            .Property(e => e.Type);

        builder
            .HasMany(e => e.Attributes)
            .WithOne();
        builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Attributes)).SetPropertyAccessMode(PropertyAccessMode.Field);

        builder
            .HasMany(e => e.Groups)
            .WithOne();
        builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Groups)).SetPropertyAccessMode(PropertyAccessMode.Field);

        builder
            .HasMany(e => e.Materials)
            .WithOne();
        builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Materials)).SetPropertyAccessMode(PropertyAccessMode.Field);

        builder
            .HasMany(e => e.Points)
            .WithOne();
        builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Points)).SetPropertyAccessMode(PropertyAccessMode.Field);

        builder
            .HasMany(e => e.Sections)
            .WithOne();
        builder.Metadata.FindNavigation(nameof(GeneralElementTemplate.Sections)).SetPropertyAccessMode(PropertyAccessMode.Field);
    }
}

Снимок экрана сеанса отладки: image one Element is in the root entity"> After adding the new element to the Elements collection of the root entity After DbContext.SaveChanges() the new element is in the Elements collection twice!

1 Ответ

1 голос
/ 20 января 2020

Проблема решена:)

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

Когда мы теперь добавим новый дочерний объект (идентификатор не установлен) в HashSet, GetHashCode () будет вызывается, и сущность сохраняется с этим ha sh в HashSet. Теперь EF Core сохранил сущность и установил идентификатор (теперь GetHashCode будет возвращать другое значение). Затем EF Core проверяет, находится ли объект в HashSet. Поскольку код ha sh изменился, метод содержимого HashSet вернет false, и EF Core снова добавит сущность в набор.

Нашим решением было использование списков для дочерних сущностей!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...