Laden...

SQLite Datenbankverbindung nicht korrekt geschlossen?

Erstellt von tobi45f vor 3 Jahren Letzter Beitrag vor 3 Jahren 1.716 Views
T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 3 Jahren
SQLite Datenbankverbindung nicht korrekt geschlossen?

Hallo zusammen,

bei mir schließt sich die SQLite-Datenbankverbindung nicht korrekt (vermute ich), leider weiß ich nicht, woran es liegen mag. Über mein Programm möchte ich eine Datenbank einlesen, ggf. manipulieren und dann wieder mit dem ursprünglichen Programm (wozu die Datenbank gehört) öffnen. Besagtes Programm kann mdb Files und db Files als Basis verwenden. Mit mbd Files habe ich diesen Fehler nicht, nur bei den db Files. Allerdings habe ich mir sagen lassen, dass man doch eher bei Datenbank Interaktionen auf SQLite statt Access setzen sollte.

Nachdem ich die db Datenbank einlesen habe kommt, dass die datei nicht geöffnet werden kann, da sie von einer anderen Anwendung benutzt wird. Die Verbindung habe ich jedoch geschlossen und weiß nicht, wie ich die Verbindung alternativ schließen soll? Wenn man eine gewisse Zeit (ca. 10-20s) wartet, dann kann ich die Datei öffnen. Ganz richtig kann das aber nicht sein, da dieses Phänomen nur bei der db und nicht bei der mdb auftritt?! Und wenn ich meine Anwendung direkt nach dem Lesen schließe, dann kann das Programm die Datenbank direkt lesen und somit starten.

Vielen Dank für die Hilfe

Gruß Tobias

public async Task DatenbankEinlesen()
        {
            if (Path.GetExtension(filename) == ".mdb")
            {
                //Datenbankmodell einlesen MDB

                connectionstring = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + filename;
                using (OleDbConnection connection = new OleDbConnection(connectionstring))
                {
                    OleDbCommand cmd = new OleDbCommand();
                    cmd.Connection = connection;
                    await connection.OpenAsync();
                    await DatenbankEinlesen(cmd);
                    connection.Close();
                }
            }
            else if (Path.GetExtension(filename) == ".db")
            {
                //Datenbankmodell einlesen DB

                connectionstring = "URI=file:" + filename;
                using (SQLiteConnection connection = new SQLiteConnection(connectionstring))
                {
                    SQLiteCommand cmd = new SQLiteCommand();
                    cmd.Connection = connection;
                    await connection.OpenAsync();
                    await DatenbankEinlesen(cmd);
                    connection.Close();
                }
            }
            else
            {
                //unbekannte Datei
                
            }
        }

public async Task DatenbankEinlesen<T>(T cmd)
        {            
            if (cmd is SQLiteCommand)
            {
                SQLiteCommand command = cmd as SQLiteCommand;

                #region Element
                command.CommandText = Element.Cmd;
                var rdr = await command.ExecuteReaderAsync();
                ElementList = new List<Element>();
                while (rdr.Read())
                {
                    ElementList.Add(new Element()
                    {
                        ...

                    });                    
                }
                rdr.Close();
                #endregion

            }
            else if (cmd is OleDbCommand)
            {
                OleDbCommand command = cmd as OleDbCommand;
                
                #region Element
                command.CommandText = Element.Cmd;
                var rdr = await command.ExecuteReaderAsync();
                ElementList = new List<Element>();
                while (rdr.Read())
                {
                    ElementList.Add(new Element()
                    {
                        ...

                    });
                }
                rdr.Close();
                #endregion

            }   
         ...         
        }

16.828 Beiträge seit 2008
vor 3 Jahren

Es ist generell etwas ungünstig, wie Du das machst - um es nett zu formulieren.
Dein Problem ist im Endeffekt ein Folgefehler, weil Du das grundlegend suboptimal angehst.

  • In einer Klasse über if-Abfragen Datenbanken zu untersützen: keine gute Idee. So funktioniert OOP nicht.
  • Für jeden Command eine SQL Verbindung zu öffnen - seltenst eine gute Idee.
  • using() auf die Connection sorgt bereits für ein Close() (über Dispose). Du schließt also doppelt.
  • Der Command sollte auch in ein using
  • Die Resultate übergibst Du offenbar durch Klasseneigenschaften. Wieso verwendest Du nicht den Methoden-Return?

Schau Dir an wie OOP funktioniert, erstell Dir ein gemeinsames Interface und pro Datenbank-Provider eine entsprechende Basis-Klasse.
ADO.NET hat mit IDbCommand bereits eine Basis für solche Aufgaben.

Wenn Du Dir mehr Aufgaben abnehmen lassen willst, dann verwende zB Dapper als Micro ORM.
Dann musst Dich um sowas nicht mehr kümmern.

Davon abgesehen: Access ist keine echte Datenbank. Es ist für solch einen Zweck überhaupt nicht gemacht, wird aber leider immer wieder missbraucht - mit meist fatalen Resultaten.
Ja, Sqlite ist der bessere Weg.

T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 3 Jahren

Danke für die schnelle Antwort.

Von der Struktur her habe ich es so, dass meine Klasse "Datenbankmodell" Listen von den jeweiligen Klassen (wie im Code dargestellt z.B. die Liste von Elements) enthält und eben die Methoden, um diese zu füllen. Da dachte ich wäre es sinnvoll, einmalig die Datenbank zu öffnen und über die Commands und den Reader die Daten einzulesen und meine Klasse mit Inhalt zu füllen. Das Füllen macht er entweder mit einer db oder mdb Datenbank, je nach dem. Mehr kann oder macht die Klasse nicht.

Wieso ich das in eine Klasse gepackt habe ist, dass das eingelesene Datenbankmodell identisch ist, egal ob die Quelle SQLite oder Access ist. Derzeit verwenden wir hauptsächlich die Accessdatenbank im Hintergrund, soll aber auf SQLite umgestellt werden. Daher ist beides enthalten. OOP mäßig nicht sauber, aber das war jetzt irgendwie eine praktikable Lösung, wenn ich nachher, unabhängig der Datenquelle, ein Datenbankmodell haben möchte.

Öffne ich für jeden Command eine eigene Connection? Ich öffne doch einmal im Using die Connection und übergebe dann den Command an die Methode, wo der Reader arbeitet und die Connection wird am Ende geschlossen. Oder sehe ich da was falsch?

OleDbCommand cmd = new OleDbCommand();
                    cmd.Connection = connection;

Dieser Teil wird dann über unterschiedliche Tabellenblätter wiederholt.


#region Element
                command.CommandText = Element.Cmd;
                var rdr = await command.ExecuteReaderAsync();
                ElementList = new List<Element>();
                while (rdr.Read())
                {
                    ElementList.Add(new Element()
                    {
                        ...

                    });
                }
                rdr.Close();
#region Line
                command.CommandText = Line.Cmd;
                var rdr = await command.ExecuteReaderAsync();
                LineList = new List<Line>();
                while (rdr.Read())
                {
                   LineList.Add(new Line()
                    {
                        ...

                    });
                }
                rdr.Close();

Doppelt schließen habe ich beseitigt, der Command sitzt jetzt auch im Using und damit hat sich auch wohl das Problem behoben. Jetzt ist die Datenbank direkt wieder frei zur Nutzung. Danke für den Hinweis! Gerne möchte ich mich aber auch so "weiteentwicklen" und die restlichen Anmerkungen verstehen bzw. verbessern!
Was genau meinst du mit dem letzten Punkt, dass ich die Resultate über den Methoden Return übergeben soll? Dass ich die Daten eher über eine TryRead-Methode umsetzen soll?

Ich habe meine Programmierkenntnisse, neben zwei Vorlesungen damals in der Uni, eher problemorientiert weiterentwickelt X( Interfaces habe ich mir beispielsweise noch nicht im Detail angeguckt, da ich bisher keine Verwendung/keine Problemstellung in der Richtung hatte. Aber scheinbar ist jetzt die Zeit dafür gekommen 😉

Bzgl. des Dappers meisnt du sowas: https://www.youtube.com/watch?v=lT3QrMykGUY ?! ok, dass ist wirklich wesentlich entspanner, als den Code zu schreiben 😁 Das hat mich für meine 15 Tabellenblätter auch gut Zeit gekostet 😉

Vielen Dank,
Gruß Tobias

16.828 Beiträge seit 2008
vor 3 Jahren

Von der Struktur her habe ich es so, dass meine Klasse "Datenbankmodell" Listen von den jeweiligen Klassen (wie im Code dargestellt z.B. die Liste von Elements) enthält und eben die Methoden, um diese zu füllen.

Hab ich verstanden; ist aber keine gute Idee. Machs lieber richtig 😉
Gerade weil es "identisch" ist solltest Du OOP verwenden: dafür ist es da.

public interface ISqlRepository {}
public class SqlLiteRepository : ISqlRepository {}
public class AccessDbRepository : ISqlRepository {}

Öffne ich für jeden Command eine eigene Connection?

Die Struktur ist sehr unübersichtlich - und unüblich.

      using (SQLiteConnection connection = new SQLiteConnection(connectionstring))
                {
                    SQLiteCommand cmd = new SQLiteCommand();
                    cmd.Connection = connection;
                    await connection.OpenAsync();
                    await DatenbankEinlesen(cmd);
                    connection.Close();
                }

Wenn man das betrachtet öffnet es pro cmd, woher das cmd auch immer kommt, eine Verbindung.

Was genau meinst du mit dem letzten Punkt, dass ich die Resultate über den Methoden Return übergeben soll?

Aktuell machst Du offenbar viel über Eigenschaften einer Klasse. Das ist ein riesen Fallstrick für Race Conditions - und nicht gut.

Verwende Methodenparameter für Eingangswerte und Methodenrückgaben für Ergebnisse - aber keine Klasseneigenschaften; dafür sind sie nicht da.

public async Task<List<TEntity>> DatenbankEinlesen<TCommand, TEntity>(TCommand cmd)
{
    var rdr = await command.ExecuteReaderAsync();
    var list = new List<TEntity>();
    while (rdr.Read())
    {
      // read stuff

    }
    return list;
}

Dapper: https://github.com/StackExchange/Dapper

Das hat mich für meine 15 Tabellenblätter auch gut Zeit gekostet

Gut, Du hast auch quasi null OOP und hast vermutlich alles doppelt und dreifach geschrieben.

4.938 Beiträge seit 2008
vor 3 Jahren

Und noch ein Hinweis:
Eine generische Methode (wie deine DatenbankEinlesen<T>(T cmd)), welche intern konkrete Typen abfragt, ist ein Anti-Pattern.

Wie das mit den Interfaces (IDBCommand, IDBConnection, ...) funktioniert, kannst du dir z.B. in Simplified Database Access via ADO.NET Interfaces anschauen.

T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 3 Jahren

Wenn man das betrachtet öffnet es pro cmd, woher das cmd auch immer kommt, eine Verbindung.

Also ich meine bei Verbindung dann das englische Wort Connection und die wird ja einmalig im Using geöffnet. Und darunter wird dann je Tabelle, wo ich Daten raussuche, je ein cmd mit der Select Anweisung gelesen. Wie mache ich das denn dann mit nur einer Verbindung, wenn jetzt hier bei jedem Befehl eine Verbindung aufgebaut wird?

Wenn ich es so mache, wie dein Beispielcode zeigt ( DatenbankEinlesen<TCommand, TEntity>(TCommand cmd)), wie rufe ich es denn auf, sodass ich nur eine Verbindung nutze?

so ganz verstehe ich noch nicht, wie es umgesetzt wird aber ich muss mir wohl mal die Zeit nehmen, bei OOP einen Punkt zu suchen, wo ich mich weiter einlesen kann - bzw. bei Interfaces von vorn 😄

Danke dir/euch!

16.828 Beiträge seit 2008
vor 3 Jahren

Schau Dir doch bitte mal bisschen Lektüre dazu an, wie bereits genannt; zB mit Dapper.
Bisher bist da ja bisschen auf grüner Wiese gestartet und hast keinen Rahmen gehabt.

Vermutlich werden dich 7/10 Tutorials auf irgendeine Weise zB auf den Repository Pattern aufmerksam machen.

Generic repository pattern using Dapper

Les Dir Tutorials durch, versuche die Inhalte anhand von eigenen Test-Codes zu verstehen - und Dir wird klar, wie das alles funktioniert 😉

Aber es macht keinen Sinn dass wir Dir das nun alles vorkauen, wenn es tausende fertiger Tutorials zu sowas gibt.
Denn das sind im Endeffekt "Basics" mit dem Umgang von Datenbanken mit ADO.NET. 😃

T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 3 Jahren

Ich habe mir dazu einiges angeguckt/durchgelesen. Den Sinn dahinter habe ich verstanden, umgesetzt noch nichts, da mir erstmal Fragen aufkommen, ob das bei meinem Programm "so" funktioniert.
Ich beschreibe kurz mein Vorhaben und skizziere dann meine Bedenken, die mir beim lesen zum Thema Dapper gekommen sind. Das ist vielleicht klüger.

Wir nutzen ein Netzberechnungsprogramm, alle elektrische Daten liegen in einer Access und bald SQLite Datenbank. Der Aufbau beider Datenbanken ist identisch. Ich möchte die Inhalte von Stammdaten-Tabellen und Berechnungsergebnis-Tabellen auslesen. Es sind 25 Stammdaten-Tabellen und 10 Tabellen für die Berechnungsergebnisse. Die Anzahl an Attributen beträgt von 5 bis zum Teil 30-40 Attributen.
Was ich damit anstellen möchte: Zunächst werden einige Tabellen gelesen, hier werden die Daten analysiert und zu Elektrische Leitungen mit all den Daten zusammengebaut (Also aus dem Knoten-Kanten-Modell mit Terminals, Sicherungen, Lasten, Erzeugungsleistungen etc werden die relevanten Kabelabschnitte zusammengebaut mit all den wichtigen Daten.) Die werden dann dargestellt. Der Nutzer hat dann zum einen einige Funktionen, um Änderungen am gesamten Modell vorzunehmen, aber auch an den dargestellten elektrischen Leitungen. Bei letzterem Geht es um die Änderung einzelner Werte, was sich ganz sicher wunderbar mit dem Dapper realisieren lässt. Bei anderen Funktionen geht es z.B. um die Anpassung der Koordinaten aller Werte - einer Verschiebung von einem Koordinatensystem in ein anderes, damit die Darstellung im Netzberechnungsprogramm klappt (kommt durch den Export aus dem GIS zustande, da GIS und Netzberechnungsprogramm andere Systeme verwenden).

Es leuchtet mir ein, dass die Verwendung von

public interface ISqlRepository {}
public class SqlLiteRepository : ISqlRepository {}
public class AccessDbRepository : ISqlRepository {}

absolut sinnvoll ist.
Auch, dass mein Ansatz in dieser Richtung nicht OOP entspricht ist korrekt. Dass hier Verbesserungsprotentiel besteht ebenso.

Bei den Erklärungen, die ich bisher gefunden habe, ging es immer um ++ein ++Tabellenblatt. In meinem Fall habe ich etwas mehr. Wenn ich die Datenauslese, wie hier gezeigt, wird dann nicht ebenfalls für das Auslesen jedes Tabellenblatts eine Verbindung aufgebaut und wieder geschlossen?
Weiterhin interessieren mich viele Attribute der einzelnen Objekte nicht. Oftmals brauche ich nur 4-5 Attribute zur Analyse/Darstellung, auch wenn das Objekt 40 hat. Ist ein GetAll dann nicht zu viel des Guten? Dann müsste ich eine Funktion schreiben, bei der ich dann einen SQL Befehl mitgebe - wenn das dann noch dem OOP Gedanken entspricht?
Und für beispielsweise die Anpassung der Koordinaten sind Anpassungen in 3 Tabellenblättern bei den zum Teil knappen tausend Objekten je Blatt notwendig. Da für die Berechnung der neuen Koordinate auch die alte Koordinate eine Rolle spielt. Derzeit sieht das bei mir so aus, dass ich eben die Befehle über die Instanzen meiner Liste ausführe, um so für jede Instanz die neue Koordinate zu berechnen.

using (SQLiteConnection connection = new SQLiteConnection(datenbankmodell.Connectionstring))
                {
                    SQLiteCommand cmd = new SQLiteCommand();
                    cmd.Connection = connection;
                    await connection.OpenAsync();

                    foreach(GraphicNode n in datenbankmodell.GraphicNodeList)
                    {
                        string strSQL = "UPDATE Node SET lon = " + Lon((n.Nodestartx.Value + n.Nodeendx.Value) / 2, (n.Nodestarty.Value + n.Nodeendy.Value) / 2)..... WHERE Node_ID = " + n.Node_id;
                        cmd = new SQLiteCommand(strSQL, connection);
                        await cmd.ExecuteNonQueryAsync();
                    }
                    connection.Close();
                }

Oder würde ich in der Schleife dann nur den Update-String definieren und dann x-Fach die Methode zum Updaten aufrufen?

Vielen Dank für die Hilfe!

16.828 Beiträge seit 2008
vor 3 Jahren

Wenn ich die Datenauslese, wie
>
gezeigt, wird dann nicht ebenfalls für das Auslesen jedes Tabellenblatts eine Verbindung aufgebaut und wieder geschlossen?

Die Best Practise sagt: eine Verbindung soll so kurz wie möglich offen sein.
Die reale Welt zeigt: das ist nicht überall gleich.

Letzten Endes kommt es auf die Anwendung an, wie kurz eine Datenbankverbindung offen sein soll.
Vor allem handelt man da lokale Datenbanken und zentrale Datenbanken oft auch anders.

  • Zentrale Datenbanken (auf einem Server) haben oft nur einen gewissen Verbindungspool; daher sollte man hier Verbindungen so schnell wie möglich freigeben, sodass sich viele Mitarbeiter nicht gegenseitig blockieren (wobei hier ein zentraler HTTP Services erfahrungsmäß die bessere Wahl ist - gibt aber immer noch Direktzugriffe von Clients)
  • Lokal ist hier die Priorität anders
  • Und dann noch die Technologie, weil zB Access (keine echte Datenbank ist und ) überhaupt nicht mit mehreren Verbindungen zeitgleich klarkommt

Attribute der einzelnen Objekte nicht.

Du sprichst von Datenbank-Feldern. Attribute sind was anderes.

Ist ein GetAll dann nicht zu viel des Guten?

Wenn Du nur gewisse Felder lädst, dann spricht man von einer Projektion.
Eine Projektion ist i.d.R. schneller - im Code im direkten Vergleich aber natürlich etwas komplexer was die Summe an Zeilen und die Methoden betrifft.

Die Frage ist nun: lohnt sich das für dich, wenn Du mit einer Projektion in 4ms lädst und alle Daten in 5ms?
Klar, prozentual ein Unterschied - aber merkt das der Nutzer lokal? Lohnt sich der Aufwand für 1ms?
KISS-Prinzip

Sofern Du bei Dapper bleibst musst Du entsprechend mehr hier selbst programmieren.
In EFCore zB. kann man sich die Projektionen (und die Expressions dahinter) jedoch dank AutoMapper automatisch erzeugen lassen.

Aber die Software Architektur kennt hier mehrere Wege; das ist eben Fall-bezogen.
Ich bin auch ein großer Fan von Projektionen - hab aber mit Anwendungen zutun die davon auch entsprechend aufgrund der Größe (und Nutzer) profitieren.
Lokal muss man abwägen wie viel Sinn das eben macht.

Dann müsste ich eine Funktion schreiben, bei der ich dann einen SQL Befehl mitgebe - wenn das dann noch dem OOP Gedanken entspricht?

Methode. Funktionen sind in OOP was anderes.

Oder würde ich in der Schleife dann nur den Update-String definieren und dann x-Fach die Methode zum Updaten aufrufen?

Du kannst alle Updates in einem Command abschicken. Dazu kannst Du entsprechende Modelle an genau der Methode annehmen.
Der Repository Pattern ist in den CRUD-Themen durchaus generisch; aber Du darfst in Repositories durchaus Methoden erstellen, die spezifische Zwecke erfüllen - zB. ein Mass-Update mit einer Auswahl von Feldern.

Achte aber darauf, dass Du Parameter verwendest. String-Frickerelein und Datenbanken mögen sich nicht so sehr.
[Artikelserie] SQL: Parameter von Befehlen

T
tobi45f Themenstarter:in
59 Beiträge seit 2017
vor 3 Jahren

Hallo zusammen!

In den Beispielen die ich gefunden habe, wurden die Datenbanken immer über die App.Config/Configmanager eingebunden.
Bei mir ist es ja keine Datenbank an einem Zentralen Ort. Somit habe ich es so gelöst, indem ich beim instanzieren, den Filename mitgebe und dann den Connectionstring dort vorhalte:

public class SqlLiteRepository<T> : ISqlRepository<T> where T : class
    {
        private readonly string _tableName;
        private readonly string _conString;

        public SqlLiteRepository(string filename, string tableName)
        {
            _tableName = tableName;
            _conString = "URI=file: " + filename;
        }
        private SQLiteConnection SqlConnection()
        {
            return new SQLiteConnection(_conString);
        }
        private IDbConnection CreateConnection()
        {
            var conn = SqlConnection();
            conn.Open();
            return conn;
        }
        public async Task<IEnumerable<T>> GetAllAsync()
        {
            using (var connection = CreateConnection())
            {
                return await connection.QueryAsync<T>($"SELECT * FROM {_tableName}");
            }
        }
   }

bzw.


public class AccessRepository<T> : ISqlRepository<T> where T : class
    {
        private readonly string _tableName;
        private readonly string _conString;

        public AccessRepository(string filename, string tableName)
        {
            _tableName = tableName;
            _conString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + filename;
        }
}

Aufruf:

public Datenbankmodell(string filename)
        {            
            _filename = filename;
        }
        public async Task LoadData()
        {
            ISqlRepository<Element> dbElement = new SqlLiteRepository<Element>(_filename, "Element");
            ElementList = await dbElement.GetAllAsync() as List<Element>;

            ISqlRepository<GraphicAreaPos> dbGraphicAreaPos = new SqlLiteRepository<GraphicAreaPos>(_filename, "GraphicAreaPos");
            GraphicAreaPosList = await dbGraphicAreaPos.GetAllAsync() as List<GraphicAreaPos>;
           ...
        }

Funktionieren tut es - ist das so "möglich" oder stylitisch eine Katastrophe?

Wie/wo unterscheide ich dann, ob Access oder SQLite eingelesen wird? Derzeit denke ich, dass es ich einmal die Methode public async Task LoadData() für SQLite habe und einmal für Access? also so:

public async Task LoadDataAccess()
        {
            
            ISqlRepository<Element> dbElement = new AccessRepository<Element>(_filename, "Element");
            ElementList = await dbElement.GetAllAsync() as List<Element>;
            
            ISqlRepository<GraphicAreaPos> dbGraphicAreaPos = new AccessRepository<GraphicAreaPos>(_filename, "GraphicAreaPos");
            GraphicAreaPosList = await dbGraphicAreaPos.GetAllAsync() as List<GraphicAreaPos>;
            
        }
if (Path.GetExtension(file) == ".db")
                {
db = new Datenbankmodell(pfad);
            await db.LoadData();
                }
                else if (Path.GetExtension(file) == ".mdb")
                {
db = new Datenbankmodell(pfad);
            await db.LoadDataAccess();
                }

Oder kann man das über die Interfaces besser abbilden, was ich bisher noch nicht verstanden habe?

Vielleicht ist diese Frage im Link von Th69 "Simplified Database Access via ADO.NET Interfaces"erklärt. Allerdings werde ich daraus leider nicht schlauer.

Das Einlesen für alle Tabellen dauert jetzt ca. 200ms statt zuvor 60ms. Aber ich denke, das ist vertretbar. So schnell arbeitet bei uns eh keiner 😄

Danke für die Hilfe!

16.828 Beiträge seit 2008
vor 3 Jahren
  • IEnumerable wandelt man mit ToList und nicht mit as um. Im Zweifel knallt das
  • Wenn Du List ohnehin schon als Typ zurück bekommst, dann verwende IList und nicht IEnumerable.
  • Die Materialisierung sollte teil des DALs sein und nicht durch die UI (ein Folgefehler durch IEnumerable) Intermediate Materialization (C#)

Das Einlesen für alle Tabellen dauert jetzt ca. 200ms statt zuvor 60ms.

Die Aussage so hart keine Wertigkeit, wenn Du nicht sagst was genau die Zeit kostet.
Und eine genaue Zeitaussage bekommst Du nur mit einem Profiler.
Measure application performance by analyzing CPU usage

Oder kann man das über die Interfaces besser abbilden, was ich bisher noch nicht verstanden habe?

Der Repository Pattern verwendet eigentlich Connection Sharing; Du hast pro Methodencall eine Connection.
Musst Du wissen, was für Dich besser ist. Hintergrund: Aus Architektursicht ist ein Repository nicht für den Aufbau und das Verwalten von Connections verantwortlich.

Anonsten: Du hast halt nun ein bisschen OOP aber kein Dependency Injection; vermutlich auch keinerlei Tests.

Dependency Injection ändert an der Funktion einer Anwendung nicht wirklich was; erleichtert aber das Entwicklerleben enorm - vor allem auch das Testen.
Wenn Du aber gar nicht testest, dann vermissen viele Leute auch kein DI - und daher siehst Du die offenen Nachteile, die Du durch die "Vereinachungen" erreicht hast, nicht.

Noch eine Bitte: das eigentliche Problem ist gelöst; der Sinn des Threads damit erfüllt.
Wenn es Dir nun nach Reviews geht, dann mach bitte ein Review Thema auf - ansonsten wird das ein Endlosthema.
Danke.