Jak zatrzymać Entity Framework przed próbą zapisania / wstawienia obiektów podrzędnych?

102

Kiedy zapisuję jednostkę za pomocą frameworka jednostki, naturalnie zakładałem, że spróbuje tylko zapisać określoną jednostkę. Jednak próbuje również ocalić byty podrzędne tej jednostki. Powoduje to różnego rodzaju problemy z uczciwością. Jak zmusić EF, aby zapisywał tylko jednostkę, którą chcę zapisać, a zatem ignorować wszystkie obiekty podrzędne?

Jeśli ręcznie ustawię właściwości na wartość null, pojawia się błąd „Operacja nie powiodła się: nie można zmienić relacji, ponieważ co najmniej jedna z właściwości klucza obcego nie dopuszcza wartości null”. Jest to wyjątkowo nieproduktywne, ponieważ ustawiłem obiekt podrzędny na null specjalnie, aby EF zostawił go w spokoju.

Dlaczego nie chcę zapisywać / wstawiać obiektów podrzędnych?

Ponieważ jest to omawiane w kółko w komentarzach, podam pewne uzasadnienie, dlaczego chcę, aby moje przedmioty dziecięce zostały pozostawione same.

W aplikacji, którą buduję, model obiektów EF nie jest ładowany z bazy danych, ale używany jako obiekty danych, które wypełniam podczas analizowania pliku płaskiego. W przypadku obiektów podrzędnych wiele z nich odwołuje się do tabel przeglądowych definiujących różne właściwości tabeli nadrzędnej. Na przykład położenie geograficzne jednostki podstawowej.

Ponieważ sam wypełniłem te obiekty, EF zakłada, że ​​są to nowe obiekty i należy je wstawić wraz z obiektem nadrzędnym. Jednak te definicje już istnieją i nie chcę tworzyć duplikatów w bazie danych. Używam obiektu EF tylko do wyszukiwania i wypełniania klucza obcego w mojej głównej encji tabeli.

Nawet w przypadku obiektów podrzędnych, które są prawdziwymi danymi, muszę najpierw zapisać rodzica i uzyskać klucz podstawowy lub EF po prostu robi bałagan. Mam nadzieję, że to daje pewne wyjaśnienie.

Mark Micallef
źródło
O ile wiem, będziesz musiał anulować obiekty podrzędne.
Johan,
Cześć Johan. Nie działa. Zgłasza błędy, jeśli null kolekcję. W zależności od tego, jak to robię, narzeka, że ​​klucze są puste lub że moja kolekcja została zmodyfikowana. Oczywiście to prawda, ale zrobiłem to celowo, aby zostawić w spokoju przedmioty, których nie powinien dotykać.
Mark Micallef
Euforyczny, to zupełnie nieprzydatne.
Mark Micallef
@Euphoric Nawet jeśli nie zmieniasz obiektów podrzędnych, EF nadal próbuje wstawić je domyślnie i nie ignoruje ich ani nie aktualizuje.
Johan,
To, co naprawdę mnie denerwuje, to to, że jeśli robię wszystko, co w mojej mocy, aby faktycznie usunąć te obiekty, to narzeka, zamiast zdawać sobie sprawę, że chcę, aby zostawił je w spokoju. Ponieważ wszystkie te obiekty podrzędne są opcjonalne (dopuszczają wartość null w bazie danych), czy istnieje sposób, aby zmusić EF do zapomnienia, że ​​mam te obiekty? czyli jakoś wyczyścić jego kontekst lub pamięć podręczną?
Mark Micallef

Odpowiedzi:

56

O ile wiem, masz dwie możliwości.

Opcja 1)

Null wszystkie obiekty podrzędne, dzięki temu EF niczego nie doda. Nie usunie również niczego z Twojej bazy danych.

Opcja 2)

Ustaw obiekty podrzędne jako odłączone od kontekstu przy użyciu następującego kodu

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

Pamiętaj, że nie możesz odłączyć List/ Collection. Będziesz musiał zapętlić listę i odłączyć każdy element z listy w ten sposób

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}
Johan
źródło
Cześć Johan, próbowałem oddzielić jedną z kolekcji i spowodował następujący błąd: Typ jednostki HashSet`1 nie jest częścią modelu dla bieżącego kontekstu.
Mark Micallef
@MarkyMark Nie odłączaj kolekcji. Będziesz musiał zapętlić kolekcję i odłączyć ją od obiektu dla obiektu (zaktualizuję teraz moją odpowiedź).
Johan
11
Niestety, nawet z listą pętli do odrywania wszystkiego, EF wydaje się nadal próbować wstawić do niektórych powiązanych tabel. W tym momencie jestem gotowy do wyrzucenia EF i powrotu do SQL, który przynajmniej zachowuje się rozsądnie. Co za ból.
Mark Micallef
2
Czy mogę użyć context.Entry (yourObject) .State jeszcze przed dodaniem go?
Thomas Klammer
1
@ mirind4 Nie użyłem stanu. Jeśli wstawię obiekt, upewniam się, że wszystkie elementy potomne są zerowe. Po aktualizacji otrzymuję obiekt jako pierwszy bez dzieci.
Thomas Klammer
41

Krótko mówiąc: użyj klucza obcego, a zaoszczędzi ci to dnia.

Załóżmy, że masz jednostkę szkolną i miejską. Jest to relacja typu „wiele do jednego”, w której miasto ma wiele szkół, a szkoła należy do miasta. I załóżmy, że miasta już istnieją w tabeli przeglądowej, więc NIE chcesz, aby były one ponownie wstawiane podczas wstawiania nowej szkoły.

Początkowo możesz zdefiniować swoje jednostki w ten sposób:

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; }
}

I możesz zrobić wstawienie Szkoła w ten sposób (załóżmy, że masz już właściwość City przypisaną do 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;
    }
}

Powyższe podejście może się sprawdzić w tym przypadku, jednak wolę podejście klucza obcego, które jest dla mnie bardziej przejrzyste i elastyczne. Zobacz zaktualizowane rozwiązanie poniżej:

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; }
}

W ten sposób wyraźnie definiujesz, że Szkoła ma klucz obcy City_Id i odnosi się do jednostki City . Więc jeśli chodzi o wstawianie szkoły , możesz:

    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;
        }
    }

W takim przypadku jawnie określasz City_Id nowego rekordu i usuwasz miasto z wykresu, aby EF nie zawracał sobie głowy dodawaniem go do kontekstu wraz ze szkołą .

Chociaż na pierwszy rzut oka podejście klucza obcego wydaje się bardziej skomplikowane, ale uwierz mi, ta mentalność pozwoli Ci zaoszczędzić dużo czasu, jeśli chodzi o wstawianie relacji wiele-do-wielu (wyobrażając sobie, że masz relację Szkoła i Uczeń, a ma właściwość City) i tak dalej.

Mam nadzieję, że to ci pomoże.

lenglei
źródło
Świetna odpowiedź, bardzo mi pomogła! Ale czy nie byłoby lepiej określić minimalną wartość 1 dla City_Id za pomocą [Range(1, int.MaxValue)]atrybutu?
Dan Rayson
Jak we śnie!! Stukrotne dzięki!
CJH
Spowoduje to usunięcie wartości z obiektu School w obiekcie wywołującym. Czy SaveChanges ponownie ładuje wartość null właściwości nawigacji? W przeciwnym razie obiekt wywołujący powinien ponownie załadować obiekt City po wywołaniu metody Insert (), jeśli potrzebuje tych informacji. To jest wzór, którego często używałem, ale nadal jestem otwarty na lepsze wzory, jeśli ktoś ma dobry.
Krzywy
1
To było dla mnie bardzo pomocne. EntityState.Unchaged jest dokładnie tym, czego potrzebowałem, aby przypisać obiekt, który reprezentuje klucz obcy tabeli odnośników na dużym wykresie obiektów, który zapisałem jako pojedynczą transakcję. EF Core zgłasza mniej niż intuicyjny komunikat o błędzie IMO. Zapisałem w pamięci podręcznej tabele wyszukiwania, które rzadko zmieniają się ze względu na wydajność. Zakładasz, że ponieważ PK obiektu tabeli przeglądowej jest taka sama, jak to, co jest już w bazie danych, wie, że jest niezmieniony i po prostu przypisuje FK do istniejącego elementu. Zamiast próbuje wstawić obiekt tabeli przeglądowej jako nowy.
tnk479
Żadna kombinacja zerowania lub ustawienia stanu na niezmieniony nie działa dla mnie. EF nalega, aby wstawić nowy rekord podrzędny, gdy chcę tylko zaktualizować pole skalarne w rodzicu.
BobRz
21

Jeśli chcesz tylko zapisać zmiany w obiekcie nadrzędnym i uniknąć zapisywania zmian w jakimkolwiek z jego obiektów podrzędnych , dlaczego nie wykonać następujących czynności:

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

Pierwsza linia dołącza obiekt nadrzędny i cały wykres zależnych od niego obiektów potomnych do kontekstu w Unchangedstanie.

Druga linia zmienia stan tylko dla obiektu nadrzędnego, pozostawiając jego dzieci w Unchangedstanie.

Zauważ, że używam nowo utworzonego kontekstu, więc pozwala to uniknąć zapisywania jakichkolwiek innych zmian w bazie danych.

keykey76
źródło
2
Ta odpowiedź ma tę zaletę, że jeśli ktoś przyjdzie później i doda obiekt podrzędny, nie złamie istniejącego kodu. Jest to rozwiązanie typu „opt-in”, w którym inne wymagają wyraźnego wykluczenia obiektów podrzędnych.
Jim
To już nie działa w rdzeniu. „Dołącz: dołącza każdą osiągalną jednostkę, z wyjątkiem sytuacji, gdy osiągalna jednostka ma klucz wygenerowany przez sklep i nie jest przypisana żadna wartość klucza; zostaną one oznaczone jako dodane”. Jeśli dzieci również są nowe, zostaną dodane.
mmix
14

Jednym z sugerowanych rozwiązań jest przypisanie właściwości nawigacji z tego samego kontekstu bazy danych. W tym rozwiązaniu właściwość nawigacji przypisana spoza kontekstu bazy danych zostanie zastąpiona. Zobacz poniższy przykład dla ilustracji.

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;}
}

Zapisywanie do bazy danych:

 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 dodaje to jako funkcję, jednak wydaje mi się to denerwujące. Jeśli obiekt działu powiązany z obiektem firmy ma identyfikator, który już istnieje w bazie danych, to dlaczego EF po prostu nie kojarzy obiektu firmy z obiektem bazy danych? Dlaczego powinniśmy sami zadbać o stowarzyszenie? Dbanie o właściwość nawigacji podczas dodawania nowego obiektu jest czymś w rodzaju przenoszenia operacji bazy danych z SQL do C #, co jest uciążliwe dla programistów.

Bikash Bishwokarma
źródło
Zgadzam się w 100%, to śmieszne, że EF próbuje utworzyć nowy rekord, gdy zostanie podany identyfikator. Gdyby istniał sposób na wypełnienie opcji listy wyboru rzeczywistym obiektem, bylibyśmy w biznesie.
T3.0
11

Najpierw musisz wiedzieć, że istnieją dwa sposoby aktualizowania jednostki w EF.

  • Dołączone przedmioty

W przypadku zmiany relacji obiektów dołączonych do kontekstu obiektu przy użyciu jednej z metod opisanych powyżej Entity Framework musi zachować synchronizację kluczy obcych, odwołań i kolekcji.

  • Odłączone obiekty

Jeśli pracujesz z odłączonymi obiektami, musisz ręcznie zarządzać synchronizacją.

W aplikacji, którą buduję, model obiektów EF nie jest ładowany z bazy danych, ale używany jako obiekty danych, które wypełniam podczas analizowania pliku płaskiego.

Oznacza to, że pracujesz z odłączonym obiektem, ale nie jest jasne, czy używasz skojarzenia niezależnego, czy skojarzenia z kluczem obcym .

  • Dodaj

    Podczas dodawania nowej jednostki z istniejącym obiektem podrzędnym (obiektem, który istnieje w bazie danych), jeśli obiekt podrzędny nie jest śledzony przez EF, obiekt podrzędny zostanie ponownie wstawiony. Chyba że najpierw ręcznie dołączysz obiekt podrzędny.

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Aktualizacja

    Możesz po prostu oznaczyć jednostkę jako zmodyfikowaną, wtedy wszystkie właściwości skalarne zostaną zaktualizowane, a właściwości nawigacji zostaną po prostu zignorowane.

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

Graph Diff

Jeśli chcesz uprościć kod podczas pracy z odłączonym obiektem, możesz spróbować wyświetlić bibliotekę różnic grafowych .

Oto wprowadzenie, wprowadzenie GraphDiff dla Entity Framework Code First - zezwalanie na automatyczne aktualizacje wykresu odłączonych jednostek .

Przykładowy kod

  • Wstaw jednostkę, jeśli nie istnieje, w przeciwnym razie zaktualizuj.

      db.UpdateGraph(entity);
  • Wstaw jednostkę, jeśli nie istnieje, w przeciwnym razie zaktualizuj i wstaw obiekt podrzędny, jeśli nie istnieje, w przeciwnym razie zaktualizuj.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
Yuliam Chandra
źródło
3

Najlepszym sposobem na to jest zastąpienie funkcji SaveChanges w kontekście danych.

    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();
    }
Wouter Schut
źródło
2

Mam ten sam problem, gdy próbuję zapisać profil, mam już powitanie stolika i nowy, aby utworzyć profil. Kiedy wstawiam profil, wstawiam również do pozdrowienia. Więc próbowałem w ten sposób przed savechanges ().

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;

ZweHtatNaing
źródło
1

To zadziałało dla mnie:

// 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;
Klaus
źródło
0

To, co zrobiliśmy, to przed dodaniem elementu nadrzędnego do zestawu dbset, odłączenie kolekcji podrzędnej od elementu nadrzędnego, upewniając się, że istniejące kolekcje są przenoszone do innych zmiennych, aby umożliwić późniejszą pracę z nimi, a następnie zastępując bieżące kolekcje podrzędne nowymi, pustymi kolekcjami. Wydawało się, że ustawienie kolekcji podrzędnych na null / nic nie działa. Po wykonaniu tej czynności dodaj rodzica do zestawu dbset. W ten sposób dzieci nie są dodawane, dopóki tego nie chcesz.

Shaggie
źródło
0

Wiem, że to stary post, ale jeśli używasz podejścia opartego na kodzie, możesz osiągnąć pożądany rezultat, używając następującego kodu w pliku mapowania.

Ignore(parentObject => parentObject.ChildObjectOrCollection);

Po prostu poinformuje EF, aby wykluczył właściwość „ChildObjectOrCollection” z modelu, aby nie była mapowana do bazy danych.

Syed Danish
źródło
W jakim kontekście używane jest słowo „Ignoruj”? Wydaje się, że nie istnieje w przypuszczalnym kontekście.
T3.0
0

Miałem podobne wyzwanie, używając Entity Framework Core 3.1.0, moja logika repozytorium jest dość ogólna.

To zadziałało dla mnie:

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

Należy pamiętać, że „ParentEntityId” to nazwa kolumny klucza obcego w jednostce podrzędnej. Dodałem wyżej wymienioną linię kodu w tej metodzie:

protected override void OnModelCreating(ModelBuilder builder)...
Manelisi Nodada
źródło