Laden...

Entity Framework: Wie Entity "Inserten" wenn diese eine Relation enthält.

Erstellt von TheShihan vor 14 Jahren Letzter Beitrag vor 12 Jahren 15.417 Views
TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren
Entity Framework: Wie Entity "Inserten" wenn diese eine Relation enthält.

verwendetes Datenbanksystem: MSSQl 2008

Hallo,

Ich versuche mich mal wieder mit dem Enitity Framework. Normale Selects und Updates klappen bisherer ohne grössere Probleme. Auch das Erstellen und Einfügen von neuen Objekten in die DB hatte bisher geklappt. Nun habe ich aber den Fall, dass bei einem Enitity eine Relation zu einem anderen Objekt/Tabelle besteht und beim Einfügen verursacht mir das nun grösste Probleme.

Ich habe eine Entity "User", ein User kann eine Anrede haben, diese Anreden sind in der Tabelle "Titles", es gibt also ein Entity "Title". (Die übersicht über diese Entitys habe ich als Bild angehängt).

Beim erstellen eines neuen Entities des Typs User gehe ich bis jetzt wie folgt vor:

  • Ich erstelle mir ein Objekt User:
 User user = new User();

Danach fülle ich das Objekte mit den gewünschten Werten:

user.City = this.txtCity.Text;
user.CountryIso = this.txtCountry.Text;
user.Email = this.txtEmail.Text;
user.Firstname = this.txtFirstname.Text;
user.Password = this.txtPassword.Text;
user.Street = this.txtStreet.Text;
user..Surname = this.txtSurname.Text;
user.Zip = this.txtZip.Text;

Dann ermittle ich welche Anrede der User hat, und füge sie dem User hinzu:

 user.Title = da.GetTitleById(((GenericIntStringItem)cbTitles.SelectedItem).IntVal);

Die Methode ist wie folgt implementiert:

 /// <summary>
/// Get a title by a given title ID.
/// </summary>
/// <returns>The title or null when nothing found</returns>
public Title GetTitleById(int titleId)
{
var title = from t in this.context.Titles
where t.TitleId == titleId
select t;

Title titleEntity = (Title)title.First();

// detach from context
this.context.Detach(titleEntity);

return titleEntity;
}

Hier muss ich anmerken, dass ich zuerst das "Title" objekt nicht vom Objekt context "detached" hatte, dann wurde aber speichern des Users gemekert, dass das Objekt bereits einem anderen Context zugeordnet war (damit war "Title" gemeint). Ob das hier eine schlaue Lösung ist weiss ich nicht... jedenfalls:

Versuche ich noch den User wie folgt zu speichern:

 /// <summary>
/// Creates a given user account
/// </summary>
/// <param name="user">The customer that shall be inserted into the DB</param>
public int CreateUser(User user)
{
this.context.AddToUsers(user);
this.context.SaveChanges();

return user.UserId;
}

Das schmeisst mir aber eine Exception (aber nur wenn ich dem User einen Title zugeordnet habe, wenn Title == null, dann geht das speichern):

{System.InvalidOperationException: The object cannot be added to the ObjectStateManager because it already has an EntityKey. 

Klar, mir ist das schon irgendwie logisch, der Title existiert ja schon, hat ja schon eine TitleId, ich möche ja aber nicht einen neuen Title hinzufügen sondern nur den User beim erstellen gleichzeitig mit einem bestehenden Title verknüpfen. Wenn ich in der CreateUser-Methode vor "AddToUser" zuerst den Title Attache, dann klappt das auch nicht:

 /// <summary>
/// Creates a given user account
/// </summary>
/// <param name="user">The customer that shall be inserted into the DB</param>
public int CreateUser(User user)
{
if (user.Title != null)
{
this.context.Attach(user.Title);
}
this.context.AddToUsers(user);
this.context.SaveChanges();

return user.UserId;
}

Dann meldet er mir:

"An object with the same key already exists in the ObjectStateManager. The existing object is in the Unchanged state. An object can only be added to the ObjectStateManager again if it is in the added state."

Weiss einer was ich hier falsch mache? Oder ist generell das Vorgehen falsch und ich sollte beim User-Entity einfach das Property für "TitleId" einfügen und dann dieses mit der gewünschten Id befüllen vor dem speichern und nicht über diese Relationen arbeiten?

Gruss, Shi

Build something that's idiot proof, and they'll build a better idiot

5.299 Beiträge seit 2008
vor 14 Jahren

sieht iwie urs kompliziert aus. du kannst doch die Combo an die Table<Title> binden, und den titel so zuweisen


user.title = (Title)cbTitles.SelectedValue;

Der frühe Apfel fängt den Wurm.

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

Erm.. sorry, aber hast du meinen Text durchgelesen?

A) die Zuweisung ist nicht das Problem
B) Du castest hier einen Value (der wohl meistens Integer ist) zu einem Entity
C) Irgendwie habe ich das gefühl, wenn ich auch auf die Uhrzeit schaue, du bist betrunken 😃

Build something that's idiot proof, and they'll build a better idiot

5.299 Beiträge seit 2008
vor 14 Jahren

Ach ja, mitte Entities muß man das Objekt selbst zum ValueMember machen. Alsso muß die Entity eine Property bekommen, die sie selbst zurückgibt (ich nenn die immer "Self"). Combobox-Databinding Über die ID geht bei Entities nicht.
Du kannst auch die ObjectBindingSource verwenden, die stellt einen "Self"-PropertyDescriptor zur Verfügung, an den gebunden werden kann, und der die Entity selbst zurückgibt.

Deine Zuweisung ist insofern problematisch, daß du iwas detachen tust, das ist n. erf..

Der frühe Apfel fängt den Wurm.

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

Danke, aber es geht hier nicht über das ComboBoxBinding.

Wie auch immer, soweit ich bis jetzt gelesen habe, ist das ein Design-Flaw im Entity Framework 1.0, es gibt aber einige Workarrounds:

http://bernhardelbl.spaces.live.com/blog/cns!DB54AE2C5D84DB78!238.entry

Ich hoffe das ist in der Version 4.0, (VS2010) dann besser gelöst).

Build something that's idiot proof, and they'll build a better idiot

5.299 Beiträge seit 2008
vor 14 Jahren

Das ist ne ziemlich große Site, ich hab nix gefunden, wassich mit deinem Post in Verbindung bringen kann - kannst du mir eine Überschrift nennen?

Der frühe Apfel fängt den Wurm.

C
282 Beiträge seit 2008
vor 14 Jahren

Ich habe mir das Ganze jetzt schon mehrmals durchgelesen und irgendwie wirkt das etwas chaotisch. Also zu viel Aufwand, für den kleinen Nutzen.

Wenn ich es richtig verstanden habe, geht es nur darum einen User einen bestimmten Titel zuzuweisen. Warum ist die UserId und TitelId dann verbunden? Also grundsätzlich sollte doch im User eine TitleId vorhanden sein, die dann die Verbindung zum Titel herstellt, also eine Relation. Mal davon abgesehen, ob es nicht gleich Sinn macht, alle Zuweisungen an eine BindingSource zu binden. Gerade mit den Entities sind ja die Relations total einfach, wie ich gerade festellen durfte.

5.299 Beiträge seit 2008
vor 14 Jahren

ich blicks auch grad nicht. Wassich verstanden habe (offensichtlich falsch) ist, dass TheShihan einen neuen User einfügen will, der einen schon vorhandenen Titel bekommen soll.
Ich würde die Titel ja an eine Combo binden, und den dort angewählten Titel dem User zuweisen.
Aber laut TheShihan geht es weder ums Binding an die Combobox, noch um die Zuweisung.
Aber das Adden eines Users geht auch nicht, jedenfalls nicht, wenner ihn mit einem detacheten Titel ausgerüstet hat.
Naja - jetzt hatterja einen Workaround aufgetan - aber den versteh ich nicht, wo der eiglich ist.

Der frühe Apfel fängt den Wurm.

3.003 Beiträge seit 2006
vor 14 Jahren

a) du verwendest in GetTitleById eine Variable context - das deutet für mich auf eine Membervariable hin. Schlechte Idee für 'nen Kontext.

b) Wozu um alles in der Welt detachst du den gefundenen Title? Ich meine, die Fehlermeldung ist ziemlich eindeutig, nicht? Da der Title detached ist, versucht er ihn neu anzulegen, was nicht funktioniert, da er schon vorhanden ist.

Ich meine:


using(var context  = new MyUserContext())
{
  var user = ... //neuen User erstellen
  var intVal = ((GenericIntStringItem)cbTitles.SelectedItem).IntVal;
  user.Title = context.Titles.FirstOrDefault(p => p.TitleId == intVal);
  context.AddToUsers(user);
  context.SaveChanges();
}


Ist ja nun keine Raketenwissenschaft 😉

LaTino
Solche Seiten wie die von dir verlinkte machen mich echt sauer. EF nicht verstehen, infolgedessen falsch anwenden, dabei Fehler produzieren, und die durch Unmengen Code wieder reparieren - und DANN dem armen EF den ganzen Mist in die Schuhe schieben....grrrr.

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

@LaTino:

Ok, ich probiere das mal so aus, wie du das beschrieben hast, danke.

Btw, wegen dem Context. Unser ASP.NET-Dozent hatte uns das mal so gezeigt. das wir das etwas so machen sollen:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DataAccess
{
    /// <summary>
    /// Handle data access
    /// </summary>
    public class DataAccessHandler : IDisposable
    {

        #region Fields

        /// <summary>
        /// The context (Entity Model)
        /// </summary>
        MediathekEntities context = new MediathekEntities();

        #endregion
		
		...		

        /// <summary>
        /// Get a list of all avaiable titles a customer can have.
        /// (Ordered by description)
        /// </summary>
        /// <returns>List with titles</returns>
        public IList<Title> GetTitles()
        {
            var titles = from t in this.context.Titles
                         orderby t.Description
                         select t;

            return titles.ToList();
        }

        /// <summary>
        /// Get a title by a given title ID.
        /// </summary>
        /// <returns>The title or null when nothing found</returns>
        public Title GetTitleById(int titleId)
        {
            var title = from t in this.context.Titles
                         where t.TitleId == titleId
                         select t;

            return (Title)title.First();
        }
		
		...

        #region IDisposable Members

        /// <summary>
        /// Dipose this object
        /// </summary>
        public void Dispose()
        {
            if (context != null)
            {
                context.Dispose();
            }
        }

        #endregion
    }
}

Und dann können wir das natürlich in einem "Using" aufrufen, damit der context wieder sauber entfernt wird:

        /// <summary>
        /// Delete an administrator account
        /// </summary>
        /// <param name="adminId">The ID of the administrator account</param>
        public void DeleteAdministrator(int adminId)
        {
            using (DataAccessHandler da = new DataAccessHandler())
            {
                da.DeleteAdministratorById(adminId);
            }
        }

Aber ich glaube ich sehe schon was du meinst.. macht ja irgendwie keinen Sinn.. man kann den Using+Context direkt in diese Methode packen, anstelle das mit dem DataAccessHandler anzustellen.

Build something that's idiot proof, and they'll build a better idiot

3.003 Beiträge seit 2006
vor 14 Jahren

Mein Hauptverständnisproblem - das letzten Endes auch etwas mit deinem Problem zu tun hat, ist:

Du hast eine DA-Klasse, die dir den Titel raussucht. So weit, so gut. Aber sollte nicht der DAL auch das speichern erzeugter Einträge übernehmen? Wieso ist das SaveChanges nach dem Erstellen eines Users nicht in der DAL? Wozu schreibst du eine Datenzugriffsklasse, wenn du den EF-Kontext, also den eigentlichen Datenzugriffs-Helper, auch ausserhalb dieser Klasse benutzt?

Entweder ich habe ein Verständnisproblem mit deiner Architektur, oder ich verstehe sie richtig - dann solltest du sie nochmal überdenken 😉.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

SaveChanges wird schon nur im DataAccessHandler verwendet. Aber ich denke es gibt in der Tat noch Verbesserungen bezüglich der Architektur die ich vornehmen kann.

Wie würdest du das umsetzen, in dieser Klasse (DataAccessHandler). Würdest du die Member-Variable "context" also entfernen und stattdessen in den einzelnen Methoden jeweils ein:

using (XyEntities context = new XyEntities())
{
    var mn = ....;
}

verwenden?

Build something that's idiot proof, and they'll build a better idiot

3.003 Beiträge seit 2006
vor 14 Jahren

Ja. Nun arbeite ich zumeist mit verteilten Systemen, und nach einer Übertragung (Serialisierung + Deserialisierung am anderen Ende) sind die Objekte logischerweise sowieso detached. Möglicherweise ist es in einer Anwendung wie bei dir, wo die Objekte zwischen verschiedenen Klassen hin- und hergereicht werden, hin und wieder sinnvoll, den Kontext mit zu übergeben, in dem sie sich befinden. Also so was hier:


using(var myContext = new MyEntityModel())
{
  var newUser = User.Create(...);
  newUser.Title = da.GetTitle(myInt, context);
}

Prinzipiell würde ich eher dazu tendieren, das Erstellen von Objekten eben dem DA zu überlassen, da gehört es hin. EF verführt manchmal etwas dazu, den Datenzugriff auf verschiedene Schichten zu verteilen - eben weil es so leicht ist, mal eben einen Datenzugriff zu machen. Auf diese Verführung ist jedenfalls der Autor auf der von dir verlinkten Seite hereingefallen, und ich glaube, du auch.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

Kennst du eine Seite, welche diesen Aufbau und die Verwendung des Entity Frameworks gut beschreibt? Meistens finde ich nur Tutorials die einem einzelnen Blog-Post entsprechen, und die gehen dann nur auf die ganzen Basics ein. Aber bezüglich der Struktur/Architektur (wo platziere ich was) habe ich noch nichts schlaues gefunden.

Build something that's idiot proof, and they'll build a better idiot

3.003 Beiträge seit 2006
vor 14 Jahren

Hm...zu Hause liegt noch das hier - hab bisher noch nicht reingeschaut, aber das könnte was sein.

Ansonsten ist da nicht viel zu finden. Liegt u.U. auch daran, dass einfach noch nicht viele Erfahrungen mit komplexeren Architekturen, die EF in der DAL verwenden, vorhanden sind, denk ich mir.

Schau auch nochmal in die Literaturtipps hier im Forum.

LaTino
@erfinder: falsche Baustelle?

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

Das Buch scheint recht gut zu sein, anhand der Beschreibung und den Kommentaren. Ich glaube ich werde mir das zulegen.

Ich bin auf der Suche noch auf diesen Artikel gestossen:
http://blogs.objectsharp.com/CS/blogs/barry/archive/2008/05/06/the-entity-framework-vs-the-data-access-layer-part-1-the-ef-as-a-dal.aspx

Was mir noch nicht ganz klar ist.. ich möchte ja meine Applikation ein wenig gescheit strukturieren. Es gibt zwei Clients. Einmal ein WinForm-Client und ein ASP.NET-Client, dann dachte ich, ich mache in der Solution jeweils Subprojekt (welches ich dann referenziere) "BussinessLogic" und "DataAccess".

Im DataAccess wird das *.edmx-Erstellt und evtl. eigene Extension-Methoden.

Im BusinessLogic sollten dann so generelle Logiken rein wie, "überprüfe Benutzer Login", "Berechne xy" oder auch sowas im sinn von "Lösche Benutzer", wobei dann mehrere Methoden des DataAccess verwendet werden, so dass eine Art ablauft abgebildet wird (User kann nur gelöscht werden wenn keine offenen Aufträge, etc. wenn alles ok -> markieren als löschen).

Ich hoffe bis hierhin sind meine Überlegungen schonmal nicht falsch?

Ich bin mir nicht sicher wo ich solche Methoden platzieren soll:GetAllUsers();
GetUserById(int id);
GetAllCategories();
DeleteUserById(int id);
CreateUser(user userEntity);

Ich dachte bisher die gehören in den DataAccess, aber vermutlich platziert man sowas in die "BusinessLogic" in eine Klasse z.B. UserHandler/UserController?

Shi

Build something that's idiot proof, and they'll build a better idiot

5.299 Beiträge seit 2008
vor 14 Jahren

also ich hab da iwie ein Problem mit.
Die Tabellen des DataContextes ssind doch die Business-Schicht.
Und der DataAccess ist doch in den Tiefen des OR-Mappers verkapselt.
Der DataContext enthält alle diese Tabellen, das ist das Datamodel.
Der Witz am or-mapper ist doch, dass man mit objekten programmiert, und der ormapper kümmert sich im Hintergrund um die persistierung.

Wennich also einen DataContext habe, brauche ich keine DataAcces-Methode zu schreiben - was soll "GetAllUsers()", wenn all users als DataContext.Users schon parat stehen?
"GetUsersByID()" braucht man doch gar nicht, IDs sind doch obsolete Eigenschaften, nur noch vom DataContext für die Persistierung nötig.

DataAccess-Programmierung findet doch ganz woanders statt, nämlich in der Konfiguration des DataContextes. Da würde man auch speziellere abfragen konfigurieren, meinetwegen "UsersWithTitle(Title title)", und das stünde dann im DataContext einfach parat.
"CreateUser(user userEntity);" ist das eine funktion, der man einen User übergibt, und die dann einen User erstellt? - was soll das? Ich würde denken, einen User erzeugt man mit var newUser = new User();, eben wie normale Objekte, halt das or-mapper-prinzip.

So, jetzt haut mich. 😉

Der frühe Apfel fängt den Wurm.

3.728 Beiträge seit 2005
vor 14 Jahren
Schichtenfrage

Die Tabellen des DataContextes ssind doch die Business-Schicht.

Nö!
Die Geschäftsschicht (Business-Schicht) ist da, wo die eigentliche Geschäftslogik (Berechnungen, Buchungen, Transaktionen) implementiert ist. Der OR-Mapper implementiert z.B. keine Methode, um eine Lager-Korrekturbuchung durchzuführen. Der OR-Mapper stellt aber die Datenstrukturen (z.B. Objekte für Bestandstabellen) und eine Persistenzmöglichkeit bereit. Die konkrete Buchung, also*Transaktion starten *Lagerbestand X lesen *Lagerbestand X um Menge Y korrigieren *Geänderten Bestand speichern *Neuen Eintrag für die Lager-Korrekturliste erzeugen *Korrekturlisteneintrag speichern *Transaktion abschließen

ist nicht im OR-Mapper implementiert. Wenn der OR-Mapper für Dich die Business-Schicht ist, stünde diese Implementierung der eigentlichen Logik bei Dir im GUI-Code und da hat sie nix verloren.

In der klassischen HelloWorld-Addressverwaltung, gibt es außer Lesen und Speichern der Adressen keine Geschäftslogik, deshalb denken viele Leute irrtümlicher Weise, die Datenklassen wären die Business-Schicht. Es ist in jedem Fall eine separate Business-Schicht einzuziehen, wenn die eigene Anwendung dem 3-Schichtenmodell genügen soll/muss. Falls die Schichten auch auf verschiedenen Maschinen laufen sollen/müssen, geht es sogar gar nicht anders, da die Clients den DataContext nicht kennen, sondern nur der Applikationsserver.

CreateUser und GetUser sind nur einfach schlechte Beispiele. Sobald man Anwendungen schreibt, die mehr tun, als Daten 1:1 anzuzeigen und zu speichern, wird klar warum man eine separate Geschäftsschicht braucht. Leider ist es schwer bis unmöglich sowas nachträglich einzubauen. Man muss es schon von Anfang an durchziehen. Am Ende hast Du dann Geschäftsklassen, die bestimmte Funktionalität als Methoden anbieten. Diese Methoden konsumiert die Präsentationsschicht. Ob ein OR-Mapper dahinter werkelt und wenn ja, welcher, davon darf die Präsentationsschicht eigentlich nichts mitbekommen. Man trennt ja deshalb die Anwendung in Schichten auf. Wenn diese Trennung nicht explizit aufgebaut und durchgezogen wird, ist die ganze Schichten-Lasagne für die Katz'.

Es muss auch nicht immer alles in Schichten eingeteilt werden, aber wenn man sich schon für diese Architektur entscheidet, sollte man sie auch verstanden haben und durchziehen.

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

ok, also nochmal für die langsamen (mich) 😃 , kann ich das mal so bestätigt haben (damit ich beruhigt schlafen kann):

das heisst also ich mache z.b. eine Klasse "UserController" in "BusinessLogic", also meiner Geschäftslogik-Schicht, dort habe ich z.b. irgendwelche User-Spezifischen Logiken/Berechnungen etc., welche von der View/GUI konsumiert werden. Dieser UserController selber greift wenn nötig auf den DataAccess zu (wo das Entity Framework, die Entities, der Context liegen).

Eine solche Methode:

public void List<User> GetAllDeletedUsers()
{
	using (AnwendungXyEntities context = new (AnwendungXyEntities())
	{
		var users =	from u in context.Users
				where u.Deleted
				select u;
	
		return users.ToList();
	}
}

Wäre also auch in dieser Klasse ("UserController") zu finden, also in der BL?

Mir ist natürlich bewusst das viele Wege nach Rom führen, aber mir geht es hier um grundlegende Designfehler. Btw, falls sich jemand das fragt: ja, ich möchte direkt die Entities als "BussinesObjects" verwenden.

Build something that's idiot proof, and they'll build a better idiot

3.728 Beiträge seit 2005
vor 14 Jahren

Wäre also auch in dieser Klasse ("UserController") zu finden, also in der BL?

Ja, definitiv.

C
282 Beiträge seit 2008
vor 14 Jahren

Da auch ich immer mit dieser ganzen Thematik zu kämpfen habe, versuche ich mir das Ganze etwas einfach vorzustellen. Die Grundidee ist ja einfach, dass ich alles entkoppele und z.B. in der GUI nicht weiß, wie auf die Datenbank zugegriffen wird. So kann ich heute mit LINQ2SQL, Entity-Framework oder morgen mit DataSets arbeiten und die GUI würde das nicht mal merken. Ich denke, wenn man sich diesen Ansatz einprägt und das nicht nur mit Datenbanken umsetzt, dann ist das Prinzip leichter zu verstehen.

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

Ich brauche leider noch immer etwas Hilfe. Ich habe mal die Architektur aufgeräumt (ich hoffe zum Guten)... jedoch bekomme ich hier langsam Hirnblutungen...

Diese dämlich context Geschichte scheint mir wirklich etwas verkorkst zu sein, ich weiss nicht ob ich jetzt einfach zu dämlich bin, oder ob das EF 1.0 hier einfach noch recht schwach ist.

Das Problem das ich habe/hatte/teilweise immer noch habe:
Ich möchte mir einen bestehenden User anpassen. Im BusinessLogicHandler habe ich eine Methode dir mir für eine ID einen User gibt, in der View fülle ich die Properties dieses User-Entity ab mit den Werten aus Textboxen (z.B. neuer Nachname, Email etc.), dann gebe ich das User-Objekt an eine Methode UpdateUser, welche wieder im BusinessLogicHandler ist.

-> zuerst ging mal gar nichts, hat gar nichts gemekert, aber in der DB kamen die Änderungen nicht an. Mittlerweile geht es, ich habe es nun so gemacht, dass die Methode GetUserById den User "detached" und bei "UpdateUser" wird der User zuerst wieder "attached" dann mit einer Custom-Extension-Method "SetAllModified" werden alle Properties als geändert markiert und der User wird dann via "context.SaveChanges()" gespeichert, siehe:
BusinessLogicHandler (ihr könnt hier das ganze Projekt im SVN Browsern, zum besseren Verständnis).

Ich nehme an das liegt daran, weil der Context im Using verwendet wird. Folglich hängt dieser Context irgendwie noch am Objekt dran wenn ich es dann später versuche zu speichern. Resp. wenn ich es nur mit SaveChanges() speichere und es nicht meinem neuen Context hinzufüge, landen die Änderungen halt im Nirvana.

  1. Gibt es da wirklich keinen besseren Weg?
  2. Leider habe ich nach wie vor den Titel zu speichern, normale Properties werden mit diesem gemurkse nun upgedated, jedoch der "Title" kann ich einfach nicht mitabspeichern, es kommt aber auch kein Fehler... wtf?!

Ehrlich... langsam sehne ich mich nach meinen typisierten DataSets zurück 😦

Build something that's idiot proof, and they'll build a better idiot

3.003 Beiträge seit 2006
vor 14 Jahren

Ich möchte mir einen bestehenden User anpassen. Im BusinessLogicHandler habe ich eine Methode dir mir für eine ID einen User gibt, in der View fülle ich die Properties dieses User-Entity ab mit den Werten aus Textboxen (z.B. neuer Nachname, Email etc.),

Da liegt doch dein Problem. Du arbeitest mit einem User-Objekt und setzt dessen Werte im View. Merkst was?

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

Hmm.. ich sehe jetzt nicht gerade was du ansprechen willst. Wenn du meinst dass die View keine User-Objekte kennen soll, dann weiss ich nicht wie ich das sonst machen soll -> ich möchte dieses eher kleine Projekt ja nicht unnötig aufblasen.

Ich denke es ist doch legitim die Werte in der View abzufüllen. Ich möchte ja nicht eine Methode schreiben, im Stil von:

public void UpdateUser(userId, string newUsername, string new Lastname, DateTime newBirthDay, string newEmail, string newAdress, string newCity, int newPlz, string newCountry, string newTelephone, string newFax, string newMobile, string new AlternateEmail, ... );

Oder meinst du was anderes?

Build something that's idiot proof, and they'll build a better idiot

3.003 Beiträge seit 2006
vor 14 Jahren

Nein, genau das meine ich schon. Das Problem aus meiner Sicht mit dem EF ist die enge Verquickung aus DA und BL - was du hier gemacht hast, bedeutet, dass du dem View ein Objekt aus dem BL übergeben hast - das Problem ist nur, dass dieses Objekt noch Informationen über den DA-Layer in sich trägt. (keine Ahnung, wie ich das besser ausdrücken soll - hoffe, es kommt rüber).

Das detach() soll das im wahrsten Sinn des Wortes lösen - beim attach() rennst du für Referenzeigenschaften wie Title aber wieder in genau das Problem. EF-Entitäten sind einerseits POCOs, andererseits (leider) auch wieder keine. Lösung ist entweder tatsächlich die Bereitstellung von entsprechenden Schnittstellen, oder...


/* methode GetUser */
return new BLUser { Title = user.Title.Name, Login = user.Login }

Die dritte Alternative, die du gewählt hast, ist, die Entitäten eben doch als POCO zu benutzen und dann beim anhängen an den Datenkontext, in dem gespeichert / geändert werden soll, die EntityStates entsprechend zu manipulieren (was dein SetAllModified() vermutlich macht - der Name kommt mir seltsam bekannt vor - aus irgendeinem Blogeintrag entnommen?).

Einen dieser Tode wirst du sterben müssen. Meine Methoden sehn gern mal so aus (pseudocode, Schnittstelle eines WCF-Service)


[OperationContract] 
public Ressource SetRessource(Ressource ress)
{
   using(Context context = new EntityContext())
   {
      var updateRess = context.Ressources.FirstOrDefault(p => p.ID = ress.ID);
      if(updateRess == null)
      {
          updateRess = Ressource.CreateRessource(ress.ID);
          context.AddToRessources(updateRess);
      }
      updateRess.Value1 = ress.Value1 != null ? ress.Value1 : defaultValue;
      updateRess.ReferenceValue = SetReferenceValue(ress.ReferenceValue); //diese Methode hat dasselbe Schema wie SetRessource selbst
      context.SaveChanges();
}

Wobei dank WCF das detach in diesem Fall sowieso schon wegfällt. Nur, damit das Schema rüberkommt.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

3.003 Beiträge seit 2006
vor 14 Jahren

Kleiner Nachtrag, speziell für deine Title-Geschichte. Es gibt sowas wie "stub entities", also nur teilweise gefüllte Entitäten. Die kann man wunderbar nutzen, um sich eine Datenbankabfrage zu sparen und dennoch die Beziehung korrekt zu speichern:


void SaveUser(User detachedUser, int titleId)
{
  using(Context context = new Context())
  {
       Title title = new Title { ID = titleId };
       context.AttachTo("Titles", title);

       detachedUser.SetAllModified();
       detached User.Title = title;
       context.SaveChanges();
  }
}

title ist in dem Fall eine solche Stub Entity. Wir haben die ID schon, und sparen uns auf die Weise das unnötige Holen der restlichen Daten des Title-Objekts unseres neuen Benutzers.

LaTino
PS: Wunderbare Serie - Tipps für die Benutzung des EF

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

TheShihan Themenstarter:in
170 Beiträge seit 2007
vor 14 Jahren

Danke, habe mir schon sowas ähnliches gedacht, dass ich es damit lösen könnte (mit der übergabe der TitleId), allerdings hätte ich das ganze Objekt geholt 😃 wieder was gelernt.

Btw, ich lese gerade das von dir empfohlene Buch durch "Programming Entity Framework" (Julia Lermann). Bis jetzt sehr empfehlenswert.

Build something that's idiot proof, and they'll build a better idiot

3.728 Beiträge seit 2005
vor 14 Jahren
Bewährtes

Ehrlich... langsam sehne ich mich nach meinen typisierten DataSets zurück 😦

Vielleicht ist folgende Diskussion dann für Dich auch interessant: Problem in Client-Server-Anwendung mit WCF und ADO.Net EntityFramework
Beide Threads laufen gerade zeitgleich parallel.

K
40 Beiträge seit 2010
vor 12 Jahren

Hallo!

Der Thread ist zwar etwas älter, aber ich habe gerade ein ähnliches "Problem".

Ich arbeite mit folgender Architektur:

Sql Server - Entit Framework 4.1 - WCF Service - Service Client - GUI

Alle Objekte sind POCOs, keine automatische Code Generierung

Jetzt geht es um folgenden Prozess:1.Ich erstelle auf dem Client ein neues Objekt
Dieses Objekt kann Referenzen zu anderen Objekten haben

1.Um diese Referenzen zu setzen, hole ich mir die entsprechenden bestehenden Objekte über den Service aus dem EF. 1.Ich besetze die Properties des neuen Objektes mit diesen Objekten 1.Ich sende das neue Objekt (mit seinen Referenzen) über den Service zum Speichern ans EF

Wenn ich jetzt zum Speichern das neue Objekt einfach so über Add() in den Context aufnehme, erhalten auch alle referenzierten Objekte den Zustand "added" was darin resultiert, dass das EF diese Objekte alle neu der DB hinzufügen möchte.

Die Lösung, die ich habe, ist wirklich über alle referenzierten Objekte zu iterieren, und diese zunächst über Attach() dem Context hinzuzufügen und zu letzt das neue Objekt über Add().

Das ist aber nun recht umständlich zu implementieren, von daher wollte ich fragen, ob jemand eine andere Lösung kennt. Vielleicht kann das EF 4.1 das ja auch irgendwie und ich hab's nur noch nicht gefunden.

Also, hat jemand 'ne Idee?

16.835 Beiträge seit 2008
vor 12 Jahren

Hi,

wenn es nur darum geht, eine neue Referenz hinzuzufügen (enichts anderes erklärst Du) reicht es, dass das neue Objekte via Referenzen zu setzen.
Das Add brauchst Du nur, wenn es eine unabhängige Entity ist.


var dbNewsEntity = myRepository.GetById( id );

var comment = new CommentEntity( );
comment.Name   = "Benutzer";
comment.Beitrag = "Text";

dbNewsEntity.Comments.Add( comment );

myRepository.Save( );


Gruß

K
40 Beiträge seit 2010
vor 12 Jahren

Hi!

Das Szenario ist leider gerade genau der umgekehrte Fall:



// Neues Training erzeugen
Training training = new Training();

// Training braucht 'nen Trainer
Trainer trainer = getTrainer(1); // getTrainer() soll hier das Abrufen eines vorhandenen Trainers simulieren

// senden an den Server und speichern via EF
insertTraining(training);


Dann passiert ja auf der Server Seite sowas:


insertTraining(Training t) {
  
  using(MyEntityContext ctx = new MyEntityContext()) {
   
    ctx.Trainings.Add(training);
    ctx.SaveChanges();

  }
}

Und an dieser Stelle ist dann das Problem (bzw. die unschöne Stelle):
Der Trainer, der in training.Trainer gespeichert ist, existiert schon in der DB. EF geht aber hin und setzt alles, was über Add() rein kommt auf "added", sofern die Objekte sich aktuell nicht Context befinden.

Das hat dann zur Folge, dass EF versucht, den Trainer auch als neues Objekt zu speichern, was entweder in 'ner ungewollten Dublette resultiert oder in einem Fehler wenn ein Unique Key definiert ist, der verletzt würde.

Wie gesagt, die mir bekannte Lösung sieht so aus:



insertTraining(Training training) {
  
  using(MyEntityContext ctx = new MyEntityContext()) {
    
    // Der Trainer wird dem Context bekannt gemacht, bevor das Training gespeichert wird. Dann wird er als "unchanged" aufgenommen.
    ctx.Trainer.Attach(training.Trainer);
    
    // Auch nach Hinzufügen des Trainings bleibt der Trainer unchanged.
    ctx.Trainings.Add(training);

    ctx.SaveChanges();

  }
}

Bei so einer einfachen Objekthierarchie ist das ja noch ganz okay. Sobald aber mehrere Referenzen im Spiel sind, muss für jede einzelne jedes Objekt vorher über Attach(entity) in den Context aufgenommen werden. Und das ist mühselige Schreibarbeit, das für jedes Objekt beim Hinzufügen zu implementieren und es ist anfällig für Veränderungen an den Objekteigenschaften.

Ich kann auch leider, da die Objekte ja über einen Service übertragen werden, keine self tracking entities verwenden. Oder?

Wenn es irgendwie geht würde ich mir die Arbeit gerne vom EF abnehmen lassen... und weil ich das noch nicht so lange mache, könnte es ja sein, dass EF das kann und ich es einfach noch nicht weiß.

6.911 Beiträge seit 2009
vor 12 Jahren

Hallo Kanalpiroge,

da ist as designed so. Möglich ist neben dem von dir vorgeschlagenen auch ein Setzen des EntityStates. In beiden Fällen muss der Objekt-Graph (rekursiv) durchlaufen werden. Das ist nicht schön und deshalb würde ich das Problem von Grund auf anders betrachten: gar nix auf dem Client machen und alles an den Server delegieren, denn der kann dann mit einem lokalen Context arbeiten. Beim WCF-Service ev. mit Sessions arbeiten und dann ist es gut.

Oder: du erstellst dann auf dem Server nochmal ein neues Objekt und kopiertst alle Eigenschaften (inkl. Fremdschlüssel, aber ohne Referenz) und dann passt es auch. Wäre sogar noch einfacher und in das bestehende System leichter zu integrieren.

Obs mit Self-Tracking geht weiß ich nciht, habs nie probiert, aber einen Versuch wäre es sonst wert.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

K
40 Beiträge seit 2010
vor 12 Jahren

Hmm... ich bin mir nicht sicher, ob ich das richtig verstehe...

Also bleiben wir bei dem Training - Trainer Beispiel.
Sollte ich dann das Training auf dem Server erzeugen und dann den Server ebenfalls anweisen, den Trainer hinzuzufügen?

Also den Service ergänzen um z.B. eine CreateTraining() und eine AddTrainerToTraining(Trainer t) und weitere Add...() Methoden für die übrigen Relationen?

6.911 Beiträge seit 2009
vor 12 Jahren

Hallo Kanalpiroge,

ja den Service ergänzen, oder so:


public class Training
{
    public int TrainerId { get; set; }
    public Trainer Trainer { get; set; }
    ...
}

Dann in der Service-Methode:


public void AddTraining(Training training)
{
    Training localTraining = new Training
    {
        TrainerId = training.TrainerId,
        // Trainer = null
        ...
    }

    // localTraining in den Context einfügen
}

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

K
40 Beiträge seit 2010
vor 12 Jahren

Ich habe gerade die Variante mit dem Attachen sämtlicher Verknüpfungen probiert und erhalte dabei folgende Exception:

Fehlermeldung:
Eine nicht behandelte Ausnahme des Typs
"System.InvalidOperationException" ist in System.Data.Entity.dll aufgetreten.

Zusätzliche Informationen: Ein Objekt mit demselben Schlüssel ist bereits im
ObjectStateManager vorhanden. Der ObjectStateManager kann nicht mehrere
Objekte mit demselben Schlüssel nachverfolgen.

Eine nicht behandelte Ausnahme des Typs "System.InvalidOperationException" ist
in System.Data.Entity.dll aufgetreten.

Zusätzliche Informationen: Ein Objekt mit demselben Schlüssel ist bereits im
ObjectStateManager vorhanden. Der ObjectStateManager kann nicht mehrere
Objekte mit demselben Schlüssel nachverfolgen.

Ich vermute, das kommt irgendwie durch Kreise im Objektgraphen zustande.
Zumindest taucht das Problem nur bei einer Verknüpfung auf, die wieder eine Referenz zurück auf das Training enthält. Selbst wenn ich die Referenz nicht setze (und das dem EF überlasse), scheint das EF diese beim Attachen dann selbst aufzubauen.

Deine zweite Variante mit gesetzten Ids und null-Referenzen für die Objekte funktioniert allerdings tadellos. Ich denke ich werd's dann dem entsprechend umbauen.

Thx!

3.728 Beiträge seit 2005
vor 12 Jahren
Status und Skalierbarkeit

Hallo zusammen,

Das ist nicht schön und deshalb würde ich das Problem von Grund auf anders betrachten: gar nix auf dem Client machen und alles an den Server delegieren, denn der kann dann mit einem lokalen Context arbeiten. Beim WCF-Service ev. mit Sessions arbeiten und dann ist es gut.

Ich würde eher sagen, dass das ganz böse ist. Das würde bedeuten, dass der Server statusbehaftet sein muss. Das ist Gift für die Skalierbarkeit der Anwendung. Der Status sollte beim Client liegen. Der Server sollte auf keinen Fall pro Clientsitzung einen EF-Kontext offen halten. Wie viele Benutzer verkraftet der Server gleichzeitig, wenn er für jeden dessen EF-Kontext verwalten muss?

Selbst wenn das mit der jetzigen Userzahl kein Problem ist, könnte es vielleicht in zwei Jahren ein Problem werden, wenn die Firma gewachsen ist (gesunde Unternehmen wachsen und die Software muss mitwachsen). Statusbehaftete Server verschwenden jede Menge Ressourcen.

EF verträgt sich mit verteilten Architekturen eben nur mäßig bis schlecht. Auch wenn es noch so schön abstrakt ist.

6.911 Beiträge seit 2009
vor 12 Jahren

Hallo Kanalpiroge,

Ich habe gerade die Variante mit dem Attachen sämtlicher Verknüpfungen probiert

Ich meint aber nicht Attachen von den Verknüpfungen, sondern das Setzen von EntityState.Unchanged (od. Unmodified weiß ich gerade nicht was korrekt ist).

Hallo Rainbird,

dein Einwand ist berechtigt und korrekt, die Skalierbarkeit hab ich glatt unterschlagen (da es bei meinen Userzahlen (die auch wieder Maschinen sind) sicher nie ein Problem werden wird). Aber danke für den Hinweis/Korrektur.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

K
40 Beiträge seit 2010
vor 12 Jahren

Stimmt... über ein manuelles Setzen des Status könnte man vermutlich auch die Exception vermeiden. Aber die Variante mit der gesetzten Id und Null-Referenzen funktioniert auch. Das lass' ich jetzt erst mal so.

Danke für die Hilfe!