Laden...

Partielle Datenbankupdates mit dem EF

Erstellt von t0ms3n vor 8 Jahren Letzter Beitrag vor 8 Jahren 9.940 Views
T
t0ms3n Themenstarter:in
314 Beiträge seit 2013
vor 8 Jahren
Partielle Datenbankupdates mit dem EF

verwendetes Datenbanksystem: MSSQL

Hallo zusammen,

ich stelle aktuell mal wieder eine API für den Zugriff auf eine Applikation/Datenbank zur Verfügung. In der Hauptapplikation selbst gibt es leider kein entsprechendes DAL, welches ich dafür verwenden möchte. Die benötigte API muss dabei allerdings aktuell nur einen geringen Umfang des Gesamtsystems abdecken. Als Seiteneffekt möchte ich jedoch langfristig das aktuelle DAL durch diese API/DAL ersetzen.
Zuletzt habe ich für einen ähnlichen Task das EF (6.x) genutzt und war damit auch recht zufrieden, wobei mir aber gerade das Verhalten im disconnected Bereich dort doch immer mal wieder Schwierigkeiten bereitet hat.

Aktuell mache ich mir Gedanken wie partielle Updates sauber und dennoch bequem abgebildet werden können. Gefunden habe ich dazu hauptsächlich die Herangehensweise, dass an den entsprechenden Properties der State geändert wird. (z.B. http://stackoverflow.com/questions/12871892/entity-framework-validation-with-partial-updates )
Alternativ sieht für mich flexibler und sauberer aus mit entsprechenden DTOs zu arbeiten und diese einfach zu mappen.

Wie führt ihr im Normalfall partielle Updates durch?

Ich wollte mir in diesem Zusammenhang ggf. auch Alternativen wie z.B. Dapper näher ansehen. Soweit ich dies dort sehe, wäre es hier nun wieder notwendig entsprechende Updates zu schreiben oder natürlich passende Extensions ( z.B. http://www.bradoncode.com/blog/2012/12/creating-data-repository-using-dapper.html )

Für sonstige Anregungen bin ich natürlich auch offen 😃.

16.834 Beiträge seit 2008
vor 8 Jahren

Dapper ist nur ein MicroORM.
FluentMigrator ist wohl eher das, was Du suchst.

T
t0ms3n Themenstarter:in
314 Beiträge seit 2013
vor 8 Jahren

Das Dapper nur ein MicroORM ist, ist mir wohl bewusst, aber es geht mir ja auch nicht (direkt) um den Featureumfang etc.

Inwiefern kann mir FluentMigrator weiterhelfen? Ich möchte ja nicht die aktuelle Datenbank ändern oder sonstiges.

16.834 Beiträge seit 2008
vor 8 Jahren

Ok, der FluentMigrator passt nicht 100% aber man kann ihn für sowas durchaus missbrauchen (mach ich auch an 2-3 Stellen), einfach, weils bequemer und schneller ist als EF.
Habs mal durchgestrichen bevor es weiter zur Verwirrung sorgt.

Aber partial updates sind nicht gleich partial updates.
Bei relationalen DBs ist das viel aufwändiger als zB bei MongoDB. Da gibt es einen Upsert-Befehl und das wars.

Dapper benötigt SQL-Commands von Haus aus; das ist korrekt und finde ich an vielen Stellen besser.
Das EF hat schon seine Vorteile, wobei die Nachteile relativ schnell aus meiner Sicht überwiegen, gerade wenn es um Bulk-Operationen geht, weswegen ich i.d.R. bei neuen Projekten und relationalen Datenbank stets Dapper verwende.

Die Performance und dem Umstand bei EF mit Bulk-Operationen hat mich an vielen Stellen so sehr genervt, dass ich teilweise doch SQL-Code in EF Repositories hatte.
Es gibt auch die Bibliothek https://github.com/loresoft/EntityFramework.Extended, die bei Abfragen wie Where auf Delete / Update deutlich bessere Resultate liefert; aber da tut sich seit Ewigkeiten auch nichts mehr, sodass es wieder auf SQL-Commands hinaus läuft.

T
t0ms3n Themenstarter:in
314 Beiträge seit 2013
vor 8 Jahren

Ich führe mal anhand eines Beispiels aus, was genau ich mit partial meine.
Im Beispiel wird SQLite, Mapster und Dapper genutzt.

Folgendes Repository sei gegeben:


public class ProductRepository : IRepository<Product>
    {
        private IDbConnection _connection;

        public ProductRepository(IDbConnection connection)
        {
            _connection = connection;
        }

        public Product Get(int id)
        {
            return _connection.Query<Product>("SELECT Id, Name, Description, IsActive, CreateDate, VendorId FROM Product WHERE Id = @id", new { id = id }).SingleOrDefault();
        }

        public bool Update(Product entity)
        {
            return _connection.Execute("UPDATE Product SET Name = @Name, Description = @Description, IsActive = @IsActive, VendorId = @VendorId WHERE Id = @Id", entity) > 0;
        }
}

Die Klasse Product besitzt natürlich entsprechend alle nötigen Properties.

Dazu gibt es den FooService.


    public class FooService
    {
        IRepository<Product> _productRepository;

        public FooService(IRepository<Product> productRepository)
        {
            _productRepository = productRepository;
        }

        public Product GetFull(int productId)
        {
            var fullItem = _productRepository.Get(productId);
            return fullItem;
        }

        public DTO.ProductUpdateActive GetActiveProduct(int productId)
        {
            // Vollständiges Item abrufen, nicht ganz so schön, da natürlich unnötig viel abgefragt wird...
            var fullItem = _productRepository.Get(productId);
            // Mappen des Products auf ProductUpdateActive (enthält nur Id, Name und IsActive)
            var partialItem = TypeAdapter.Adapt<DTO.ProductUpdateActive>(fullItem);

            return partialItem;
        }

        public bool UpdateActiveProduct(DTO.ProductUpdateActive item)
        {
            // Vollständiges Item abrufen, nicht ganz so schön, da natürlich unnötig viel abgefragt wird...
            var fullItem = _productRepository.Get(item.Id);
            // Mappen der Properties von ProductUpdateActive (enthält nur Id, Name und IsActive) zurück ins volle Item
            var updatedFullItem = TypeAdapter.Adapt<DTO.ProductUpdateActive, Product>(item, fullItem);

            return _productRepository.Update(updatedFullItem);
        }
    }

Und so würde z.B. eine Verwendung aussehen.


 class Program
    {
        static Container container;

        static void Main(string[] args)
        {
            // Init Container
            Config();
            var foo = container.GetInstance<FooService>();

            var full = foo.GetFull(2);
            Console.WriteLine(string.Format("Full ### Id: {0}, Name: {1}, IsActive: {2}", full.Id, full.Name, full.IsActive));

            var partial = foo.GetActiveProduct(2);
            Console.WriteLine(string.Format("Partial ### Id: {0}, Name: {1}, IsActive: {2}", partial.Id, partial.Name, partial.IsActive));
            partial.IsActive = !partial.IsActive;
            Console.WriteLine(string.Format("Partial ### Id: {0}, Name: {1}, IsActive: {2}", partial.Id, partial.Name, partial.IsActive));

            var success = foo.UpdateActiveProduct(partial);
            full = foo.GetFull(2);
            Console.WriteLine(string.Format("Full ### Id: {0}, Name: {1}, IsActive: {2}", full.Id, full.Name, full.IsActive));
        }
...
}

Schöner fände ich natürlich, wenn tatsächlich nur die Felder geupdated werden, die auch im ProductUpdateActive vorhanden sind und genauso auch nur diese abgefragt werden.

Das EF könnte an dieser Stelle dies ja durch das ChangeTracking entsprechend erledigen und nur IsActive updaten.

Im Anhang das volle Beispielprojekt.

16.834 Beiträge seit 2008
vor 8 Jahren

Ich hab sowas schon mit MongoDB gemacht, aber nicht mit EF/MSSQL.

Dazu hatte ich ein generisches Repositories, das neben Entities auch mit Entity-"Views" arbeiten konnte.
Verletzt ein wenig den prinzipiellen DDD-Ansatz von Repositories, das normalerweise nicht mit DTOs sondern mit Entitäten und OOP arbeiten sollte.

public interface IPersonEntity
{
     
}

public class PersonEntity : IPersonEntity
{
    public ObjectId Id {get;set;}
    public String FirstName {get;set;}
    public String LastName {get;set;}
    public DateTime DoB {get;set;}
}

public class PersonBirthdayView : IPersonEntity
{
    public ObjectId Id {get;set;}
    public DateTime DoB {get;set;}
}

// Repository
public IList<T> GetAll<T>() where T : IPersonEntity, new()
{
    // Ermitteln von allen Properties in T
    // MongoDB Query so aufbauen, dass nur diese Felder geladen werden (über Fields()
    // Query ausführen
    // Liste zurückgeben
}

Und in dem Stil ging natürlich auch der Aufbau beim Upsert.
MongoDB ist hier um Welten einfacher beim Aufbau des Queries, da das alles in der FOrm auch direkt vom MongoDB Driver unterstützt wird; EF hat das in dieser Form nicht.

Dapper sollte das aber über die anonymen Typen unterstützen können.
zB Execute("SELECT * FROM Persons", PersonBirthdayView-Object...) lädt Dir auch nur die beiden Properties in das Objekt.
Wenn Dapper nun das SELECT so anpasst, dass er statt * nur diese Felder lädt, dann wäre es ja genau das, was Du willst.

Ansonsten kannste Dir ja selbst über den TypeDescriptor die Eigenschaften ziehen, die im jeweiligen (auch anonymen Typ) hinterlegt sind und entsprechend das * im Select austauschen.

F
10.010 Beiträge seit 2004
vor 8 Jahren

Ich frage mich in solchen Situationen dann immer, wenn es so kompliziert ist das zu machen was ich vorhabe, ist meine Idee vielleicht nicht so zielführend wie ich denke.

Wozu meinst du das partiale Update zu brauchen?
Geschwindigkeit kann es nicht sein, denn bei keiner mir bekannten Datenbank werden einzelne Bytes gelesen oder geschrieben, sondern immer Sektoren.

Speziell in deinem Fall, was hindert dich daran ein Model ProductUpdateActive zu machen?
Meinst du irgendein Mapper käme nicht damit zurecht 2 Typen auf eine Tabelle zu mappen?
Oder warum nicht mit OOP also Vererbung arbeiten?
http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/implementing-inheritance-with-the-entity-framework-in-an-asp-net-mvc-application
http://weblogs.asp.net/manavi/inheritance-mapping-strategies-with-entity-framework-code-first-ctp5-part-1-table-per-hierarchy-tph

T
t0ms3n Themenstarter:in
314 Beiträge seit 2013
vor 8 Jahren

Darüber, dies mit Vererbung zu erschlagen, habe ich tatsächlich noch nicht nachgedacht. Allerdings sollten in diesem Moment ja auch Inserts für die "Partial"-Entity verboten werden. Wobei dies ja auch kein gewünschtes/benötigtes Verhalten ist.

Es geht mir z.B. darum, dass der API Konsument auch nur die Felder ändern kann, die an der entsprechenden Stelle vorgesehen sind. Da klang es für mich plausibel, diesem auch nur diese Felder anzubieten.

Ein eigenes PartialModel würde welche Auswirkungen habe?:

  • eigenes Repository (ohne Insert, ggf. ohne Delete)
  • im Fall von z.B. Dapper zusätzliche Selects sofern man es nicht bei "SELECT * FROM <tableName>" belässt
16.834 Beiträge seit 2008
vor 8 Jahren

Es geht mir z.B. darum, dass der API Konsument auch nur die Felder ändern kann, die an der entsprechenden Stelle vorgesehen sind. Da klang es für mich plausibel, diesem auch nur diese Felder anzubieten.

Ein GET liefert bei einer API prinzipiell immer das ganze Objekt laut REST. (Mit REST-Erweiterungen wie OData kannst Du dieses einschränken - aber vom Client aus!

Es ist aber laut REST auch möglich einzelne Felder zu aktualisieren.
Das passiert dann laut REST via PATCH. Die übergebenen Daten _müssen _nicht mal dem Schema der Entität entsprechen.
PUT/POST arbeiten aber immer mit vollständigen Objekten, die dem Schema entsprechen sollten.

Aber: PUT / PATCH / POST liefern i.d.R. immer - genauso wie GET - das vollständige, aktualisierte Objekt zurück - kein Teil davon.

Dahingehend ist die Frage nach Partial eigentlich hinfällig, da Du ohnehin das vollständige Objekt brauchst.

T
t0ms3n Themenstarter:in
314 Beiträge seit 2013
vor 8 Jahren

Hmmm, ich bin nicht an eine REST API gebunden. Und in wiefern widerspräche ein DTO, welche ja vollständig geliefert werden würde, dem Grundsatz, dass das ganze Objekt geliefert wird?

Das ganze Objekt kann ja nicht immer den vollen Datenstand aus der Datenbank bedeuten!?

16.834 Beiträge seit 2008
vor 8 Jahren

Ich hoffe, dass Du REST für die API weglassen könntest, nur ein Spaß ist 😉
APIs heutzutage ohne REST-Ansatz zu entwickeln ist in meinen Augen mehr als fragwürdig.

Das ganze Objekt heisst i.d.R. ohne Navigation-Properties bzw. diese wenn dann explizit.
Wenn man OData verwendet macht das die IQueryable-Schnittstelle automatisch für Dich, sofern bis zum DAL durchgängig implementiert.

Ich finde Deinen Code schon nicht gut, dass Du in Deinem DAL mit DTOs arbeitest.
Der DAL sollte eigentlich nur die Entitäten kennen; jedenfalls ist das im Sinne von CRUD.

Wenn ich nur Teile einer Entität aktualisieren will, dann gebe ich das i.d.R. explizit über Parameter an; nicht über eine Partial-Entity.
Auch wenn ichs wie oben erwähnt schon selbst so gemacht habe, finde ich den Ansatz nicht so toll und trägt auch nicht wirklich zur Übersichtlichkeit und Wartungsfreundlichkeit bei.

F
10.010 Beiträge seit 2004
vor 8 Jahren

APIs heutzutage ohne REST-Ansatz zu entwickeln ist in meinen Augen mehr als fragwürdig.

Es gibt tatsächlich Menschen die noch Software machen die nichts, aber auch garnichts mit (Web-) Services zu tun haben.
Soll es echt geben.

T
t0ms3n Themenstarter:in
314 Beiträge seit 2013
vor 8 Jahren

Ich lasse mich gerne überzeugen, bin es aber noch nicht 😃.
Ich kann auch nicht bestätigen, dass der DAL die DTO kennt bzw. damit arbeitet. Das Repository kennt nur Product. Das Übertragen der Eigenschaften von der DTO auf das DAL-Model übernimmt an der Stelle ja der FooService.

Das ganze Objekt heisst i.d.R. ohne Navigation-Properties bzw. diese wenn dann explizit.

Folglich führt eine unbedachte Nutzung also immer zu einem "SELECT * FROM x"? Das verursacht irgendwie Bauchschmerzen, wenn ich doch weiß, dass der Rest des Models den Benutzer gar nicht interessiert. Oder versteh ich hier etwas falsch?

16.834 Beiträge seit 2008
vor 8 Jahren

Es gibt tatsächlich Menschen die noch Software machen die nichts, aber auch garnichts mit (Web-) Services zu tun haben.

Und was genau rechtfertigt nun die Ignoranz von REST?
REST ist doch gar nicht nur auf das Web bezogen, sondern empfiehlt einen standardisierten Weg beim Austausch von Daten über verschiedene Technologien hinweg basierend auf dem HTTP Protokoll.

Ja, ich habe hier API als (Web)Service verstanden und nicht im Sinne eines SDKs.

F
10.010 Beiträge seit 2004
vor 8 Jahren

Und was genau rechtfertigt nun die Ignoranz von REST?

Warum soll ich mir nen HTTP-Webservice ( wie auch immer ) erstellen, wenn ich nur Local in einer Software eine DB ansprechen will?
Was rechtfertigt es deinen Hammer zu benutzen?

16.834 Beiträge seit 2008
vor 8 Jahren

Ja, ich habe hier API als (Web)Service verstanden und nicht im Sinne eines SDKs.

T
t0ms3n Themenstarter:in
314 Beiträge seit 2013
vor 8 Jahren

Ja, ggf. wäre auch SDK ein passender Ausdruck. Die Bereitstellung wird jedoch sehr wahrscheinlich via Web (Intranet) geschehen.

Wie gesagt möchte ich zumindest den DAL ggf. auch zukünftig als einfache Library für die Hauptapplikation nutzen (oder auch komplett über die "öffentliche" API/SDK).

Was ich wie gesagt **vermeiden **möchte ist, dass 3rd-Party Nutzer Daten ändern kann, die nicht dafür vorgesehen sind. Diese Gefahr sehe ich eben, wenn ich diesen immer den vollständigen Datensatz zur gebe.

Falls es irgendwie einen Unterscheid machen sollte... ich spreche hier nur von firmeninternen Applikationen die darauf zugreifen.

16.834 Beiträge seit 2008
vor 8 Jahren

Die Bereitstellung wird jedoch sehr wahrscheinlich via Web (Intranet) geschehen.

Dann wäre wenigstens geklärt, dass FZelles Einwand an dieser Stelle unberechtigt ist und wir hier sehr wohl von REST sprechen (sollten).

Was ich wie gesagt **vermeiden **möchte ist, dass 3rd-Party Nutzer Daten ändern kann, die nicht dafür vorgesehen sind.

Das ist aber nicht Aufgabe des DALs, sondern der Business-Logik, die für die Authorisierung verantwortlich ist.

ich spreche hier nur von firmeninternen Applikationen die darauf zugreifen.

Vor allem Firmeninterne Applikationen, die meist eine sehr viel längere Laufzeit haben, sollten sich an Standards halten, damit sie eben möglichst lange ohne Anpassungen funktionieren.

T
t0ms3n Themenstarter:in
314 Beiträge seit 2013
vor 8 Jahren

Das die Authorisierung nicht im DAL passiert sollte selbstverständlich sein. Ich hoffe, dass dies nicht so zwischen den Zeilen/Posts steht 😃.

Aber sprechen wir hier nicht dann dennoch wieder von z.B. einem "SimpleProduct" und von mir aus einem zusätzlichen "FullProduct" Endpoint?

Wobei SimpleProduct für die beschriebene 3rd Party und FullProduct eben für z.B. die Hauptanwendung freigegeben wird?
Das würde mich dann aber wieder zum Eingangspost zurückführen... Da dies letzten endes für das DAL nur ein partial Update der Gesamtentität ist!?