Laden...

LINQ / DataContext / Problem beim Speichern

Erstellt von honkman16 vor 12 Jahren Letzter Beitrag vor 12 Jahren 4.693 Views
H
honkman16 Themenstarter:in
87 Beiträge seit 2007
vor 12 Jahren
LINQ / DataContext / Problem beim Speichern

verwendetes Datenbanksystem: MSSQL 2008 R2
Applikation: .Net 4.0 / C#

Gleich zu Beginn die Fehlermeldung: "Es wurde versucht, eine Entität anzufügen oder hinzuzufügen, die nicht neu ist und möglicherweise aus einem anderen DataContext geladen wurde. Dies wird nicht unterstützt."

Wie kommt es dazu?
Ich habe ein Objekt "Buchung" mit einigen relationalen Unterobjekten (Gast, etc.). Wenn ich nun mein Form lade erstelle ich einen neuen DataContext der so lange lebt, so lange das Form existiert. Von diesem DataContext lade ich nun mein Objekt "Buchung".

Wenn ich dieses nun speichern möchte, tritt der Fehler auf. Es ist aber immer noch der gleiche DataContext.

Was ich schon probiert habe:
*) Timestamp Feld in der Datenbank für alle Tabellen angelegt (zwecks Versionskontrolle des Datensatzes)
*) Vor dem Speichern verwerfen des DataContext, neu instanzieren und das Objekt mittel "Attach" dem DataContext anfügen. Hierfür ist beim Laden zu Beginn der Parameter "DeferredLoadingEnabled" auf false zu setzen.

Beide Lösungsansätze habe ich aus Foren. Ich bekomme den Fehler aber immer noch.

Kann mir jemand eine Erklärung oder einen Lösungsansatz dafür geben?

Herzlichen Dank!

4.939 Beiträge seit 2008
vor 12 Jahren

Der DataContext sollte nur so kurzzeitig wie möglich benutzt werden (d.h. am besten innerhalb einer Methode mittels einer using-Anweisung).

Und deiner Beschreibung nach hört es sich nicht so an, als ob du ein 3-Schichten-Model (GUI, Model, DataLayer) verwenden würdest.

Informiere dich auch mal über das Repository-Pattern.
Der Code könnte dann so ähnlich aussehen (aus einem meiner Testprogramme):


    public class PersonRepository : IRepository<Person>
    {
        public Person Get(int id)
        {
            using (var context = new PersonDataContext())
            {
                var person = from p in context.Persons where p.Id == id select p;
                return person.FirstOrDefault();
            }
        }

        public IList<Person> GetAll()
        {
            using (var context = new PersonDataContext())
            {
                return context.Persons.ToList();
            }
        }

        public bool Save(Person person)
        {
            using (var context = new PersonDataContext())
            {
                context.Persons.Attach(person);
                context.SubmitChanges();
                return true;
            }
        }
    }

H
honkman16 Themenstarter:in
87 Beiträge seit 2007
vor 12 Jahren

Danke für die Antwort. Korrekt: Die 3 Layer habe ich nicht. Habe jetzt testweise die 3 Layer umgesetzt. Und zwar:

Mein Interface:

public interface IRepository<T>
    {
        T Get(int id);
        bool Save(T entity);
    }

Meine Entity Klasse:

class RBooking : IRepository<Booking>
    {
        public Booking Get(int id)
        {
            using (var context = new BookixDbDataContext(Constants.ConnectionString))
            {
                var booking = from b in context.Bookings where b.BookingId == id select b;
                return booking.SingleOrDefault();
            }
        }

        public bool Save(Booking booking)
        {
            using (var context = new BookixDbDataContext(Constants.ConnectionString))
            {
                context.Bookings.Attach(booking);
                context.SubmitChanges(ConflictMode.FailOnFirstConflict);
                return true;
            }
        }
    }

Aufruf:

private void Test()
        {
            RBooking rb = new RBooking();
            Booking booking = rb.Get(129);
            booking.IsFa = true; // nur drin um einen Wert zu ändern

            rb.Save(booking);
        }

Leider immer noch der gleiche Fehler. Was ist daran nicht korrekt?

H
honkman16 Themenstarter:in
87 Beiträge seit 2007
vor 12 Jahren

UPDATE:

Wenn ich beim Laden "context.DeferredLoadingEnabled = false;" setze, dann kann ich ohne Fehler speichern. Aber: Der Wert wird nicht in die DB geschrieben.

Wenn ich beim Speichern dann nach dem "Attach" noch folgenden Code hinzufüge, dann wird die Änderung in die Datenbank geschrieben:

context.Refresh(RefreshMode.KeepCurrentValues, booking);

Wirklich durchblicken bzw. verstehen kann ich es aber leider noch nicht 😦

16.834 Beiträge seit 2008
vor 12 Jahren

Hallo,

Dein Repository ist falsch aufgebaut; Repositories kümmern sich nie selbst um das Öffnen und Verwalten von Datenbankverbindungen, sondern stellen lediglich eine Schnittstelle dar. Sie erhalten aber die aktive Verbindung über den Konstruktor.

Normalerweise hat ein Repository immer eine Basis, von der sie erbt; zudem sind Standardimplementierungen wie Get, GetMany, GetAll, Add, Delete, Save enthalten.

public abstract class RepositoryBase<T> where T : class
{
	protected RepositoryBase( DbContext dbContext )
	{
		m_dataContext = dbContext;

		m_dbset = DataContext.Set<T>();
	}
	
	// Hier die Standardimplementierunge, wie zB ein Single-Get
	 public T Get( Expression<Func<T, Boolean>> where )
	{
		return m_dbset.SingleOrDefault( where );
	}
}

Ein spezifisches Repository erbt nun von diesem.
Es hat ebenso einen Konstruktor mit dem DbContext; gibts diesen aber an die Basis weiter. Zudme enthält es spezifische Methoden, die nur für diese Entity bzw. dessen spezifisches Repository gelten soll.

public class UserRepository : RepositoryBase<User>
{
	public UserRepository( DbContext dbContext )
		: base( dbContext )
	{
	}

	// Spezifische Abfragen nur für dieses Repository
	public User GetByID( Int64 id )
	{
		return Get( userEntity => userEntity.UserID.Equals( id ) );
	}
}

Die Verwendung wäre dann zB:

public void AddMyUser(String userName, String hashedPassword, String eMail)
{
	var user = new User
	{
		UserName = userName,
		PasswordHash = hashedPassword,
		EMail = eMail
	};

	using( var dbContext = DatabaseConnectionFactory.SingleConnection )
	{
		var userRepository = new UserRepository ( dbContext );
		userRepository.Add(user);
		userRepository.Save();
	}
}

Das Problem das Du hast ist nämlich, dass Du die Entity in einem anderen Kontext erstellst.
Du machst irgendwo zB ein new User(); und willst dieses dann in einem ganz anderen Kontext hinzufügen und speichern - und das funktioniert nicht ohne weiteres.
Der Aufgbau Deiner Anwendung stimmt hier ganz einfach nicht.

GANZ WICHTIG:
Das Beispiel von Th69 ist leider Käse, da man zudem tunlichst ToList() hier vermeiden soll!!! Repository in EF 4.1 korrekt einsetzen
Gründe dafür haben xxMUROxx und ich ausführlich genannt!

Auch ist die Aussage falsch, dass man den Context nur so kurz wie möglich verwenden soll.
Grund: wenn man für jedes Repository eine einzige Verbindung verwendet, muss man ständig die Entity vom Kontext A in Kontext B detachen und attachen; das ist absolut unnötig und überhaupt nicht realitätsnah - man hat schließlich zwischen den Entities durchaus Relationen.
Man kann alles in ein using - wie in meinem Beispiel - packen, und mehrere Repositories innerhalb diesem damit versorgen.

Grüße

4.939 Beiträge seit 2008
vor 12 Jahren

Hallo Abt,

mir ging es nur um das Stichwort "Repository" (daß mein Beispiel nicht optimal ist, weiß ich, daher habe ich ja auch "Testprogramm" geschrieben...).
Außerdem scheint es ja wohl unterschiedliche Sichtweisen von den Aufgaben eines Repositories zu geben. Auch http://devtyr.norberteder.com/post/Das-Repository-Pattern-anhand-eines-Beispiels-inkl-Tests.aspx verwendet in seinem Repository eine Sortierung sowie List<T> als Rückgabewert.
Ich stimme dir aber zu, daß im allgemeinen IQueryable<T> besser ist.

Und bzgl. kurzzeitige Verwendung des DataContext

In general, a DataContext instance is designed to last for one "unit of work" however your application defines that term. A DataContext is lightweight and is not expensive to create. A typical LINQ to SQL application creates DataContext instances at method scope or as a member of short-lived classes that represent a logical set of related database operations.

Und die steht im Gegensatz zur Aussage:

Wenn ich nun mein Form lade erstelle ich einen neuen DataContext der so lange lebt, so lange das Form existiert.

A
350 Beiträge seit 2010
vor 12 Jahren

Hi,
etwas OffTopic:
wieso nicht ein vollkommen Generisches Repo ?

public interface IRepository<T> : IQueryable<T>
    {
        bool Save(T entity);
        T Get(int id);
        void Remove(T entity);
    } 
 public class GenericRepository<T> : IRepository<T>
    {
        private readonly ISession _session;

        public GenericRepository(ISession session)
        {
            _session = session;
        }

        public IEnumerator<T> GetEnumerator()
        {
            return _session.Query<T>().GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public Expression Expression
        {
            get { return _session.Query<T>().Expression; }
        }

        public Type ElementType
        {
            get { return _session.Query<T>().ElementType; }
        }

        public IQueryProvider Provider
        {
            get { return _session.Query<T>().Provider; }
        }


        public bool Save(T entity)
        {
            try
            {
                using (var transact = _session.BeginTransaction())
                {
                    _session.SaveOrUpdate(entity);
                    transact.Commit();
                    return true;
                }
            }
            catch (Exception)
            {

                return false;
            }
        }

        public T Get(int id)
        {
            return _session.Get<T>(id);
        }

        public void Remove(T entity)
        {
            using (var transact = _session.BeginTransaction())
            {
                _session.Delete(entity);
                transact.Commit();
            }
        }
    }

Ich finde die Art von Repository recht genial : Man muss keine Ableitungen schreiben sondern erstellt sich im Code, je nach gewünschten Typ ein Repo. Ich mus also nicht zwingend ein UserRepository , ein AdressRepository ein CusterRepo usw. schreiben sondern rufe mir nur das GenericRepository auf.

Grüße

16.834 Beiträge seit 2008
vor 12 Jahren

Hi,

dann muss man sagen, dass der Herr Eder das Prinzip nicht verstanden hat, das xxMUROxx und ich in dem andern Thread erklärt hatten. Ich wollte Dich damit auch nicht verurteilen oder sowas; ein prinzipielles Materialisieren und Casten in der Schnittstelle ist einfach das schlimmste was machen dort machen kann in Sachen Performance.

Zum anderen: Eine Form stellt ja quasi einen UnitOfWork mehr oder weniger dar: alles was innerhalb dieser Form geändert wird ist - in der Regel - eine Arbeitseinheit für den Commit.

wieso nicht ein vollkommen Generisches Repo ?

Weil Du hier keine Modularisierung im eigentlichen Sinne erstellen kannst - und genau dafür ist ein Repository gedacht.
In spezifischen Repositories kannst Du Methoden implementieren, die nur für dieses eine Entity gelten.
==> Vorteil Modularisierung
==> Vorteil Code-Wartung

Du kannst nicht einfach übe rein GenericRepository sagen GetUserByEMail(), sondern musst jedesmal an jeder Codestelle Get(user => user.EMail.Equals(mail)); schreiben. Willst Du dann nachträglich zB die StringComparison ändern, muss Du das überall, an jeder Codestelle änder... super Sache 🙄 Dann kann mans gleich lasse 😉

Siehe auch mein Beispiel in Repository in EF 4.1 korrekt einsetzen

A
350 Beiträge seit 2010
vor 12 Jahren

wieso nicht ein vollkommen Generisches Repo ?
Weil Du hier keine Modularisierung im eigentlichen Sinne erstellen kannst.
In spezifischen Repositories kannst Du Methoden implementieren, die nur für dieses eine Entity gelten.
==> Vorteil Modularisierung
==> Vorteil Code-Wartung

Siehe auch mein Beispiel in
>

Hmm wenn ich aber eine Modularisierung brauche bzw. eine Spezialisierung kann ich ein Repository erstellen, welches von dem generischem ableitet oder ?

Ich meine viele Wege führen nach Rom und solange man sich an gewisse Praktiken hält...... Oder sehe ich das zu Naiv ?

Grüße

16.834 Beiträge seit 2008
vor 12 Jahren

Naja, ich hab auch 2-3 Repositories, die keine spezifischen Methoden haben - aber das ist gerade mal eine Datei mit 5 Zeilen, die man hierfür erstellen muss.. deutlich weniger Aufwand als ohne Modularisierung.
Siehe auch mein Edit ein Beitrag zuvor.

Angenommen Du hast aktuell kein spezifisches Repository, weil Dus keine Methoden dafür hast.
Irgendwann kommt der Tag X, an dem eben genau das angefordert wird - was machst dann?
Musst überall den Code anfassen, oder Du nutzt an der Codestelle A nen generisches und an der neuen das spezifische - nicht so wirklich das Gelbe vom Ei 😉

Und ich sehe viele viele Code-Schnippsel, die sehr naiv aufgebaut sind - manchmal auch meine eigenen 8).
Code-Wartung wird viel zu oft vernachlässigt; dabei ist das 80% des gesamten Aufwandes innerhalb der Lebenszeit einer Applikation.

A
350 Beiträge seit 2010
vor 12 Jahren

Hi, im Grunde hast du Recht , man kann aber auch den Weg gehen , das Generische Repository zu nutzen und für gewisse Entitäten Filter zu bauen.
zB GetUserByName und diese als ExtensionMethod anbieten.
Dann habe ich auch nur eine Stelle wo ich den Code warten muss.

Wobei der Aufwand dann auch gleich ist entweder ein spezifiziertes Repository oder der Filter .... 😉

Und ja : Wartung wird oft vernachlässigt, vorallem wenn man CodeSchnipsel postet ohne das große Ganze zu sehen .

16.834 Beiträge seit 2008
vor 12 Jahren

Über ExtensionMethods? Auch möglich; aber dann hast gleich zwei Baustellen 😉
Bin kein Freund von solchen einem Code-Design - möglich ist es natürlich. Aber ich will die Dinge dort ändern können, wo ich sie auch erwarte. Sowohl von mir, als auch von übernommenen Projekten oder Team-Projekten.

Aber das schweift jetzt ein wenig vom Thema ab 🙂