Как мне остановить Entity Framework от попытки сохранить / вставить дочерние объекты?

102

Когда я сохраняю сущность с помощью структуры сущностей, я, естественно, предполагал, что она попытается сохранить только указанную сущность. Однако он также пытается сохранить дочерние сущности этой сущности. Это вызывает всевозможные проблемы с целостностью. Как заставить EF сохранять только тот объект, который я хочу сохранить, и, следовательно, игнорировать все дочерние объекты?

Если я вручную установил для свойств значение null, я получаю сообщение об ошибке «Операция завершилась неудачно: связь не может быть изменена, поскольку одно или несколько свойств внешнего ключа не допускают значения NULL». Это крайне контрпродуктивно, поскольку я специально установил для дочернего объекта значение null, чтобы EF оставил его в покое.

Почему я не хочу сохранять / вставлять дочерние объекты?

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

В приложении, которое я создаю, объектная модель EF не загружается из базы данных, а используется в качестве объектов данных, которые я заполняю при анализе плоского файла. В случае дочерних объектов многие из них относятся к таблицам поиска, определяющим различные свойства родительской таблицы. Например, географическое положение основного объекта.

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

Даже с дочерними объектами, которые являются реальными данными, мне нужно сначала сохранить родительский и получить первичный ключ, иначе EF просто напутает. Надеюсь, это даст какое-то объяснение.

Марк Микаллеф
источник
Насколько я знаю, вам придется обнулить дочерние объекты.
Johan
Привет, Йохан. Не работает. Это вызывает ошибки, если я обнуляю коллекцию. В зависимости от того, как я это делаю, он жалуется на то, что ключи равны нулю или что коллекция I была изменена. Очевидно, что это правда, но я сделал это специально, чтобы оставить в покое объекты, которых нельзя касаться.
Марк Микаллеф
Эйфория, это совершенно бесполезно.
Марк Микаллеф
@Euphoric Даже если дочерние объекты не меняются, EF по-прежнему пытается вставить их по умолчанию, не игнорируя и не обновляя их.
Johan
Что меня действительно раздражает, так это то, что если я изо всех сил стараюсь обнулить эти объекты, он жалуется, а не понимает, что я хочу, чтобы он просто оставил их в покое. Поскольку все эти дочерние объекты являются необязательными (допускают значение NULL в базе данных), есть ли способ заставить EF забыть, что у меня были эти объекты? т.е. как-нибудь очистить его контекст или кеш?
Марк Микаллеф,

Ответы:

56

Насколько я знаю, у вас есть два варианта.

Опция 1)

Обнулить все дочерние объекты, это гарантирует, что EF ничего не добавит. Он также ничего не удалит из вашей базы данных.

Вариант 2)

Установите дочерние объекты как отделенные от контекста, используя следующий код

 context.Entry(yourObject).State = EntityState.Detached

Обратите внимание, что вы не можете отсоединить List/ Collection. Вам нужно будет перебрать ваш список и отсоединить каждый элемент в вашем списке вот так

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}
Йохан
источник
Привет, Йохан, я попытался отсоединить одну из коллекций, и это вызвало следующую ошибку: Тип объекта HashSet`1 не является частью модели для текущего контекста.
Марк Микаллеф
@MarkyMark Не отключайте коллекцию. Вам нужно будет перебрать коллекцию и отсоединить ее от объекта (сейчас я обновлю свой ответ).
Johan
11
К сожалению, даже со списком циклов для отсоединения всего, EF, похоже, все еще пытается вставить в некоторые связанные таблицы. На этом этапе я готов вырезать EF и вернуться к SQL, который, по крайней мере, ведет себя разумно. Вот это боль.
Марк Микаллеф
2
Могу ли я использовать context.Entry (yourObject) .State, прежде чем добавлять его?
Thomas Klammer
1
@ mirind4 Я не использовал состояние. Если я вставляю объект, я убеждаюсь, что все дочерние элементы равны нулю. При обновлении я сначала получаю объект без дочерних элементов.
Томас Кламмер
41

Короче говоря: используйте внешний ключ, и это спасет вас.

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

Первоначально вы можете определять свои сущности следующим образом:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public City City { get; set; }
}

И вы можете сделать вставку School следующим образом (предположим, что у вас уже есть свойство City, назначенное для newItem ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

Вышеупомянутый подход может отлично работать в этом случае, однако я предпочитаю подход с использованием внешнего ключа, который для меня более ясен и гибок. См. Обновленное решение ниже:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

Таким образом, вы явно определяете, что школа имеет внешний ключ City_Id, и он относится к объекту City . Итак, когда дело доходит до вставки школы , вы можете:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

В этом случае вы явно указываете City_Id новой записи и удаляете город из графика, чтобы EF не потрудился добавить его в контекст вместе со школой .

Хотя на первый взгляд подход с использованием внешнего ключа кажется более сложным, но поверьте мне, такой подход сэкономит вам много времени, когда дело доходит до вставки отношения многие-ко-многим (представляя, что у вас есть отношения между школой и учеником, и ученик имеет свойство City) и так далее.

Надеюсь, это поможет вам.

lenglei
источник
Отличный ответ, мне очень помог! Но не лучше ли указать минимальное значение 1 для City_Id с помощью [Range(1, int.MaxValue)]атрибута?
Дэн Рэйсон,
Как мечта!! Бесконечно благодарен!
CJH
Это приведет к удалению значения из объекта School в вызывающей программе. Перезагружает ли SaveChanges значение нулевых свойств навигации? В противном случае вызывающий должен перезагрузить объект City после вызова метода Insert (), если ему нужна эта информация. Это шаблон, который я часто использовал, но я все еще открыт для лучших шаблонов, если у кого-то есть хороший.
однобокий
1
Это было действительно полезно для меня. EntityState.Unchaged - это именно то, что мне нужно, чтобы назначить объект, представляющий внешний ключ таблицы поиска в графе большого объекта, который я сохранял как одну транзакцию. EF Core выдает менее интуитивное сообщение об ошибке IMO. Я кэшировал свои таблицы поиска, которые редко меняются по соображениям производительности. Вы предполагаете, что, поскольку PK объекта таблицы поиска совпадает с тем, что уже есть в базе данных, он знает, что он не изменился, и просто назначил FK существующему элементу. Вместо этого пытается вставить объект таблицы поиска как новый.
tnk479
Никакая комбинация обнуления или установки состояния без изменений у меня не работает. EF настаивает на необходимости вставить новую дочернюю запись, когда я просто хочу обновить скалярное поле в родительском элементе.
BobRz
21

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

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

Первая строка присоединяет родительский объект и весь граф его зависимых дочерних объектов к контексту в Unchangedсостоянии.

Вторая строка изменяет состояние только для родительского объекта, оставляя его дочерние элементы в этом Unchangedсостоянии.

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

keykey76
источник
2
Этот ответ имеет то преимущество, что если кто-то придет позже и добавит дочерний объект, это не повредит существующий код. Это решение «по выбору», в котором другие требуют, чтобы вы явно исключили дочерние объекты.
Джим
Это больше не работает в ядре. «Присоединить: присоединяет каждую достижимую сущность, кроме тех случаев, когда у достижимой сущности есть ключ, созданный хранилищем, и значение ключа не назначено; они будут отмечены как добавленные». Если дети тоже новые, они будут добавлены.
mmix
14

Одно из предлагаемых решений - назначить свойство навигации из того же контекста базы данных. В этом решении свойство навигации, назначенное извне контекста базы данных, будет заменено. См. Следующий пример для иллюстрации.

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

Сохранение в базу данных:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

Microsoft считает это функцией, но меня это раздражает. Если объект отдела, связанный с объектом компании, имеет идентификатор, который уже существует в базе данных, то почему EF просто не связывает объект компании с объектом базы данных? Почему мы должны сами заботиться об ассоциации? Забота о свойстве навигации при добавлении нового объекта - это что-то вроде переноса операций с базой данных с SQL на C #, что неудобно для разработчиков.

Бикаш Бишвокарма
источник
Я согласен на 100%, это смешно, что EF пытается создать новую запись, когда указан идентификатор. Если бы существовал способ заполнить параметры списка выбора фактическим объектом, мы бы занимались бизнесом.
T3.0
11

Для начала вам нужно знать, что есть два способа обновить сущность в EF.

  • Прикрепленные объекты

Когда вы изменяете отношения объектов, прикрепленных к контексту объекта, с помощью одного из описанных выше методов, Entity Framework необходимо синхронизировать внешние ключи, ссылки и коллекции.

  • Отключенные объекты

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

В приложении, которое я создаю, объектная модель EF не загружается из базы данных, а используется в качестве объектов данных, которые я заполняю при анализе плоского файла.

Это означает, что вы работаете с отключенным объектом, но неясно, используете ли вы независимую ассоциацию или ассоциацию внешнего ключа .

  • Добавить

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

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Обновить

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

      db.Entity(entity).State = EntityState.Modified;

График Diff

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

Вот введение, Знакомство с GraphDiff для Entity Framework Code First - Разрешение автоматического обновления графа отсоединенных сущностей .

Образец кода

  • Вставьте объект, если он не существует, в противном случае обновите.

      db.UpdateGraph(entity);
  • Вставьте объект, если он не существует, в противном случае обновите И вставьте дочерний объект, если он не существует, в противном случае обновите.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
Юлиам Чандра
источник
3

Лучший способ сделать это - переопределить функцию SaveChanges в вашем тексте данных.

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }
Воутер Шут
источник
2

У меня такая же проблема, когда я пытаюсь сохранить профиль, я уже привожу приветствие и новый для создания профиля. Когда я вставляю профиль, он также вставляется в приветствие. Итак, я пробовал это до savechanges ().

db.Entry (Профиль.Salutation) .State = EntityState.Unchanged;

ZweHtatNaing
источник
1

Это сработало для меня:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;
Клаус
источник
0

Что мы сделали, так это перед добавлением родительской коллекции к dbset отключили дочерние коллекции от родительской, убедившись, что существующие коллекции перешли в другие переменные, чтобы можно было работать с ними позже, а затем заменили текущие дочерние коллекции новыми пустыми коллекциями. Установка дочерних коллекций на null / ничего нам показалась неудачной. После этого добавьте родителя в dbset. Таким образом, дети не добавляются до тех пор, пока вы этого не захотите.

Shaggie
источник
0

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

Ignore(parentObject => parentObject.ChildObjectOrCollection);

По сути, это укажет EF исключить свойство ChildObjectOrCollection из модели, чтобы оно не было сопоставлено с базой данных.

Сайед Датский
источник
В каком контексте используется "Игнорировать"? Похоже, что в предполагаемом контексте его не существует.
T3.0,
0

У меня была аналогичная проблема с использованием Entity Framework Core 3.1.0, моя логика репо довольно общая.

Это сработало для меня:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

Обратите внимание, что «ParentEntityId» - это имя столбца внешнего ключа дочерней сущности. Я добавил вышеупомянутую строку кода к этому методу:

protected override void OnModelCreating(ModelBuilder builder)...
Манелиси Нодада
источник