Aplikacje mobilne · Baza danych

Baza danych SQLite w aplikacji mobilnej: wprowadzenie

Pliki TXT i JSON dobrze służą prostym danym, ale gdy aplikacja ma przechowywać setki rekordów, sortować je i filtrować, zapis całej listy w jednym pliku przestaje wystarczać. Poznasz SQLite w MAUI: tabelę opisaną przez klasę modelu i pełny zestaw operacji dodawania, odczytu i usuwania.

  • około 60 minut
  • .NET MAUI · SQLite · sqlite-net-pcl
  • INF.04.6.2

Po tej lekcji: zainstalować pakiet sqlite-net-pcl, opisać tabelę atrybutami modelu, napisać klasę bazy z CreateTableAsync, InsertAsync, DeleteAsync i wczytać dane na ekran.

Własny notes z dodawaniem, edycją i usuwaniem

Kiedy notatek przybywa, potrzebujesz rekordów z identyfikatorem, a nie jednego długiego napisu. Najpierw uruchomimy listę z SQLite, potem sprawdzimy każdą operację CRUD.

Najpierw uruchomisz mały ekran. Potem połączysz go z kodem i sprawdzisz konkretne wyniki. Dodatki wykonuj dopiero, gdy wersja podstawowa działa. Quiz i oceniane zadania pozostają w biurku po zalogowaniu.

Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji

1 · Sprawdź w działaniu

Z atrybutów modelu powstaje tabela

Włączaj i wyłączaj atrybuty i obserwuj CREATE TABLE. Dodaj notatkę, zaznacz ją i usuń. Potem wyłącz CreateTableAsync i spróbuj dodać notatkę, a na końcu wyłącz [PrimaryKey], aby zobaczyć, co tracisz.

Najpierw przewidź, potem sprawdź: W modelu ORM porównaj właściwość Id z kolumną tabeli. Sprawdź PrimaryKey i AutoIncrement. Zastanów się, dlaczego edycja ma zachować dotychczasowe Id.

Ta interakcja wymaga włączonego JavaScriptu.

Klasa modelu opisuje tabelę, a CreateTableAsync<T>() ją tworzy. Bez klucza głównego nie zidentyfikujesz pojedynczego wiersza. Model pomaga zobaczyć mechanizm; właściwą aplikację uruchomisz z plików poniżej.

1. Przygotuj miejsce pracy

Co dodajesz, a co podmieniasz?

Stronę dodaj przez Projekt → Dodaj → Nowy element → .NET MAUI ContentPage (XAML). Powstaje para .xaml i .xaml.cs. Dla ContentView wybierz szablon ContentView; dla modeli i serwisów zwykłą klasę. Jeśli plik już istnieje, podmień wskazaną zawartość — nie twórz drugiej klasy o tej samej nazwie.

PlikRola w ćwiczeniu
projekt.csproj (fragment)Fragment w istniejącym pliku
Models/Notatka.csLogika i obsługa danych
Data/NotatkaDatabase.csLogika i obsługa danych
App.xaml.csLogika i obsługa danych
NotatkiPage.xamlWygląd strony lub kontrolki
NotatkiPage.xaml.csLogika i obsługa danych

Zanim uruchomisz: trzy połączenia

x:Class="MojaAplikacja.NazwaStrony" w XAML musi zgadzać się z namespace MojaAplikacja i nazwą klasy C#. InitializeComponent() tworzy kontrolki opisane w XAML.

x:Name="PoleImie" daje kodowi C# dostęp przez PoleImie.Text. Z kolei Clicked="Zapisz_Clicked" wskazuje metodę o tej nazwie w pliku .xaml.cs. Sam napis przycisku w Text niczego nie podpina.

Gdy dodajesz XAML z obsługą zdarzeń, od razu uzupełnij też odpowiadający C#. Uruchamiaj dopiero po skończeniu oznaczonej pary plików; brakujący handler nie jest błędem środowiska.

2. Uruchom sam wygląd — jeszcze bez logiki

W świeżym projekcie zastąp MainPage.xaml poniższym ekranem. W MainPage.xaml.cs zachowaj konstruktor z InitializeComponent(); usuń szablonowy licznik i OnCounterClicked, bo nie ma ich już w XAML. Na tym etapie App i AppShell pozostają z szablonu. To pełny, tymczasowy XAML — później świadomie go podmienisz, nie dokleisz do drugiego ContentPage.

MainPage.xaml — etap tymczasowyxaml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MojaAplikacja.MainPage">
    <VerticalStackLayout Padding="24" Spacing="14">
        <Label Text="Moje notatki" FontSize="24" />
        <Entry Placeholder="Treść notatki" />
        <Label Text="Pusty ekran to jeszcze nie pusta baza." />
    </VerticalStackLayout>
</ContentPage>
  1. Pierwsza próba Uruchom. Zobaczysz kontrolki, ale przyciski bez Clicked jeszcze nic nie robią. Sprawdź rozmiar i kolejność elementów. Zatrzymaj aplikację przed następnym etapem.

Zacznijmy od jednego pliku

Cała baza to jeden plik

W aplikacji mobilnej baza SQLite to jeden plik (.db3 lub .db) w prywatnym katalogu aplikacji. Nie instalujesz żadnego serwera i nie konfigurujesz niczego dodatkowego, a działa tak samo na Androidzie, iOS i Windows.

To połączenie z wiedzą z kwalifikacji INF.03 (tabela, klucz główny, SELECT, INSERT, DELETE), tylko w nowym kontekście: baza jest wbudowana w aplikację.

2 · Przygotowanie projektu

Pakiet sqlite-net-pcl

SQLite w MAUI obsługuje się przez pakiet NuGet. Zaznaczaj kroki w trakcie.

4 · Model = tabela

Atrybuty biblioteki SQLite

AtrybutZastosowanie
[PrimaryKey, AutoIncrement]Pole Id jest kluczem głównym, a baza sama nadaje kolejne numery (jak AUTO_INCREMENT w SQL).
[MaxLength(100)]Opisuje długość deklarowaną w schemacie. SQLite nie egzekwuje samego VARCHAR(n), więc długość sprawdzaj też w walidacji aplikacji.
[NotNull]Kolumna nie może być pusta (jak NOT NULL).
[Ignore]Właściwość klasy, która nie ma być zapisywana jako kolumna.
Nie przypisuj samodzielnie `Id` przed `InsertAsync`, jeśli pole jest auto-inkrementowane. Baza sama nada numer.

6 · Odpowiedniki SQL

Metody `SQLiteAsyncConnection`

MetodaOdpowiednik SQL
CreateTableAsync<T>()CREATE TABLE IF NOT EXISTS: tworzy tabelę, jeśli jej jeszcze nie ma (bezpieczne przy każdym starcie).
Table<T>().ToListAsync()SELECT * FROM tabela.
InsertAsync(obiekt)INSERT INTO ....
UpdateAsync(obiekt)UPDATE ... (po kluczu głównym).
DeleteAsync(obiekt)DELETE FROM ... WHERE Id = ....
Wszystkie metody są asynchroniczne, więc używasz ich z `await`.

4. Połącz wygląd z działaniem

Teraz zbuduj docelowy przykład. Pełne pliki poniżej zastępują swoje wcześniejsze wersje. Przeczytaj opis nad kodem i objaśnienia pod nim; potem sprawdź przepływ danych, nie tylko brak czerwonych podkreśleń.

Gdy przykład ma kilka plików zależnych od siebie, najpierw dodaj wszystkie wskazane typy, a potem uruchom całość. Ustaw breakpoint w obsłudze głównego przycisku i prześledź jedną próbę.

Budowa krok po kroku

Zależności w pliku projektu

To fragment do istniejącego projekt.csproj (fragment), nie cały plik. Zachowaj pozostałe ustawienia szablonu i dodaj podaną sekcję w odpowiednim miejscu.

Wersja użyta w kontroli tego przykładu: sqlite-net-pcl1.11.285. Dostarcza aktualne zależności SQLitePCLRaw3; nie dokładaj starego bundle_green2.x. Po aktualizacji przywróć pakiety i sprawdź komunikaty NuGet.

projekt.csproj (fragment)xml
<ItemGroup>
  <PackageReference Include="sqlite-net-pcl" Version="1.11.285" />
</ItemGroup>

Budowa krok po kroku

Notatka.cs

Atrybuty opisują mapowanie klasy na tabelę. Id identyfikuje rekord, a nie jego pozycję na ekranie. AutoIncrement nada wartość przy dodawaniu.

Każda właściwość publiczna staje się kolumną tabeli.

Models/Notatka.cscsharp
using SQLite;

namespace MojaAplikacja.Models;

public class Notatka
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }

    [MaxLength(100)]
    public string Tytul { get; set; } = "";

    public string Tresc { get; set; } = "";
}

Budowa krok po kroku

NotatkaDatabase.cs

Czytaj w kolejności: konstruktor i _inicjalizacja, InitAsync, pobranie, dodanie/aktualizacja, usunięcie. Wspólne Task inicjalizacji jest oczekiwane przez każdą operację.

Konstruktor uruchamia tworzenie tabeli i zachowuje Task, bez blokowania wątku przez .Wait(). Każda operacja najpierw await InitAsync(), więc czeka na to samo ukończenie inicjalizacji. Błąd inicjalizacji trafia do wywołującej metody; ekran powinien go przechwycić i pokazać komunikat.

Data/NotatkaDatabase.cscsharp
using SQLite;
using MojaAplikacja.Models;

namespace MojaAplikacja.Data;

public class NotatkaDatabase
{
    private readonly SQLiteAsyncConnection _database;
    private readonly Task _inicjalizacja;

    public NotatkaDatabase(string dbPath)
    {
        _database = new SQLiteAsyncConnection(dbPath);
        _inicjalizacja = _database.CreateTableAsync<Notatka>();
    }

    private Task InitAsync() => _inicjalizacja;

    public async Task<List<Notatka>> GetNotatkiAsync()
    {
        await InitAsync();
        return await _database.Table<Notatka>().ToListAsync();
    }

    public async Task<int> ZapiszNotatkeAsync(Notatka notatka)
    {
        await InitAsync();
        return await _database.InsertAsync(notatka);
    }

    public async Task<int> AktualizujNotatkeAsync(Notatka notatka)
    {
        await InitAsync();
        return await _database.UpdateAsync(notatka);
    }

    public async Task<int> UsunNotatkeAsync(Notatka notatka)
    {
        await InitAsync();
        return await _database.DeleteAsync(notatka);
    }
}
  1. private readonly Task _inicjalizacja Zapamiętana jedna operacja tworzenia tabeli. Nie uruchamiamy jej ponownie przy każdym zapytaniu.
  2. await InitAsync() Oczekiwanie asynchroniczne, przed odczytem i każdą zmianą. Nie używaj .Result ani .Wait() do blokowania UI.
  3. return await ... Po inicjalizacji wykonujemy właściwą operację i zwracamy jej wynik.

Budowa krok po kroku

App.xaml.cs

App tworzy jeden obiekt dostępu do bazy i wystawia App.Database. Plik danych trafia do AppDataDirectory. To nie to samo co otwieranie połączenia przy każdym kliknięciu.

Bazę tworzysz raz przy starcie i udostępniasz statycznie. Strony korzystają z App.Database.

App.xaml.cscsharp
using MojaAplikacja.Data;

namespace MojaAplikacja;

public partial class App : Application
{
    // Jedna, współdzielona instancja bazy dla całej aplikacji
    public static NotatkaDatabase Database { get; private set; } = null!;

    public App()
    {
        InitializeComponent();

        string dbPath = Path.Combine(FileSystem.AppDataDirectory, "notatki.db3");
        Database = new NotatkaDatabase(dbPath);
    }

    protected override Window CreateWindow(IActivationState? activationState)
    {
        return new Window(new NotatkiPage());
    }
}
  1. public static NotatkaDatabase Database Wspólna instancja dla całej aplikacji.
  2. Path.Combine(FileSystem.AppDataDirectory, "notatki.db3") Plik bazy w prywatnym katalogu aplikacji.

Budowa krok po kroku

NotatkiPage.xaml

Dodaj lub otwórz NotatkiPage.xaml i podmień jego zawartość. Najpierw odszukaj kontener, potem nazwane kontrolki oraz wskazane zdarzenia. Pozostaw zgodne x:Class i nazwę klasy w pliku .xaml.cs.

Zaznaczenie wypełnia pola. Dodaj tworzy nową notatkę, Zapisz zmiany aktualizuje zaznaczoną, Usuń usuwa ją z bazy.

NotatkiPage.xamlxaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:models="clr-namespace:MojaAplikacja.Models"
             x:Class="MojaAplikacja.NotatkiPage" Title="Notatki">
    <VerticalStackLayout Padding="16" Spacing="12">

        <Entry x:Name="TytulEntry" Placeholder="Tytuł notatki" />
        <Editor x:Name="TrescEntry" Placeholder="Treść notatki"
                AutoSize="TextChanges" HeightRequest="120" Margin="0,4,0,0" />
        <Button Text="Dodaj notatkę" Clicked="Dodaj_Click" Margin="0,8,0,0" />

        <CollectionView x:Name="ListaNotatek" HeightRequest="300"
                        SelectionMode="Single" SelectionChanged="Zaznaczenie_Zmienione" Margin="0,12,0,0">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="models:Notatka">
                    <Border Stroke="Gray" Padding="8" Margin="0,6">
                        <VerticalStackLayout Spacing="4">
                            <Label Text="{Binding Tytul}" FontAttributes="Bold" FontSize="16" />
                            <Label Text="{Binding Tresc}" FontSize="14" LineBreakMode="WordWrap" />
                        </VerticalStackLayout>
                    </Border>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>

        <Button Text="Zapisz zmiany zaznaczonej" Clicked="Edytuj_Click" />
        <Button Text="Usuń zaznaczoną" Clicked="Usun_Click" Margin="0,8,0,0" />

    </VerticalStackLayout>
</ContentPage>
  1. Przeczytaj powiązanie x:Name identyfikuje kontrolkę, a Binding odczytuje właściwość obiektu ustawionego jako BindingContext. To dwa różne mechanizmy.

Budowa krok po kroku

NotatkiPage.xaml.cs

Najpierw odświeżenie listy, potem dodanie, zaznaczenie/edycja i usunięcie. Przy edycji zachowaj Id wybranej notatki, bo Id=0 oznacza nowy rekord.

Pełne dodawanie, odczyt, edycja i usuwanie. Operacje bazy są awaitowane; wyjątki w zdarzeniach mają komunikat. Zaznaczenie wypełnia pola, a edycja zachowuje klucz Id.

NotatkiPage.xaml.cscsharp
using MojaAplikacja.Models;

namespace MojaAplikacja;
public partial class NotatkiPage : ContentPage
{
    private Notatka? _wybrana;
    public NotatkiPage() => InitializeComponent();
    protected override async void OnAppearing()
    {
        base.OnAppearing();
        try { await OdswiezAsync(); }
        catch (Exception) { await DisplayAlertAsync("Baza", "Nie udało się wczytać notatek.", "OK"); }
    }
    private async Task OdswiezAsync()
        => ListaNotatek.ItemsSource = await App.Database.GetNotatkiAsync();

    private void Zaznaczenie_Zmienione(object sender, SelectionChangedEventArgs e)
    {
        _wybrana = e.CurrentSelection.FirstOrDefault() as Notatka;
        TytulEntry.Text = _wybrana?.Tytul ?? "";
        TrescEntry.Text = _wybrana?.Tresc ?? "";
    }
    private async void Dodaj_Click(object sender, EventArgs e)
    {
        if (string.IsNullOrWhiteSpace(TytulEntry.Text))
        { await DisplayAlertAsync("Tytuł", "Podaj tytuł.", "OK"); return; }
        try
        {
            await App.Database.ZapiszNotatkeAsync(new Notatka { Tytul = TytulEntry.Text.Trim(), Tresc = TrescEntry.Text ?? "" });
            await OdswiezAsync();
        }
        catch (Exception) { await DisplayAlertAsync("Baza", "Nie udało się dodać notatki.", "OK"); }
    }
    private async void Edytuj_Click(object sender, EventArgs e)
    {
        if (_wybrana is null || string.IsNullOrWhiteSpace(TytulEntry.Text))
        { await DisplayAlertAsync("Edycja", "Zaznacz notatkę i wpisz tytuł.", "OK"); return; }
        try
        {
            // Zachowujemy Id, aby UpdateAsync zmieniło istniejący rekord.
            var zmieniona = new Notatka { Id = _wybrana.Id, Tytul = TytulEntry.Text.Trim(), Tresc = TrescEntry.Text ?? "" };
            await App.Database.AktualizujNotatkeAsync(zmieniona);
            await OdswiezAsync();
        }
        catch (Exception) { await DisplayAlertAsync("Baza", "Nie udało się zapisać zmian.", "OK"); }
    }
    private async void Usun_Click(object sender, EventArgs e)
    {
        if (_wybrana is null) { await DisplayAlertAsync("Usuwanie", "Zaznacz notatkę.", "OK"); return; }
        try { await App.Database.UsunNotatkeAsync(_wybrana); await OdswiezAsync(); }
        catch (Exception) { await DisplayAlertAsync("Baza", "Nie udało się usunąć notatki.", "OK"); }
    }
}
  1. SelectionChanged i _wybrana Zdarzenie listy daje bieżące zaznaczenie. Brak zaznaczenia oznacza null i blokuje edycję/usunięcie.
  2. Id = _wybrana.Id Tożsamość rekordu pozostaje ta sama. InsertAsync utworzyłby nowy rekord; do zmiany używamy UpdateAsync.
  3. await OdswiezAsync() Ponownie pobiera dane po zapisie; ItemsSource nie odświeża się automatycznie po SQL.

5. Odbiór: czy naprawdę działa?

Wykonaj próby w swojej aplikacji, nie tylko w modelu na stronie. Zaznaczaj dopiero po sprawdzeniu wyniku. Funkcje urządzenia wymagają testu na właściwej platformie.

11 · Czego unikać

Cztery błędy z SQLite

Nowe połączenie na każdej stronie
Tak nie
var db = new SQLiteAsyncConnection(sciezka);   // w każdej stronie

Niepotrzebne zużycie zasobów i możliwe konflikty przy jednoczesnym dostępie.

Tak
App.Database.GetNotatkiAsync()   // jedna wspólna instancja

Jedno połączenie dla całej aplikacji.

Brak `CreateTableAsync`
Tak nie
_database = new SQLiteAsyncConnection(dbPath);   // bez tworzenia tabeli

Błąd „no such table”, bo tabela nigdy nie powstała.

Tak
await InitAsync(); // przed każdym zapytaniem w klasie bazy

Każda operacja czeka na jedną zapamiętaną inicjalizację. Nie blokuj wątku UI przez .Wait().

Brak odświeżenia listy po zmianie
Tak nie
await App.Database.ZapiszNotatkeAsync(nowa);   // lista bez zmian

Notatka jest w bazie, ale nie widać jej na ekranie.

Tak
await App.Database.ZapiszNotatkeAsync(nowa);
WczytajNotatki();

InsertAsync zmienia bazę, a nie ItemsSource.

Ręczne `Id` przy auto-inkrementacji
Tak nie
var nowa = new Notatka { Id = 5, Tytul = "A" };

Możliwe konflikty kluczy.

Tak
var nowa = new Notatka { Tytul = "A" };   // Id nada baza

Przy AutoIncrement numer nadaje baza.

Ćwiczenie z pomocą · samodzielna praca

6. Mała zmiana, którą zrobisz samodzielnie

Dodaj potwierdzenie usunięcia wybranej notatki.

Oczekiwany rezultat
Dodaj potwierdzenie usunięcia wybranej notatki.
Dotychczasowe działanie ma pozostać poprawne.
Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź

Przed wywołaniem metody usuwającej otwórz DisplayAlertAsync z Tak i Nie.

Krok 2 — pokaż podpowiedź

Wywołuj bazę dopiero, gdy wynik bool jest true.

Krok 3 — pokaż podpowiedź

Sprawdź anulowanie — rekord i zaznaczenie nie powinny zostać bez powodu skasowane.

Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.

12 · Sprawdź się

Atrybut, metoda czy odświeżenie?

Cztery sytuacje.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Baza SQLite w MAUI to jeden plik w FileSystem.AppDataDirectory. Używasz pakietu sqlite-net-pcl.
  2. Klasa modelu = tabela. Atrybuty: [PrimaryKey, AutoIncrement], [MaxLength], [NotNull], [Ignore].
  3. Metody: CreateTableAsync, Table<T>().ToListAsync(), InsertAsync, UpdateAsync, DeleteAsync.
  4. Całą obsługę tabeli trzymaj w jednej klasie, a bazę utwórz raz i udostępnij przez App.Database.
  5. Po zmianie w bazie odśwież listę ponownym pobraniem danych.

Dalej na swoim koncie

Rozgrzewki, quiz i zadania są w biurku ucznia.

Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.

Zaloguj się →
Źródła i dalsza nauka