Как обновить свойство навигации - без изменения каких-либо других полей

1

У меня возникла проблема с использованием EF для создания запроса на обновление, когда дело доходит до fk. Я хочу изменить навигационное поле в существующем объекте, не загружая его раньше. Таким образом, объект не предварительно загружен в контексте, а не проксирован;

Например, у меня есть простая связь 1- *, которая дает мне следующие два объекта:

public partial class NameMap
{
    public NameMap()
    {
        this.SurnameMaps = new HashSet<SurnameMap>();
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string MainLang { get; set; }

    public virtual ICollection<SurnameMap> SurnameMaps { get; set; }
    public virtual TitleMap TitleMap { get; set; }
}

public partial class TitleMap
{
    public TitleMap()
    {
        this.NameMaps = new HashSet<NameMap>();
    }

    public int Id { get; set; }
    public string Title { get; set; }

    public virtual ICollection<NameMap> NameMaps { get; set; }
}

Когда я изменяю некоторые скалярные свойства, я делаю следующее:

public void PartiallyChangeEntity()
{
    var nmToModify = new NameMap()
    {
        Id = 2, //item i want to change
        Name = "This is a test", //the prop i want to change
        MainLang = String.Empty //must be init and not null - but i come back to this point later (1)
    };

    _context.NameMaps.Attach(nmToModify);

    _context.Entry(nmToModify).Property(a => a.Name).IsModified = true;

    _context.SaveChanges();
}

И EF только изменяют имя в таблице. (1) В поле MainLang не внесены изменения, но проверка не выполняется, если значение установлено равным null (кажется, что поле MainLang не имеет значения NULL в db, означает, что он не может быть нулевым при вычислении валидации)

Теперь я хочу изменить название имени. Как и раньше, я попытался:

public void PartiallyChangeEntityConstraint()
{
    var nmToModify = new NameMap()
    {
        Id = 2,
        MainLang = String.Empty //same as (1)
    };

    _context.NameMaps.Attach(nmToModify);

    var title = new TitleMap {Id = 3}

    _context.NameMaps.Attach(title);

    _context.Entry(title).Collection(a => a.NameMaps).CurrentValue.Add(nmToModify);
    _context.Entry(nmToModify).Reference(a => a.TitleMap).CurrentValue = tm;

    _context.SaveChanges();
}

Этот метод не удался и выбрал эту ошибку:

Test Name:  PartiallyChangeEntityConstraint
Test FullName:  EFTests.UnitTest1.PartiallyChangeEntityConstraint
Test Source:    d:\TFS\EFTests\EFTests\UnitTest1.cs : line 100
Test Outcome:   Failed
Test Duration:  0:00:02.0409469

Result Message: 
Test method EFTests.UnitTest1.PartiallyChangeEntityConstraint threw exception: 
System.Data.Entity.Infrastructure.DbUpdateException: An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details. ---> System.Data.Entity.Core.UpdateException: A relationship from the 'TitleMapNameMap' AssociationSet is in the 'Added' state. Given multiplicity constraints, a corresponding 'NameMap' must also in the 'Added' state.
Result StackTrace:  
at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.RelationshipConstraintValidator.ValidateConstraints()
   at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.ProduceCommands()
   at System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.Update()
   at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.<Update>b__2(UpdateTranslator ut)
   at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update[T](T noChangesResult, Func'2 updateFunction)
   at System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update()
   at System.Data.Entity.Core.Objects.ObjectContext.<SaveChangesToStore>b__35()
   at System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction[T](Func'1 func, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess)
   at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesToStore(SaveOptions options, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction)
   at System.Data.Entity.Core.Objects.ObjectContext.<>c__DisplayClass2a.<SaveChangesInternal>b__27()
   at System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute[TResult](Func'1 operation)
   at System.Data.Entity.Core.Objects.ObjectContext.SaveChangesInternal(SaveOptions options, Boolean executeInExistingTransaction)
   at System.Data.Entity.Core.Objects.ObjectContext.SaveChanges(SaveOptions options)
   at System.Data.Entity.Internal.InternalContext.SaveChanges()
 --- End of inner exception stack trace ---
    at System.Data.Entity.Internal.InternalContext.SaveChanges()
   at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
   at System.Data.Entity.DbContext.SaveChanges()
   at EFTests.UnitTest1.PartiallyChangeEntityConstraint() in d:\TFS\EFTests\EFTests\UnitTest1.cs:line 103

Я пробовал много идеи, например:

_context.Entry(nmToModify).Property(a => a.TitleMap).IsModified = true;

но TitleMap не является свойством Ef, поэтому он не хочет этого; звучит честно.

Мой главный вопрос: как решить частичное обновление, когда дело касается свойств навигации?

Примечание: если это возможно, я не хочу раскрывать TitleMapId в NameMap.

Бонусный вопрос: есть ли способ не инициализировать непустые поля, так как я показываю его в (1)

Спасибо за вашу помощь.

Теги:
entity-framework

1 ответ

0
Лучший ответ

Эта часть исключения дает немного понять, что может быть проблемой:

Отношения "AssociationMapNameMap" AssociationSet находятся в состоянии "Добавлено". Учитывая ограничения множественности, соответствующая "NameMap" также должна быть в состоянии "Добавлено".

Ваши отношения необходимы, т. NameMap должен иметь ссылку на TitleMap. Если добавлена требуемая связь, соответствующий зависимый объект (NameMap в этом случае) должен быть новым (то есть также в Added состоянии), поскольку существующий объект уже должен иметь отношения, и связь может быть изменена только. Модели EF изменили отношения как удаление старых отношений и добавление новых отношений в менеджер отношений. Поскольку в State Deleted нет отношения, предполагается, что зависимый объект может быть только новым. Однако в отслеживании изменений нет нового объекта, который вызывает исключение.

Теперь, вероятно, эта проблема также может быть решена, если менеджер отношений найдет запись отношения в состоянии Deleted соответствующую тому, который Added. Вы можете достичь этого, NameMap ссылку на TitleMap прежде чем присоединяться:

public void PartiallyChangeEntityConstraint()
{
    var nmToModify = new NameMap()
    {
        Id = 2,
        MainLang = String.Empty,
        TitleMap = new TitleMap { Id = XXX }
    };
    _context.NameMaps.Attach(nmToModify);

    var title = new TitleMap { Id = 3 }
    _context.TitleMaps.Attach(title);

    _context.Entry(nmToModify).Reference(a => a.TitleMap).CurrentValue = title;
    // You could also simply use here: nmToModify.TitleMap = title;
    // or did you disable automatic change detection?

    _context.SaveChanges();
}

EF распознает назначение нового title nmToModify в качестве изменения отношений от внешнего ключа XXX до 3 и создаст две записи отношений в состоянии " Deleted и в состоянии " Added соответственно.

Теперь большой вопрос: что такое значение XXX? Это не должно быть 3 потому что тогда вы получите исключение, что два объекта с одним и тем же ключом были прикреплены, что запрещено. Эту проблему легко обойти (используйте 3+1 или что-то еще). Большая проблема заключается в том, что вы, по-видимому, не можете использовать какое-либо произвольное значение фиктивного типа (например, -1 или 0 или что-то еще). Это должен быть внешний ключ для TitleMap записи 2 NameMap которая в настоящее время фактически хранится в базе данных. В противном случае UPDATE в базе данных не работает, потому что сгенерированный SQL содержит предложение WHERE не только для PK NameMap но также для FK для TitleMap (WHERE NameMaps.Id = 2 AND NameMaps.TitleMap_Id = XXX). Если запись для обновления не найдена (и она не будет найдена, если XXX не является правильным текущим FK), вы получаете исключение параллелизма.

Такой вид изменения отношений, не зная старого внешнего ключа, намного проще с ассоциациями внешних ключей, т.е. Подвергая FK как свойство в вашей модели. Изменение отношения - это просто изменение скалярного свойства.

Об отказе в проверке исключений свойств, которые не были изменены: вы должны отключить проверку глобального контекста, а затем проверить вручную на уровне свойств (только те свойства, которые вы действительно хотите отметить как измененные):

_context.Configuration.ValidateOnSaveEnabled = false;

var entry = ctx.Entry(nmToModify);
var validationErrors = entry.Property(a => a.Name).GetValidationErrors();
if (validationErrors.Count > 0)
    throw new DbEntityValidationException("An entity property is invalid.", 
        new DbEntityValidationResult[] { new DbEntityValidationResult(
            entry, validationErrors) });

entry.Property(a => a.Name).IsModified = true;

(Идея и код, основанные на этом ответе, который также содержит больше объяснений и подробностей.)

  • 0
    Большое спасибо за это. Теперь я получаю правильный запрос sql. Я должен признать, что восстановление идентификатора fk немного скучно; Я думаю о выставлении fk Id в качестве скалярного свойства в моих документах. Похоже, это лучшая практика для достижения моей цели.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню