Laden...

[erledigt] Entity Framework 6: Ergebnis von ExecuteSqlCommand wird nicht angezeigt

Erstellt von bb1898 vor 6 Jahren Letzter Beitrag vor 6 Jahren 2.598 Views
B
bb1898 Themenstarter:in
112 Beiträge seit 2008
vor 6 Jahren
[erledigt] Entity Framework 6: Ergebnis von ExecuteSqlCommand wird nicht angezeigt

verwendetes Datenbanksystem: PostgreSQL 10.2, .NET-Provider Npgsql

Hallo,

ich möchte in einem WPF-Programm folgendes machen:

  • In einem DataGrid werden Datensätze angezeigt.
  • Auf Knopfdruck wird in der zugrundeliegenden Datenbank eine gespeicherte Prozedur ausgeführt, die Felder in eben diesen Datensätzen ändert.
  • Die Datensätze werden mit einer neuen Abfrage wieder aus der Datenbank geholt und im gleichen DataGrid angezeigt.

Für den ganzen Vorgang wird eine Instanz der passenden von DbContext abgeleiteten Klasse benutzt, die beim Öffnen des Fensters erzeugt und beim Schließen zerstört wird.

Das funktioniert so nicht: nach der zweiten Abfrage werden im DataGrid die unveränderten Daten angezeigt. Ich kann mich aber davon überzeugen, dass die gespeicherte Prozedur richtig ausgeführt wurde, z.B. indem ich das Fenster schließe und neu öffne.

Also habe ich ein kleines Konsolenprogramm gebastelt, das etwas Ähnliches tut:

  • Abfrage an die Datenbank, Ergebnisse anzeigen
  • Direkter UPDATE-Befehl an die Datenbank (context.Database.ExecuteSqlCommand(...))
  • Wiederholung von Abfrage und Ergebnisanzeige

Tut richtig, wenn die drei Aktionen in getrennten DbContext-Instanzen ablaufen; ich nehme sehr stark an, dass ich die ersten beiden auch zusammenfassen könnte, habe das aber noch nicht probiert. Läuft alles im gleichen Kontext ab, dann hat die zweite Abfrage das gleiche Ergebnis wie die erste.

Fragen, die ich mit Hilfe der Dokumentation nicht lösen konnte:

  • Was genau passiert da eigentlich? Ich lese immer und überall, dass Abfragen an den DbContext an die Datenbank geschickt werden - wenn man es anders haben will, muss man das extra einrichten. Auch wenn der SQL-Befehl in seiner eigenen Transaktion ausgeführt wird, müsste eine Abfrage, die danach abgesetzt wird, doch das Ergebnis schon kennen? Oder wird vielleicht der SQL-Befehl ohne mein ausdrückliches Zutun in einem eigenen Thread ausgeführt und das "danach" stimmt so eben nicht?

  • Ist eine neue DbContext-Instanz das Mittel der Wahl oder geht es auch anders?

Ich hänge das Konsolenprogramm mal an, obwohl der Beitrag sowieso schon lang ist. Es benutzt eine PostgreSQL-Version der Northwind-Datenbank, daraus nur drei Felder der Tabelle "Customers". Die zwei getrennten Namensräume sind hier natürlich ein schlechter Witz.

Danke für Hinweise aller Art!

#######################################################################

//Customer.cs
using System;

namespace Nordwind_Daten
{
    public class Customer
    {
        public string CustomerId { get; set; }
        public string CompanyName { get; set; }
        public string Region { get; set; }

        public override string ToString() => String.Format("Customer Id {0}, Company {1}, Region {2}",
                CustomerId, CompanyName, Region);
    }
}

//CustomerMap.cs
using System.Data.Entity.ModelConfiguration;

namespace Nordwind_Daten
{
    internal class CustomerMap : EntityTypeConfiguration<Customer>
    {
        public CustomerMap()
        {
            ToTable("customers");
            Property(c => c.CustomerId).HasColumnName("customerid")
                .IsFixedLength().HasMaxLength(5).IsRequired();
            Property(c => c.CompanyName).HasColumnName("companyname")
                .HasMaxLength(40).IsRequired();
            Property(c => c.Region).HasColumnName("region").HasMaxLength(15);
        }
    }
}

//NordwindDB.cs
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace Nordwind_Daten
{
    public class NordwindDB : DbContext
    {
        static NordwindDB()
        {
            Database.SetInitializer<NordwindDB>(null);
        }

        public NordwindDB() : base("name=NWConn")
        { }

        public DbSet<Customer> CustomerSet { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
            modelBuilder.HasDefaultSchema("public");
            modelBuilder.Configurations.Add(new CustomerMap());
        }

    }
}

//Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Npgsql;
using NpgsqlTypes;
using Nordwind_Daten;

namespace ExecSql_Nordwind
{
    class Program
    {
        static string GetCustomersByIdStart(string idstart, NordwindDB CDB)
        {
            var sb = new StringBuilder();
            //using (var CDB = new NordwindDB())
            //{
                var query = from cust in CDB.CustomerSet
                            where cust.CustomerId.StartsWith(idstart)
                            orderby cust.CustomerId
                            select cust;
                foreach (var cust in query)
                {
                    sb.AppendLine(cust.ToString());
                }
            //}
            return sb.ToString();
        }

        static int ChangeRegion(string idstart, string newchar, NordwindDB CDB)
        {
            string sql = "UPDATE customers SET region = CONCAT(region, :newchar) " +
                "WHERE customerid LIKE :idstart";
            //using (var CDB = new NordwindDB())
            //{
                var par_nc = new NpgsqlParameter(":newchar", NpgsqlDbType.Char, 1);
                var par_ids = new NpgsqlParameter(":idstart", NpgsqlDbType.Char, 2);
                par_nc.Value = newchar;
                par_ids.Value = idstart + "%";
                int cnt = CDB.Database.ExecuteSqlCommand(sql, par_nc, par_ids);
                return cnt;
            //}
        }

        static void Main(string[] args)
        {
            string newchar = "*";
            string appchar = "C";
            using (var CDB = new NordwindDB())
            {
                Console.WriteLine(GetCustomersByIdStart(appchar, CDB));
                int z = ChangeRegion(appchar, newchar, CDB);
                Console.WriteLine("Ergebnis von ChangeRegion: {0}", z);
                Console.WriteLine(GetCustomersByIdStart(appchar, CDB));
            }
            Console.Write("Beenden mit beliebiger Taste ...");
            Console.ReadKey();
        }
    }
}

16.842 Beiträge seit 2008
vor 6 Jahren

Sending raw commands to the database

Note that any changes made to data in the database using ExecuteSqlCommand are opaque to the context until entities are loaded or reloaded from the database.

Ein Kontext kann einfach nicht das tracken, was Du an ihm vorbei schleust.

Warum Du einen ORM verwendest, um dann SQL Code Raw zu senden, erschließt sich mir nicht.
Das Verhalten ist - so wie ich das sehen - genau das.

B
bb1898 Themenstarter:in
112 Beiträge seit 2008
vor 6 Jahren

Sending raw commands to the database

Note that any changes made to data in the database using ExecuteSqlCommand are opaque to the context until entities are loaded or reloaded from the database.
Ein Kontext kann einfach nicht das tracken, was Du an ihm vorbei schleust.

Das ist mir nicht neu, aber:

So far you’ve used LINQ to query a DbSet directly, which always results in a SQL query being sent to the database to load the data. Quelle: Lerman, Julia; Miller, Rowan: Programming Entity Framework : DbContext. O'Reilly Media, 2012. Kap. 2: Querying with DbContext; Querying Local Data. Blöderweise finde ich in meiner EBook-Ausgabe keine Seitenzahl dafür.

Daraus schließe ich doch, dass die Abfrage nach der Ausführung des SQL-Kommandos die Entities eben gerade neu aus der Datenbank lädt.

Warum Du einen ORM verwendest, um dann SQL Code Raw zu senden, erschließt sich mir nicht.
Das Verhalten ist - so wie ich das sehen - genau das.

So weit ich sehen kann, ist das die einzige Möglichkeit, aus dem EF heraus die Ausführung einer gespeicherten Prozedur anzustoßen. Und an einer Stelle in meinem Programm soll eben das stattfinden.

T
2.224 Beiträge seit 2008
vor 6 Jahren

@bb1898
Das Verhalten laut Doku ist das korrekte und hat auch Vorrang für allen anderen nicht offziellen Aussagen/Dokumentationen.

Es bringt nichts, auf ein Buch von 2012 aufzubauen, wenn die Doku dir genau das Gegenteil sagt.
Wie soll der OR Mapper auch wissen, dass deine RAW Query Auswirkungen auf deine Entitäten haben kann?
Soll er jede Abfrage zusätzlich analysieren, was enorm Performance kosten würde?
Das macht kein OR Mapper und kann man auch nicht von einem OR Mapper verlangen.

Wenn du die aktuellen Daten willst, musst du entweder einen neuen Context verwenden oder den aktuellen neuladen.

Was mich aber stört ist, warum die eine Prozedur im Server aufrufen musst.
Klingt für mich wie das auslagern von Business Logik in die DB, was du nie machen solltest.
Wenn es keinen Grund gibt dafür, dann ersetzt die Logik der Prozedur durch Code bei dir.
Sonst kannst du dir den OR Mapper sparen oder auf einen Micro OR Mapper ala Dapper umsteigen.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.842 Beiträge seit 2008
vor 6 Jahren

@bb1898
Das Verhalten laut Doku ist das korrekte und hat auch Vorrang für allen anderen nicht offziellen Aussagen/Dokumentationen.

Naja, Rowan Miller war bis letztes Jahr PM des Entity Framework.
Ich würde ihn daher durchaus als offizielle Quelle bezeichnen. Vielleicht nicht ganz so schnell urteilen 😉

Aber ja, eine 6 Jahre alte Quelle zitieren, zu deren Zeit es nicht mal EF 6 gab, ist halt Käse - genauso wie ein ORM verwenden, aber dann mit Stored Procs und Raw SQL arbeiten.
Wenn sich das auf diese eine Stelle bezieht, ist das auch keine große Tragik - und sollte auch ein T-Virus nicht persönlich "stören".
Jedenfalls ist das Verhalten des EF wohl hier korrekt und dokumentiert.

Lass halt mal einen Profiler laufen, ob auch wirklich nen Query an die DB gesendet wird, oder ob Du in ein Caching Feature gelaufen bist, das es halt 2012 noch nicht gab.

M
48 Beiträge seit 2011
vor 6 Jahren
  1. Möglichkeit: Kontext CDB neu instanziieren und Daten neu laden.
  2. Möglichkeit:

public void RefreshAll()
{
     foreach (var entity in CDB.ChangeTracker.Entries())
     {
           entity.Reload();
     }
}

  1. Möglichkeit:

ObjectContext objectContext = ((IObjectContextAdapter)CDB).ObjectContext;
ObjectSet<Transactions> set = objectContext.CreateObjectSet<Customer>();
set.MergeOption = MergeOption.OverwriteChanges;
List<Customer> customers = set.Where(c => c.CustomerId.StartsWith(idstart)).ToList();

B
bb1898 Themenstarter:in
112 Beiträge seit 2008
vor 6 Jahren

Nochmalige Suche in der EF-Dokumentation hat wohl die entscheidende Stelle zutage gefördert:

Note that DbSet and IDbSet always create queries against the database and will always involve a round trip to the database even if the entities returned already exist in the context. A query is executed against the database when:

It is enumerated by a foreach (C#) or For Each (Visual Basic) statement.  
It is enumerated by a collection operation such as ToArray, ToDictionary, or ToListenter link description here.  
LINQ operators such as First or Any are specified in the outermost part of the query.  
The following methods are called: the Load extension method on a DbSet, DbEntityEntry.Reload, and Database.ExecuteSqlCommand.  

When results are returned from the database, objects that do not exist in the context are attached to the context. If an object is already in the context, the existing object is returned (the current and original values of the object's properties in the entry are not overwritten with database values). Finding entities using a query

Das dürfte der Knackpunkt sein. Es widerspricht im Übrigen auch meinem alten Lehrbuch nicht, wird dort halt nur nicht erwähnt (an der Stelle liegt der Fokus auf dem Vermeiden unnötiger Datenbankabfragen).

So dass ich also eine der von @Marco_GR aufgezählten Varianten brauche (Danke schön!). Damit scheint mir das Problem gelöst, ich habe den Thread als erledigt markiert.