Невозможно обновить отношение «многие ко многим». Сначала код Entity Framework

Я пытаюсь обновить отношения «многие ко многим», которые я установил в Entity Framework, используя Code First. Я создал следующие модели.

[Serializable]
public class ClientFormField : FormField
{
    public ClientFormField()
    {
        CanDisable = true;
    }

    public virtual ClientFormGroup Group { get; set; }
    public virtual ICollection<ClientValue> Values { get; set; }

    public virtual ICollection<LetterTemplate> LetterTemplates { get; set; }
}

[Serializable]
public class CaseFormField : FormField
{
    public CaseFormField()
    {
        CanDisable = true;
    }

    public virtual CaseFormGroup Group { get; set; }

    public virtual ICollection<LetterTemplate> LetterTemplates { get; set; }

    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        foreach (var val in base.Validate(validationContext))
            yield return val;
    }
}

[Serializable]
public class SystemField : TrackableEntity
{
    public string Name { get; set; }
    public string Value { get; set; }
    public string VarName { get; set; }
    public virtual SystemFieldType SystemFieldType { get; set; }
    public int TypeId { get; set; }

    public ICollection<LetterTemplate> LetterTemplates { get; set; }

    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (string.IsNullOrWhiteSpace(Name))
            yield return new ValidationResult("System field must have a name.", new[] { "SystemFieldName" });

        if (string.IsNullOrWhiteSpace(Value))
            yield return new ValidationResult("System field must have a value.", new[] { "SystemFieldValue" });

        var regex = new Regex(@"^[a-zA-Z0-9-_]+$");
        if (!string.IsNullOrWhiteSpace(VarName) && !regex.IsMatch(VarName))
            yield return
                new ValidationResult("Varname can only contain alphanumeric, underscore, or hyphen",
                                     new[] { "SystemFieldVarName" });

        if (TypeId <= 0)
            yield return new ValidationResult("System Field must have a type.", new[] { "SystemFieldType" });
    }
}

[Serializable]
public class LetterTemplate : TrackableEntity
{
    public LetterTemplate()
    {
        ClientFields = new Collection<ClientFormField>();
        CaseFields = new Collection<CaseFormField>();
        SystemFields = new Collection<SystemField>();
    }

    public string Name { get; set; }
    public string Data { get; set; }

    public virtual ICollection<ClientFormField> ClientFields { get; set; }
    public virtual ICollection<CaseFormField> CaseFields { get; set; }
    public virtual ICollection<SystemField> SystemFields { get; set; }

    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if(string.IsNullOrWhiteSpace(Name))
            yield return new ValidationResult("Form Template must have a name", new[] { "Name" });

        if(string.IsNullOrWhiteSpace(Data))
            yield return new ValidationResult("Form Template must have content", new[] { "Data" });
    }
}

Ниже приведена конфигурация класса LetterTemplate.

public class LetterTemplateConfiguration : BaseTrackableEntityConfiguration<LetterTemplate>
{
    public LetterTemplateConfiguration()
    {
        HasMany(c => c.ClientFields).WithMany(c => c.LetterTemplates)
            .Map(m =>
                     {
                         m.MapLeftKey("LetterTemplateId");
                         m.MapRightKey("ClientFormFieldId");
                         m.ToTable("LetterTemplateClientFields");
                     });

        HasMany(c => c.CaseFields).WithMany(c => c.LetterTemplates)
            .Map(m =>
                     {
                         m.MapLeftKey("LetterTemplateId");
                         m.MapRightKey("CaseFormFieldId");
                         m.ToTable("LetterTemplateCaseFields");
                     });

        HasMany(c => c.SystemFields).WithMany(c => c.LetterTemplates)
            .Map(m =>
                     {
                         m.MapLeftKey("LetterTemplateId");
                         m.MapRightKey("SystemFieldId");
                         m.ToTable("LetterTemplateSystemFields");
                     });
    }
}

Вот метод контроллера для добавления/обновления и метод сервера, который содержит бизнес-логику для добавления/обновления.

[HttpPost]
[ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Manage(LetterTemplate template)
{
    if(ModelState.IsValid)
    {
        if (_letterTemplateService.Save(template) != null)
            return RedirectToAction("List");
    }

    ViewBag.ClientFields = _clientFieldService.GetAllFields().OrderBy(f => f.Name);
    ViewBag.CaseFields = _caseFieldService.GetAllFields().OrderBy(f => f.Name);
    ViewBag.SystemFields = _systemFieldService.GetAllFields().OrderBy(f => f.Name);

    return View(template);
}

public LetterTemplate Save(LetterTemplate template)
{
    var dbTemplate = template;

    if (template.Id > 0)
    {
        dbTemplate = _letterTemplateRepo.GetById(template.Id);
        dbTemplate.Name = template.Name;
        dbTemplate.Data = template.Data;
    }

    dbTemplate.ClientFields.Clear();
    foreach (var field in _clientFieldRepo.All().Where(field => template.Data.Contains("~~" + field.VarName + "~~")))
        dbTemplate.ClientFields.Add(field);

    dbTemplate.CaseFields.Clear();
    foreach (var field in _caseFieldRepo.All().Where(field => template.Data.Contains("~~" + field.VarName + "~~")))
        dbTemplate.CaseFields.Add(field);

    dbTemplate.SystemFields.Clear();
    foreach (var field in _systemFieldRepo.All().Where(field => template.Data.Contains("~~" + field.VarName + "~~")))
        dbTemplate.SystemFields.Add(field);

    return template.Id <= 0 ? _letterTemplateRepo.Add(dbTemplate) : _letterTemplateRepo.Update(dbTemplate);
}

Вот представление для добавления/обновления шаблона письма.

@section RightContent
{
<h4>Manage</h4>
<form method="POST" action="/LetterTemplate/Manage" id="templateForm">
    <div id="toolbar">
        <div style="float: left;padding: 3px 0px 0px 10px;">
            @Html.LabelFor(m => m.Name)
            @Html.TextBoxFor(m => m.Name)
        </div>
        <div class="item" onclick=" $('#templateForm').submit(); ">
            <span class="save"></span>Save
        </div>
        <div class="item" onclick=" window.location = '/LetterTemplate/List'; ">
            <span class="list"></span>Back To List
        </div>
    </div>
    <div class="formErrors">
        @Html.ValidationSummary()
    </div>
    @Html.HiddenFor(m => m.Id)
    @Html.HiddenFor(m => m.Active)
    @Html.HiddenFor(m => m.IsDeleted)
    @Html.TextAreaFor(m => m.Data)
    @Html.AntiForgeryToken()
</form>
}

Когда я создаю новый шаблон из представления, все работает нормально. Я получаю поля, заполненные в моих таблицах отношений «многие ко многим», как я и ожидал. Когда я пытаюсь обновить отношения, которые должны очистить все существующие отношения и создать новые отношения, ничего не происходит. Таблицы никак не затрагиваются. Я прочитал несколько разных сообщений о проблемах с обновлением многих таблиц, но не нашел ничего, что могло бы решить мою проблему. Это первый раз, когда я сначала попробовал многие ко многим с кодом EF и предварительно выполнил множество руководств, но кажется, что независимо от того, что я делаю, EF не будет обновлять таблицы отношений.

ОБНОВЛЕНИЕ:

Запросы, генерируемые при добавлении нового шаблона:

DECLARE @0 nvarchar = N'Test',
        @1 nvarchar = N'<p>~~case_desc~~</p>

<p>~~fname~~</p>

<p>~~lname~~</p>
',
        @2 bit = 1,
        @3 bit = 0,
        @4 int = 2,
        @5 int = 2,
        @6 DateTime2 = '2013-04-08T16:36:09',
        @7 DateTime2 = '2013-04-08T16:36:09'

insert [dbo].[LetterTemplates]([Name], [Data], [Active], [IsDeleted], [CreatedById], [ModifiedById], [DateCreated], [DateModified])
values (@0, @1, @2, @3, @4, @5, @6, @7)


DECLARE @0 int = 2,
        @1 int = 1

insert [dbo].[LetterTemplateClientFields]([LetterTemplateId], [ClientFormFieldId])
values (@0, @1)

DECLARE @0 int = 2,
        @1 int = 2

insert [dbo].[LetterTemplateClientFields]([LetterTemplateId], [ClientFormFieldId])
values (@0, @1)

DECLARE @0 int = 2,
        @1 int = 3

insert [dbo].[LetterTemplateClientFields]([LetterTemplateId], [ClientFormFieldId])
values (@0, @1)

Запрос, созданный при обновлении:

DECLARE @0 nvarchar = N'Test',
        @1 nvarchar = N'<p>~~case_desc~~</p>

<p> </p>

<p>~~fname~~</p>

<p> </p>

<p>~~dob~~</p>
',
        @2 bit = 1,
        @3 bit = 0,
        @4 int = 2,
        @5 int = 2,
        @6 DateTime2 = '2013-04-08T16:23:12',
        @7 DateTime2 = '2013-04-08T16:33:15',
        @8 int = 1

update [dbo].[LetterTemplates]
set [Name] = @0, [Data] = @1, [Active] = @2, [IsDeleted] = @3, [CreatedById] = @4, [ModifiedById] = @5, [DateCreated] = @6, [DateModified] = @7
where ([Id] = @8)

ОБНОВЛЕНИЕ

В моем шаблоне репозитория есть 2 базовых универсальных класса. Базовый репозиторий отслеживаемых сущностей и базовый репозиторий. Базовый репозиторий с отслеживаемыми объектами обеспечивает обратимое удаление удаленных элементов, получение неудаленных элементов и управление параметрами createdby/modifiedby и createdDate/UpdatedDate. Базовое репо обрабатывает остальные основные операции CRUD. Ниже приведен метод обновления и связанные с ним методы, которые вызываются, когда я вызываю обновление через LetterTemplateRepository. Поскольку этот репозиторий наследует базовый репозиторий отслеживаемых объектов, он запускает обновление из базового класса.

public override T Update(T entity)
{
    return Update(entity, false);
}

public override T Update(T entity, bool attachOnly)
{
    InsertTeData(ref entity);
    entity.ModifiedById = CurrentUserId;
    entity.DateModified = DateTime.Now;
    _teDB.Attach(entity);
    _db.SetModified(entity);
    if (!attachOnly) _db.Commit();
    return entity;
}

private void InsertTeData(ref T entity)
{
    if (entity == null || entity == null) return;
    var dbEntity = GetById(entity.Id);
    if (dbEntity == null) return;
    _db.Detach(dbEntity);
    entity.CreatedById = dbEntity.CreatedById;
    entity.DateCreated = dbEntity.DateCreated;
    entity.ModifiedById = dbEntity.ModifiedById;
    entity.DateModified = dbEntity.DateModified;
}

Метод SetModified в DbContext просто устанавливает для EntityState значение Modified. Я использую поддельные DbContext и DbSet в своих модульных тестах, поэтому любые вызовы, специфичные для EF, я расширяю через DbContext, чтобы мои тесты работали без необходимости создавать кучу поддельных репозиториев.


person DSlagle    schedule 08.04.2013    source источник
comment
Что делает ваш метод _letterTemplateRepo.Update()?   -  person Chris Curtis    schedule 09.04.2013
comment
Я обновил свой вопрос вызовами моих классов репозитория для метода обновления.   -  person DSlagle    schedule 09.04.2013
comment
Не могли бы вы провести эксперимент? Замените _letterTemplateRepo.Update(dbTemplate) на _letterTemplateRepo.Save(), что вызовет просто _db.SaveChanges().   -  person Chris Curtis    schedule 09.04.2013
comment
Это определенно привело бы меня к правильному ответу, если бы я не нашел его раньше, поскольку проблема заключалась в вызове метода Update.   -  person DSlagle    schedule 10.04.2013
comment
Рад, что ты понял это. В EF, когда вы прикрепляете объект и устанавливаете его состояние «Изменен», он не устанавливает состояние на всем графике, поэтому ни одно из свойств навигации не будет обновляться (или, по крайней мере, как я понимаю)   -  person Chris Curtis    schedule 10.04.2013


Ответы (1)


Оказывается, проблема была в методе InsertTeData. Когда он отсоединял объект, который я вытащил из базы данных, чтобы убедиться, что он создан и создан, это привело к тому, что объект, с которым я работал, потерял всю информацию об отношениях «многие ко многим». Я предполагаю, как работает отслеживание сущностей, и у них обоих был один и тот же ключ.

Я удалил метод InsertTeData и теперь управляю всем как значениями по умолчанию в конструкторе абстрактного класса TrackableEntity, и теперь все работает.

public override T Update(T entity, bool attachOnly)
{
    entity.ModifiedById = CurrentUserId;
    entity.DateModified = DateTime.Now;
    _teDB.Attach(entity);
    _db.SetModified(entity);
    if (!attachOnly) _db.Commit();
    return entity;
}

После выполнения всех моих модульных тестов, интеграционных тестов и некоторых ручных тестов все прошло успешно, поэтому я согласен с этим изменением.

person DSlagle    schedule 09.04.2013