Laden...

EF: Self-Tracking Entities // ChangeTracking trackt Initialisierung eines neuen Objekts

Erstellt von mctimotheus vor 13 Jahren Letzter Beitrag vor 13 Jahren 4.385 Views
M
mctimotheus Themenstarter:in
179 Beiträge seit 2008
vor 13 Jahren
EF: Self-Tracking Entities // ChangeTracking trackt Initialisierung eines neuen Objekts

verwendetes Datenbanksystem: SQL Compact 3.5 // Entity Framework 4.0

Hallo Community,

ich weiß, der Beitragstitel ist alles und nichts sagend 😉. Ich habe ein Problem mit den Self-Tracking Entities und versuch dieses jetzt mal so gut ich kann zu beschreiben.

Ich erzeuge mir eine neue Instanz einer Entität und starte das Tracking:


User user = new User() { FirstName = "Hans", Surname = "Wurst" };
user.StartTracking();

Soweit klar. Nun möchte ich die alte User-Instanz mit einer neuen Instanz überschreiben und anschließend in meiner Datenbank speichern:


user = new User() { FirstName = "Caroline", Surname = "Semmel" };
using(UserService service = new UserService())
{
    user.Persist(user);
}

Meine Persist-Methode sieht wie folgt aus:


public void Persist(User user)
{
    if(user != null)
    {
        DbContext ctx = new DbContext();
        ctx.Users.ApplyChanges(user);
        ctx.SaveChanges();
    }
}

Nun finde ich in meiner Datenbank zwei Einträge:


ID    FirstName    Surname
--------------------------
#1    Hans         Wurst
#2    Caroline     Semmel

Ich erwarte aber eigentlich, dass nur die Caroline Semmel in meiner Datenbank auftaucht, den Hans Wurst habe ich doch überschreiben?!

Kann mir diesen Umstand jemand erklären? Ich finde leider keine passende Lösung weil ich mir nicht erklären kann, wieso das alte Objekt auch gespeichert wird. Liegt das am ChangeTracking?

Liebe Grüße,

MCT

Wer nicht wagt .. der nicht gewinnt .. !

F
10.010 Beiträge seit 2004
vor 13 Jahren

Wo bitte hast du den Hans Wurst überschrieben?
Du hast hast mit dem New (..) einen weiteren Eintrag angelegt, nicht mehr und nicht weniger.

Willst du Hans Wurst überschreiben musst du das auch in dem Object tun.

M
mctimotheus Themenstarter:in
179 Beiträge seit 2008
vor 13 Jahren

Weißt meinst du genau mit "im Objekt tun". Ich seh den Fehler leider immer noch nicht. Aber toll, dass du schonmal eine Spur gefunden hast, das beruhigt mich 😃

Wer nicht wagt .. der nicht gewinnt .. !

3.430 Beiträge seit 2007
vor 13 Jahren

Hallo,

wie FZelle gesagt hat überschreibst du den User nicht, sondern erstellst nur ein neues Objekt.
D.h. die user Variable zeigt dann auf den neuen User, aber den Alten gibt es immer noch.
Alle Änderungen die du danach machst werden am neuen User getätigt.

Wenn du den existierenden User ändern willst dann darfst du nicht einen neuen erstellen sondern einfach die Eigenschaften umändern. (user.Name = "blabla";)

Gruss
Michael

M
mctimotheus Themenstarter:in
179 Beiträge seit 2008
vor 13 Jahren

Soweit erstmal danke, das Problem habe ich jetzt verstanden.

Allerdings müsste ich den user tatsächlich durch eine neue Instanz ersetzen und die alte Instanz somit verwerfen. Gibt es hierfür einen sinnvollen Workaround?

Wer nicht wagt .. der nicht gewinnt .. !

1.552 Beiträge seit 2010
vor 13 Jahren

Hallo mctimotheus,

du kannst entweder den User komplett aus der Datenbank entfernen oder du machst es wie michlG sagte durch das einzelne Umändern der Properties. Wobei ich aber ersteren Weg bevorzugen würde. Ich glaube kaum dass der User nur so alleine ohne weiteren Tabellenreferenzen in der Datenbank steht, also wird zweiterer Vorschlag aus folgender Überlegung nicht zutreffen können
User1 macht Ding1, Ding2, Ding3. Somit ist es unlogisch User1 auf User2 zu ändern, denn dann hat User2 auf einmal Ding1, Ding2 und Ding3 gemacht und der User1 kommt der Gefängnisstrafe davon 😁

Gruß
Michael

Mein Blog
Meine WPF-Druckbibliothek: auf Wordpress, myCSharp

16.834 Beiträge seit 2008
vor 13 Jahren

Ich mach was ähnliches - nur kann ich aufgrund von Relationen keine Einträge entfernen, sondern muss diese wirklich updaten.


        // Repository-Function!
        public Entity Update( Entity r )
        {
            Entity dbItem = this.DatabaseScope.EntitySet
                .SingleOrDefault( i => i.ID.Equals( r.ID) );

            dbItem.SetAllModified( this.DatabaseScope );
            dbItem.UpdateAllProperties( this.DatabaseScope, r );

            this.DatabaseScope.SaveChanges();
        }

Die dazugehörigen Helper:



        public static void SetAllModified<T>( this T entity, ObjectContext context )
where T : IEntityWithKey
        {
            var stateEntry = context.ObjectStateManager.GetObjectStateEntry( entity.EntityKey );
            var propertyNameList = stateEntry.CurrentValues.DataRecordInfo.FieldMetadata.Select
              ( pn => pn.FieldType.Name );
            foreach ( var propName in propertyNameList )
                stateEntry.SetModifiedProperty( propName );
        }

        public static void UpdateAllProperties<T>( this T entity, ObjectContext context, T updatedItem )
            where T : IEntityWithKey
        {
            var stateEntry = context.ObjectStateManager.GetObjectStateEntry( entity.EntityKey );
            stateEntry.ApplyCurrentValues( updatedItem );
        }
    }

Wirkung:

Ich setze zuerst den State aller Properties auf modified, übertrage anschließend alle Werte des Templates auf die Datenbank-Entität. Da die ID identisch ist, erfolgt ein SQL-Update-Befehl.
Natürlich kannst Du hier noch eine Optimierung durchführen, um den SQL-Overhead zu entfernen und nur die Objekte aktualisieren, die wirklich eine Änderung nötig haben.

Sollte man vielleicht in die Snippets übernehmen, da das Entity-Update eines kompletten Objekts schon öfter gefragt wurde und das EF selbst dafür keine Funktion bereitstellt.

M
mctimotheus Themenstarter:in
179 Beiträge seit 2008
vor 13 Jahren

Leider hilft mir das nicht bei meinem Problem 🙁

Ich machs mal noch ein bisschen konkreter:

Ich habe eine Maske die an ein Day-Objekt gebunden ist. Das Day-Objekt hat verschiedene Properties, so auch ein Date.

In meiner Maske gibt es einen DatePicker welcher an das Property Date gebunden ist.
Immer wenn ich ein neues Datum auswähle wird die set-Methode des Date-Propertys aufgerufen und hier prüfe ich folgendes:

Gibt es bereits einen Tag für das gewählte Datum?
Ja: Lade den Tag und weise ihn dem gebunden Tag zu
Nein: Instanziiere einen neuen Tag und weise diesen dem gebunden Tag zu

In "Code" sieht das so aus:



private Day _day;

public Constructor()
{
    using (IServiceDay serviceDay = ServiceDayFactory.Create())
    {
        if (serviceDay.Exists(DateTime.Now))
            this._day = serviceDay.SelectByDate(DateTime.Now);
        else
            this._day = serviceDay.NewDay(); // new Day()
    }
}


// An die Maske gebundenes Datum des Tages
public DateTime Date
{
    get { return this._day.Date; }
    set
    {
        using (IServiceDay serviceDay = ServiceDayFactory.Create())
        {
            if (serviceDay.Exists(value))
                this._day = serviceDay.SelectByDate(value);
            else
            {
                this._day = serviceDay.NewDay(); // new Day()
                this._day.Date = value;
            }
        }
        // set all binded properties to "changed"
        this.OnPropertyChanged(string.Empty);
    }
}

public string feeling { ... }

public string notes { ... }

Vielleicht habt ihr mir ja oben schon die richtige Lösung genannt und ich kann sie nicht auf mein Problem umbrechen. Könnt ihr mir nochmal einen Rat geben?

Liebe Grüße und Danke !

Wer nicht wagt .. der nicht gewinnt .. !

1.378 Beiträge seit 2006
vor 13 Jahren

Ich sehe den Zusammenhang zu deinem obigen Problem hier nicht. Was genau willst du erreichen und was daran funktioniert nicht?

Lg XXX

//EDIT: Dein Beispiel schaut ja ganz ok aus. Wenn dein Day in der DB noch nicht vorhanden ist, legst du ein neues an. Wo genau liegt jetzt das Problem?

M
mctimotheus Themenstarter:in
179 Beiträge seit 2008
vor 13 Jahren

Ich sehe den Zusammenhang zu deinem obigen Problem hier nicht. Was genau willst du erreichen und was daran funktioniert nicht?

Immer wenn ich ein neues Datum in meiner Maske auswähle wird geschaut, ob es bereits einen Eintrag mit dem gewählten Datum in der Datenbank gibt, falls ja, wird dieser geladen und dem gebundenen Day-Objekt zugeordnet, wenn nicht wird ein neue Instanz erzeugt und an das gebundene Day-Objekt gebunden. Das ist ja das anfangs beschriebene Problem.

Soweit klar?

Wer nicht wagt .. der nicht gewinnt .. !

1.378 Beiträge seit 2006
vor 13 Jahren

Trotzdem könntest du auch gleich dazu schreiben was du erreichen willst und was daran nicht funktioniert.

Wenn es sich also um das selbe Problem wie oben handelt, heißt das, das du beim Ändern des Datums kein neues Day Objekt anlegen willst sondern das bestehende Ändern? Na dann erzeug kein neues Dayobjekt sondern veränder das bestehende.

Wenn es sich um ein neues Objekt handelt aber das alte überflüssig ist, musst du das alte halt bei der Datumsänderung löschen.

Ansonsten gibts eh nur die dritte Variante, die du eh bereits umgesetzt hast, dass du beim Datumswechsel ständig einen neue Dayeintrag anlegst sofern es ihn noch nicht gibt.

Entweder so oder ich hab dein Problem nach wie vor nicht verstanden.

Lg XXX

M
mctimotheus Themenstarter:in
179 Beiträge seit 2008
vor 13 Jahren

Ansonsten gibts eh nur die dritte Variante, die du eh bereits umgesetzt hast, dass du beim Datumswechsel ständig einen neue Dayeintrag anlegst sofern es ihn noch nicht gibt.

Genau das möchte ich ja erreichen nur immer wenn ich ein new auf das gebundene Objekt mache wird das nicht überschrieben. Wenn ich später die Änderungen am Objekt mit "SaveChanges" in die Datenbank zurück schreiben will erscheinen mehrere Tage in der Datenbank, obqohl nur einer dort erscheinen sollte. Das Objekt wird also mit new nicht richtig überschrieben sondern es wird ein zusätzliches angelegt?!

Wer nicht wagt .. der nicht gewinnt .. !

1.378 Beiträge seit 2006
vor 13 Jahren

Ich versteh nicht wo es da Verständnisprobleme geben kann:

Wenn du ein Blatt Papier beschreibst, dann ein neues Blatt her nimmst und auf dem schreibst, hast du danach zwei Blätter wo was drauf steht. Wenn du das nicht willst, musst du entweder das erste zerknüllen und wegwerfen oder statt einem neuen Blatt das alte wiederverwerten(Text ausradieren oder irgendwie drüber malen).

Was du machst ist ein zweites Blatt herzunehmen und erwarten das dann nur ein Blatt existiert. Aber das erste löst sich nicht von alleine in Luft auf.

Die 2 Möglichen Varianten:


public DateTime Date
{
    get { return this._day.Date; }
    set
    {
        this._day.Date = value;

        // set all binded properties to "changed"
        this.OnPropertyChanged(string.Empty);
    }
}


public DateTime Date
{
    get { return this._day.Date; }
    set
    {
        using (IServiceDay serviceDay = ServiceDayFactory.Create())
        {
            if (serviceDay.Exists(value))
                this._day = serviceDay.SelectByDate(value);
            else
            {
                serviceDay.DeleteDay(this._day); //delete old day

                this._day = serviceDay.NewDay(); // new Day()
                this._day.Date = value;
            }
        }
        // set all binded properties to "changed"
        this.OnPropertyChanged(string.Empty);
    }
}

Lg XXX

M
mctimotheus Themenstarter:in
179 Beiträge seit 2008
vor 13 Jahren

Wenn du das nicht willst, musst du entweder das erste zerknüllen und wegwerfen

Genau das ist das was ich will 👍

Dein Codevorschlag sieht auch verdammt sinnig aus, das werd ich gleich mal ausprobieren freu

Danke für deine Geduld, ich hoffe das Problem ist damit gelöst.

Wer nicht wagt .. der nicht gewinnt .. !

M
mctimotheus Themenstarter:in
179 Beiträge seit 2008
vor 13 Jahren

Hallo,

leider habe ich die Lösung von xxxprod erst jetzt richtig testen können und dabei ist mir aufgefallen, dass dies leider noch nicht der Weg zum Ziel ist.

  
if (serviceDay.Exists(value))  
   this._day = serviceDay.SelectByDate(value);  
else  
{  
   serviceDay.DeleteDay(this._day); //delete old day  
  
   this._day = serviceDay.NewDay(); // new Day()  
   this._day.Date = value;  
}  

Ich kann ja das Objekt noch nicht in der Datenbank löschen, weil es ja noch garnicht da ist. Ein SaveChanges() des Entity Frameworks ist ja noch nicht erfolgt.

Gibt es denn keine Möglichkeit die Instanz dieses Objektes einfach zu überspielen oder den ChangeTracker zurück zu setzen um die bisherigen Änderungen zu vergessen? Das Updaten der einzelnen Properties kommt nicht in Frage, da es sich ja um ein völlig neues Objekt handelt.

Vielleicht hat ja jemand noch eine Idee?!

Wer nicht wagt .. der nicht gewinnt .. !

1.378 Beiträge seit 2006
vor 13 Jahren

Wenn es noch nicht in der Datenbank existiert kannst du es Detachen aber das musst du quasi selbst prüfen ob die Entity neu ist(dann detachen) oder bereits in der Datenbank ist(dann löschen).

Lg XXX