Przejście do treści

Bazy danych i ORM-y w .NET MAUI — Realm, Entity Framework, SQLite i Dapper w porównaniu

Praktyczne porównanie czterech popularnych podejść do persystencji danych w aplikacjach .NET MAUI. Wydajność, developer experience i rekomendacje dla różnych przypadków użycia.

Paweł Rudnicki
10 min
  • .NET MAUI
  • SQLite
  • Entity Framework
  • Realm
  • Dapper

Wstęp

Każda aplikacja mobilna musi przechowywać dane lokalnie — czy to ustawienia użytkownika, zcache'owane odpowiedzi API, czy pełne zestawy danych offline-first. W ekosystemie .NET MAUI mamy do dyspozycji kilka dojrzałych opcji, z których każda ma swoje kompromisy.

Po latach budowania produkcyjnych aplikacji MAUI miałem okazję przetestować w boju wszystkie cztery podejścia opisane w tym artykule. Przeanalizujmy je.

SQLite — klasyczny fundament

SQLite to de facto standard lokalnego przechowywania danych na urządzeniach mobilnych. Z biblioteką sqlite-net-pcl dostajemy lekką, bezkonfiguracyjną bazę danych:

  • Minimalny footprint — cały silnik to jeden plik

  • Cross-platform — działa identycznie na Androidzie, iOS, Windows i macOS

  • Oparty na SQL — znajome zapytania dla każdego, kto zna relacyjne bazy danych

  • Dojrzały i sprawdzony — używany na miliardach urządzeń na całym świecie

public class ArticleRepository
{
    private readonly SQLiteAsyncConnection _db;

    public ArticleRepository(string dbPath)
    {
        _db = new SQLiteAsyncConnection(dbPath);
        _db.CreateTableAsync<Article>().Wait();
    }

    public Task<List<Article>> GetAllAsync()
        => _db.Table<Article>().ToListAsync();

    public Task<Article> GetByIdAsync(int id)
        => _db.Table<Article>()
              .Where(a => a.Id == id)
              .FirstOrDefaultAsync();

    public Task SaveAsync(Article article)
        => article.Id == 0
            ? _db.InsertAsync(article)
            : _db.UpdateAsync(article);
}

Kiedy wybrać SQLite?

SQLite jest idealny dla prostych do średnio złożonych modeli danych, gdzie chcemy pełnej kontroli nad zapytaniami, minimalne zależności i maksymalną przenośność. To wybór numer jeden, gdy schemat danych jest prosty i nie potrzebujemy zaawansowanych funkcji ORM.

Entity Framework Core — pełnoprawny ORM

EF Core przynosi pełną moc ORM do .NET MAUI dzięki providerowi SQLite:

  • Podejście code-first — definiuj modele w C#, a EF wygeneruje schemat

  • Zapytania LINQ — typowane, sprawdzane w czasie kompilacji

  • Change tracking — automatyczne wykrywanie zmian i batch updates

  • Migracje — wersjonowanie schematu bazy razem z kodem

public class AppDbContext : DbContext
{
    public DbSet<Article> Articles => Set<Article>();
    public DbSet<Tag> Tags => Set<Tag>();

    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<Article>()
            .HasMany(a => a.Tags)
            .WithMany(t => t.Articles);
    }
}

var recentArticles = await _db.Articles
    .Include(a => a.Tags)
    .Where(a => a.PublishedAt > DateTime.UtcNow.AddDays(-30))
    .OrderByDescending(a => a.PublishedAt)
    .Take(10)
    .ToListAsync();

Kiedy wybrać Entity Framework Core?

EF Core sprawdza się najlepiej przy złożonych relacyjnych modelach danych z relacjami many-to-many, gdy potrzebujemy change trackingu lub chcemy współdzielić definicje modeli między klientem MAUI a backendem ASP.NET.

Realm — obiektowa baza danych

Realm reprezentuje fundamentalnie inne podejście — to baza obiektowa, nie relacyjna:

  • Architektura zero-copy — obiekty są odczytywane bezpośrednio z pliku bazy, bez deserializacji

  • Reaktywność — wbudowane notyfikacje o zmianach przez INotifyPropertyChanged

  • Thread-safe — automatyczne ograniczenie wątkowe z wzorcami cross-thread

  • Gotowy do synchronizacji — Atlas Device Sync do synchronizacji z chmurą (opcjonalnie)

public partial class Article : IRealmObject
{
    [PrimaryKey]
    [MapTo("_id")]
    public ObjectId Id { get; set; } = ObjectId.GenerateNewId();

    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public DateTimeOffset PublishedAt { get; set; }
    public IList<Tag> Tags { get; } = null!;
}

var recentArticles = _realm.All<Article>()
    .Where(a => a.PublishedAt > DateTimeOffset.UtcNow.AddDays(-30))
    .OrderByDescending(a => a.PublishedAt);

_realm.Write(() =>
{
    _realm.Add(new Article
    {
        Title = "Nowy wpis",
        Content = "Witaj, świecie!",
        PublishedAt = DateTimeOffset.UtcNow
    });
});

Kiedy wybrać Realm?

Realm sprawdza się znakomicie w aplikacjach wymagających reaktywnych aktualizacji UI w czasie rzeczywistym, synchronizacji offline-first z backendem chmurowym lub przy głęboko zagnieżdżonych grafach obiektów, które byłyby uciążliwe w modelu relacyjnym.

Dapper — micro-ORM

Dapper plasuje się między surowym SQLite a pełnym ORM-em. Mapuje wyniki SQL na obiekty z minimalnym narzutem:

  • Surowy SQL z mapowaniem — piszesz zapytania, Dapper mapuje wyniki

  • Błyskawicznie szybki — praktycznie zero narzutu w porównaniu z surowym ADO.NET

  • Lekki — jeden pakiet NuGet, brak złożonej konfiguracji

  • Elastyczny — procedury składowane, multi-mapping, dynamiczne parametry

public class ArticleRepository
{
    private readonly IDbConnection _connection;

    public ArticleRepository(string dbPath)
    {
        _connection = new SqliteConnection($"Data Source={dbPath}");
    }

    public async Task<IEnumerable<Article>> GetRecentAsync()
    {
        const string sql = @"
            SELECT a.*, t.Name as TagName
            FROM Articles a
            LEFT JOIN ArticleTags at ON a.Id = at.ArticleId
            LEFT JOIN Tags t ON at.TagId = t.Id
            WHERE a.PublishedAt > @Since
            ORDER BY a.PublishedAt DESC";

        return await _connection.QueryAsync<Article>(
            sql,
            new { Since = DateTime.UtcNow.AddDays(-30) });
    }

    public async Task<int> InsertAsync(Article article)
    {
        const string sql = @"
            INSERT INTO Articles (Title, Content, PublishedAt)
            VALUES (@Title, @Content, @PublishedAt);
            SELECT last_insert_rowid();";

        return await _connection.ExecuteScalarAsync<int>(sql, article);
    }
}

Kiedy wybrać Dappera?

Dapper jest idealny, gdy chcesz mieć pełną kontrolę nad SQL z wygodą automatycznego mapowania obiektów. Świetnie sprawdza się na ścieżkach krytycznych wydajnościowo, przy złożonych zapytaniach, z którymi ORM-y sobie nie radzą, lub w zespołach preferujących SQL nad LINQ.

Porównanie

CechaSQLite (surowy)Entity FrameworkRealmDapper
TypWbudowana DBPełny ORMObiektowa DBMicro-ORM
Język zapytańSQLLINQLINQSQL
WydajnośćBardzo wysokaŚredniaBardzo wysokaBardzo wysoka
Krzywa uczeniaNiskaŚredniaŚredniaNiska
Change trackingNieTakTakNie
Reaktywne aktualizacjeNieNieTakNie
Synchronizacja z chmurąNieNieTak (Atlas)Nie
Rozmiar pakietuMałyDużyŚredniMały

Benchmarki wydajności

W moich testach na 10 000 rekordach na średniopółkowym urządzeniu z Androidem:

OperacjaSQLiteEF CoreRealmDapper
Insert (batch)120ms450ms95ms130ms
Odczyt wszystkich45ms180ms30ms50ms
Zapytanie (filtrowane)12ms85ms8ms15ms
Update (pojedynczy)3ms25ms2ms4ms

Uwaga: to wartości przybliżone — rzeczywista wydajność zależy od złożoności danych, urządzenia i projektu schematu.

Podsumowanie

Moje rekomendacje w zależności od typu projektu:

  1. Proste aplikacje z podstawowym storage → SQLite z sqlite-net — minimalna konfiguracja, spełnia swoją rolę

  2. Złożone dane relacyjne + współdzielone modele z backendem → Entity Framework Core — moc LINQ, migracje, znajome API

  3. Aplikacje offline-first z synchronizacją w czasie rzeczywistym → Realm — reaktywne aktualizacje, cloud sync, doskonała wydajność mobilna

  4. Aplikacje krytyczne wydajnościowo z kontrolą nad SQL → Dapper — praktycznie zerowy narzut, pełna elastyczność SQL

W praktyce często łączyłem podejścia: SQLite/Dapper do lekkiego cachowania obok EF Core dla głównego modelu danych w tej samej aplikacji. Nie bójcie się mieszać rozwiązań w zależności od potrzeb poszczególnych funkcjonalności.

Porozmawiajmy o planowanym projekcie

Pierwsza konsultacja jest bezpłatna i niezobowiązująca. Wystarczy krótki opis procesu oraz oczekiwanego rezultatu.

Porozmawiajmy o projekcie