André Krämers Blog

Lösungen für Ihre Probleme

Kürzlich erhielt ich eine Frage von einem Leser meines Buches Cross-Plattform-Apps entwickeln mit Xamarin.Forms, der auf Seite 336 des Buches auf meine Behandlung von SQLite-Datenbankverbindungen in Xamarin.Forms aufmerksam wurde. Der Leser bemerkte, dass ich im Beispielcode eine Verbindung im Konstruktor öffnete und diese dauerhaft geöffnet hielt, ohne Close oder Dispose aufzurufen. Der Leser befürchtete, dass dies zu Speicherlecks oder sogar Datenverlust führen könnte und wies darauf hin, dass in Foren oft empfohlen wird, Verbindungen nur kurz zu öffnen und sofort wieder zu schließen.

In diesem Blogpost möchte ich auf diese Bedenken eingehen und die verschiedenen Ansätze zum Umgang mit SQLite-Datenbankverbindungen in Xamarin.Forms diskutieren. Die folgenden Ausführungen gelten übrigens nicht nur für Xamarin.Forms, sondern auch für .NET MAUI. In meinem Buch [Cross-Plattform-Apps entwickeln mit .NET MAUI] (/maui-buch/) verwende ich schließlich ein nahezu identisches Beispiel zum Umgang mit SQLite-Connections mit .NET MAUI wie im Xamarin-Buch.

Das ausschlaggebende Codebeispiel zur Verwendung von SQLite aus meinem Xamarin-Buch

Die Codezeilen, um die es geht, finden sich hier im Github-Repository zum Xamarin-Buch bzw. hier im Github-Repository zum .NET-MAUI-Buch.

Vereinfacht sieht der Code so aus:

using SQLite;

namespace LocalDataSample.Services
{
    public class DbDataStore : IDataStore<Item>
    {

        private const SQLiteOpenFlags SqliteFlags =
            SQLiteOpenFlags.ReadWrite |
            SQLiteOpenFlags.Create |
            SQLiteOpenFlags.SharedCache;
        
        private static readonly string DatabasePath = 
            Path.Combine(Xamarin.Essentials.FileSystem.AppDataDirectory, "items.db3");

        private static SQLiteAsyncConnection _database;


        public DbDataStore()
        {
            _database = new SQLiteAsyncConnection(DatabasePath, SqliteFlags);
            _database.CreateTableAsync<Item>().Wait();
        }
       
       // hier steht der eigentliche Datenzugriffscode
    }
}

Meine Empfehlung im Buch lautet:

Eine Verbindung zu einer SQLite-Datenbank sollte in Ihrer App möglichst einmalig hergestellt und dauerhaft geöffnet bleiben.

Das ist natürlich nur die halbe Wahrheit, schließlich gibt es in der Softwareentwicklung fast nichts, was sich pauschal beantworten lässt. Werfen wir also einen Blick auf die Alternativen.

Langlebig vs. kurzlebig

SQLite-Datenbankverbindungen können in .NET MAUI und Xamarin.Forms-Apps entweder langlebig oder kurzlebig sein. In meinem Buch empfehle ich den langlebigen Ansatz, der häufig durch ein Singleton realisiert wird.

Dies kann unter Xamarin.Forms erreicht werden, indem die Datenzugriffsklasse im DependencyService wie folgt als Singleton registriert wird: DependencyService.RegisterSingleton<DbDataStore>();. In einer .NET MAUI Anwendung wäre das Pendant dazu die Singleton-Registrierung in der Servicecollection: builder.Services.AddSingleton<IDataStore<Item>, DbDataStore>();.

Durch die Registrierung als Singleton bleibt die Verbindung während der gesamten Lebensdauer der Anwendung offen. Dieser Ansatz bietet folgende Vorteile

  • Bessere Performance durch Wiederverwendung der Verbindung und geringere CPU-Belastung.
  • Einfacherer Code, da das wiederholte Öffnen und Schließen von Verbindungen entfällt.

Aber wo Licht ist, ist auch Schatten. Daher möchte ich die folgenden Nachteile des Singleton-Ansatzes nicht verschweigen:

  • Eine offene Verbindung verbraucht mehr Ressourcen, insbesondere wenn sie längere Zeit nicht benutzt wird.
  • In Multithreading-Szenarien kann der Singleton-Ansatz zu Problemen mit gleichzeitigen Zugriffen und möglicherweise zu Inkonsistenzen in der Datenbank führen, da SQLiteConnection-Objekte nicht gleichzeitig in mehreren Threads verwendet werden dürfen.

Als Alternative kann die Datenzugriffsklasse transient registriert werden, wodurch sie kurzlebig wird. Benutzt dazu einfach die Methode Register statt RegisterSingleton des DependencyService: DependencyService.Register<DbDataStore>(); bzw. AddTransient statt AddSingleton in .NET MAUI: builder.Services.AddTransient<IDataStore<Item>, DbDataStore>();.

In der Datenzugriffsklasse sollte dann noch das Interface IDisposable implementiert werden:

public class DbDataStore : IDataStore<Item>, IDisposable
{
    // ...

    public void Dispose()
    {
        _database.Close();
        _database.Dispose();
    }
}

Sobald die Datenzugriffsklasse nicht mehr benötigt wird, ruft man einfach Dispose auf und schließt damit die Datenverbindung ordnungsgemäß.

Die Vorteile dieser Lösung sind:

  • Fehler oder Probleme durch gleichzeitige Zugriffe auf die Datenbank werden vermieden.
  • Der Speicherverbrauch wird reduziert, da die Verbindung nur bei Bedarf geöffnet wird.

Der Nachteil ist, dass das Öffnen und Schließen der Verbindung bei jedem Zugriff zu einer erhöhten CPU-Belastung und damit zu einer schlechteren Performance führt, insbesondere bei häufigen Datenbankzugriffen.

Fazit

SQLite-Datenbankverbindungen können in .NET MAUI und Xamarin.Forms auf verschiedene Arten gehandhabt werden. Wie wir in diesem Artikel gesehen haben, hängt die Entscheidung von den Anforderungen der Anwendung ab. Wenn eure Anwendung nur wenige Datenbankzugriffe hat und ihr euch Sorgen um die Ressourceneffizienz macht, dann empfehle ich, die Verbindung nach jedem Zugriff zu schließen. Wenn jedoch die Performance bei vielen Datenbankzugriffen eine höhere Priorität hat, dann ist der Singleton-Ansatz besser.