Bevezetés
Majdnem minden mobilalkalmazásnak kell valamilyen helyi adattárolás — ez egyszerűen megkerülhetetlen. Felhasználói beállítások mentése, offline gyorsítótár, vagy egy komplett helyi adatbázis: az SQLite a .NET MAUI világában egyértelműen a legelterjedtebb megoldás. És nem véletlenül, mert az SQLite gyakorlatilag a világ legelterjedtebb adatbázismotorja. Ott van minden telefonon, a legtöbb laptopon, sőt, még a böngésződben is.
Ebben az útmutatóban végigvezetlek azon, hogyan integrálhatsz SQLite adatbázist a .NET MAUI appodba. Szóba kerül a teljes CRUD, az MVVM mintával való összekötés, teljesítményoptimalizálás, meg az offline szinkronizálás is. Gyakorlati kódpéldákat kapsz, amiket azonnal be tudsz vetni a saját projektjeidben.
SQLite-net vagy Entity Framework Core?
Mielőtt belevágunk a kódolásba, tisztázzunk egy dolgot, ami rengeteg emberben felmerül: melyik könyvtárat válasszuk? A két fő jelölt az SQLite-net (sqlite-net-pcl NuGet csomag) és az Entity Framework Core (Microsoft.EntityFrameworkCore.Sqlite).
SQLite-net — A könnyűsúlyú bajnok
Az SQLite-net egy kifejezetten mobilra tervezett, minimalista ORM. Nekem személy szerint ez a kedvencem mobilfejlesztéshez, és van rá jó okom:
- Nagyon alacsony overhead — kis méret, gyors inicializálás
- Egyszerű API: attribútumalapú tábla-definiálás, automatikus tábla-létrehozás
- Natív aszinkron támogatás
SQLiteAsyncConnectionrévén - A Microsoft hivatalos dokumentációjában ajánlott megoldás .NET MAUI-hoz
- Write-Ahead Logging (WAL) támogatás a jobb párhuzamos teljesítményért
Entity Framework Core — Az ismerős óriás
Ha szerver oldali .NET fejlesztésből jössz, az EF Core valószínűleg ismerős:
- Teljes értékű ORM migrációkkal és Code-First megközelítéssel
- Komplex LINQ lekérdezések támogatása
- Automatikus séma-migrációk verziókezeléshez
- Kód újrafelhasználhatósága szerver és kliens között
- Viszont nagyobb memória- és indulási idő költséggel jár
Melyiket válaszd?
Ha mobilappot fejlesztesz és fontos a teljesítmény (márpedig mobilon mindig fontos), az SQLite-net az ajánlott választás. Az EF Core-t akkor érdemes elővenni, ha komplex séma-migrációkra van szükséged, vagy van egy meglévő EF Core kódbázisod, amit szeretnél újrahasznosítani. Ebben a cikkben az SQLite-net-re fókuszálunk, de a végén az EF Core alternatívát is megnézzük.
A projekt előkészítése
Kezdjük a NuGet csomag telepítésével. Semmi bonyolult:
# SQLite-net telepítése a NuGet Package Manager Console-ból
dotnet add package sqlite-net-pcl
A legfrissebb verzió a sqlite-net-pcl 1.9.172. Egyébként ne zavarjon, hogy a csomag neve „pcl"-re végződik — ez történelmi örökség, tökéletesen működik .NET MAUI projektekkel.
Ha titkosított adatbázisra van szükséged, használd helyette a sqlite-net-sqlcipher csomagot. Ez SQLCipher-alapú, AES-256 szintű titkosítást ad a helyi adatbázisnak. (Később a FAQ szekcióban erről is lesz szó.)
Az adatmodell létrehozása
Az SQLite-net attribútumalapú megközelítést használ a táblák definiálásához. Csináljunk egy egyszerű jegyzetalkalmazás modelljét — ez tökéletesen illusztrálja a lényeget:
using SQLite;
// Jegyzet adatmodell — automatikusan leképezi a táblát
public class Jegyzet
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
[MaxLength(200)]
[NotNull]
public string Cim { get; set; }
public string Tartalom { get; set; }
[Indexed]
public string Kategoria { get; set; }
public bool Kedvenc { get; set; }
public DateTime Letrehozva { get; set; }
public DateTime? Modositva { get; set; }
}
A legfontosabb attribútumok:
[PrimaryKey, AutoIncrement]— elsődleges kulcs automatikus növeléssel[MaxLength(200)]— oszlop maximális hossza[NotNull]— az oszlop nem lehet null[Indexed]— index létrehozása a gyorsabb kereséshez[Ignore]— a tulajdonság kihagyása az adatbázis-leképezésből (itt nem használtuk, de jó tudni róla)
Az adatbázis-szolgáltatás felépítése
A legjobb, ha az adatbázis-kezelést egy dedikált szolgáltatásosztályba pakolod. Így a kód tiszta marad, könnyen tesztelhető, és később újra fel tudod használni. Mindenképp aszinkron kapcsolatot (SQLiteAsyncConnection) használj, különben az adatbázis-műveletek befagyasztják a UI-t — és az ugye nem jó.
using SQLite;
public class JegyzetAdatbazis
{
private SQLiteAsyncConnection _kapcsolat;
private readonly string _adatbazisUtvonal;
public JegyzetAdatbazis()
{
// Platformfüggetlen adatbázis elérési útvonal
_adatbazisUtvonal = Path.Combine(
FileSystem.AppDataDirectory,
"jegyzetek.db3"
);
}
// Lusta inicializálás — az adatbázis csak akkor jön létre,
// amikor először szükség van rá
private async Task<SQLiteAsyncConnection> GetKapcsolat()
{
if (_kapcsolat is not null)
return _kapcsolat;
_kapcsolat = new SQLiteAsyncConnection(
_adatbazisUtvonal,
SQLiteOpenFlags.ReadWrite |
SQLiteOpenFlags.Create |
SQLiteOpenFlags.SharedCache
);
// WAL mód engedélyezése a jobb teljesítményért
await _kapcsolat.EnableWriteAheadLoggingAsync();
// Tábla létrehozása, ha még nem létezik
await _kapcsolat.CreateTableAsync<Jegyzet>();
return _kapcsolat;
}
// CREATE — Új jegyzet beszúrása
public async Task<int> JegyzetHozzaadasa(Jegyzet jegyzet)
{
var db = await GetKapcsolat();
jegyzet.Letrehozva = DateTime.UtcNow;
return await db.InsertAsync(jegyzet);
}
// READ — Összes jegyzet lekérdezése
public async Task<List<Jegyzet>> OsszesJegyzetLekerdezes()
{
var db = await GetKapcsolat();
return await db.Table<Jegyzet>()
.OrderByDescending(j => j.Letrehozva)
.ToListAsync();
}
// READ — Egyetlen jegyzet lekérdezése azonosító alapján
public async Task<Jegyzet> JegyzetLekerdezes(int id)
{
var db = await GetKapcsolat();
return await db.Table<Jegyzet>()
.FirstOrDefaultAsync(j => j.Id == id);
}
// READ — Jegyzetek keresése kategória alapján
public async Task<List<Jegyzet>> JegyzetekKategoriaAlapjan(
string kategoria)
{
var db = await GetKapcsolat();
return await db.Table<Jegyzet>()
.Where(j => j.Kategoria == kategoria)
.OrderByDescending(j => j.Letrehozva)
.ToListAsync();
}
// UPDATE — Jegyzet frissítése
public async Task<int> JegyzetFrissitese(Jegyzet jegyzet)
{
var db = await GetKapcsolat();
jegyzet.Modositva = DateTime.UtcNow;
return await db.UpdateAsync(jegyzet);
}
// DELETE — Jegyzet törlése
public async Task<int> JegyzetTorlese(Jegyzet jegyzet)
{
var db = await GetKapcsolat();
return await db.DeleteAsync(jegyzet);
}
// Kedvencek lekérdezése
public async Task<List<Jegyzet>> KedvencJegyzetek()
{
var db = await GetKapcsolat();
return await db.Table<Jegyzet>()
.Where(j => j.Kedvenc)
.OrderByDescending(j => j.Letrehozva)
.ToListAsync();
}
// Keresés szöveges tartalomban
public async Task<List<Jegyzet>> JegyzetekKeresese(
string keresesiKifejezes)
{
var db = await GetKapcsolat();
return await db.QueryAsync<Jegyzet>(
"SELECT * FROM Jegyzet WHERE Cim LIKE ? OR Tartalom LIKE ? ORDER BY Letrehozva DESC",
$"%{keresesiKifejezes}%",
$"%{keresesiKifejezes}%"
);
}
}
Pár dolog, amire érdemes figyelni itt:
- A lusta inicializálás (
GetKapcsolatmetódus) azt jelenti, hogy az adatbázis-kapcsolat csak akkor jön létre, amikor tényleg kell — szóval az app gyorsabban indul el. - A WAL mód engedélyezése nagyon sokat dob a párhuzamos teljesítményen: az olvasók és írók nem blokkolják egymást. Érdemes mindig bekapcsolni.
- Az SQLiteOpenFlags beállítások szabályozzák a kapcsolat viselkedését:
ReadWriteaz olvasás-íráshoz,Createaz adatbázis-fájl automatikus létrehozásához,SharedCachepedig a megosztott gyorsítótárhoz.
Regisztráció a Dependency Injection rendszerben
A .NET MAUI ad nekünk egy beépített DI konténert, amit a MauiProgram.cs fájlban konfigurálhatunk. Az adatbázis-szolgáltatást singleton-ként regisztráld — egy példány bőven elég az app teljes életciklusára:
// MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Adatbázis-szolgáltatás regisztrálása singleton-ként
builder.Services.AddSingleton<JegyzetAdatbazis>();
// ViewModel-ek regisztrálása
builder.Services.AddTransient<JegyzetekViewModel>();
builder.Services.AddTransient<JegyzetReszletekViewModel>();
// Oldalak regisztrálása
builder.Services.AddTransient<JegyzetekOldal>();
builder.Services.AddTransient<JegyzetReszletekOldal>();
return builder.Build();
}
}
Az AddSingleton itt azért jó választás, mert az adatbázis-kapcsolatot nem akarod minden oldallátogatáskor újra létrehozni. A ViewModel-eknek meg az oldalaknak viszont AddTransient kell, mert azokból minden navigációnál friss példányt szeretnél. Ez egy fontos különbség, amit sokan elrontanak az elején.
MVVM integráció a CommunityToolkit.Mvvm-mel
Oké, az adatbázis-szolgáltatás kész. Most kössük össze az MVVM mintával. A CommunityToolkit.Mvvm csomag forrásgenerátorai egy csomó boilerplate kódot megspórolnak — őszintén szólva, mióta ezt használom, nem tudom elképzelni nélküle az MVVM-et MAUI-ban:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
public partial class JegyzetekViewModel : ObservableObject
{
private readonly JegyzetAdatbazis _adatbazis;
[ObservableProperty]
private bool _betoltes;
[ObservableProperty]
private string _keresesiKifejezes;
public ObservableCollection<Jegyzet> Jegyzetek { get; } = new();
public JegyzetekViewModel(JegyzetAdatbazis adatbazis)
{
_adatbazis = adatbazis;
}
// Jegyzetek betöltése az adatbázisból
[RelayCommand]
private async Task JegyzetekBetoltese()
{
if (Betoltes) return;
try
{
Betoltes = true;
var jegyzetLista = await _adatbazis.OsszesJegyzetLekerdezes();
Jegyzetek.Clear();
foreach (var jegyzet in jegyzetLista)
{
Jegyzetek.Add(jegyzet);
}
}
finally
{
Betoltes = false;
}
}
// Keresés a jegyzetekben
[RelayCommand]
private async Task Kereses()
{
if (string.IsNullOrWhiteSpace(KeresesiKifejezes))
{
await JegyzetekBetoltese();
return;
}
try
{
Betoltes = true;
var talalatok = await _adatbazis.JegyzetekKeresese(
KeresesiKifejezes);
Jegyzetek.Clear();
foreach (var jegyzet in talalatok)
{
Jegyzetek.Add(jegyzet);
}
}
finally
{
Betoltes = false;
}
}
// Jegyzet törlése swipe-művelettel
[RelayCommand]
private async Task JegyzetTorlese(Jegyzet jegyzet)
{
await _adatbazis.JegyzetTorlese(jegyzet);
Jegyzetek.Remove(jegyzet);
}
// Kedvenc státusz váltása
[RelayCommand]
private async Task KedvencValtas(Jegyzet jegyzet)
{
jegyzet.Kedvenc = !jegyzet.Kedvenc;
await _adatbazis.JegyzetFrissitese(jegyzet);
}
// Navigáció az új jegyzet oldalra
[RelayCommand]
private async Task UjJegyzet()
{
await Shell.Current.GoToAsync(nameof(JegyzetReszletekOldal));
}
// Navigáció a részletek oldalra
[RelayCommand]
private async Task JegyzetMegnyitasa(Jegyzet jegyzet)
{
await Shell.Current.GoToAsync(
nameof(JegyzetReszletekOldal),
new Dictionary<string, object>
{
{ "Jegyzet", jegyzet }
});
}
}
A felhasználói felület összeállítása
Na, most jön a XAML. Hozzuk létre a felületet, ami egy CollectionView-ban jeleníti meg a jegyzeteket:
<?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:vm="clr-namespace:JegyzetApp.ViewModels"
x:Class="JegyzetApp.Views.JegyzetekOldal"
x:DataType="vm:JegyzetekViewModel"
Title="Jegyzetek">
<Grid RowDefinitions="Auto,*" Padding="16">
<!-- Keresősáv -->
<SearchBar Grid.Row="0"
Text="{Binding KeresesiKifejezes}"
Placeholder="Keresés a jegyzetekben..."
SearchCommand="{Binding KeresesCommand}" />
<!-- Jegyzetlista -->
<CollectionView Grid.Row="1"
ItemsSource="{Binding Jegyzetek}"
SelectionMode="None">
<CollectionView.EmptyView>
<VerticalStackLayout
HorizontalOptions="Center"
VerticalOptions="Center"
Spacing="12">
<Label Text="Még nincsenek jegyzetek"
FontSize="18"
HorizontalTextAlignment="Center"
TextColor="Gray" />
<Button Text="Új jegyzet létrehozása"
Command="{Binding UjJegyzetCommand}"
HorizontalOptions="Center" />
</VerticalStackLayout>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Jegyzet">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="Törlés"
BackgroundColor="Red"
Command="{Binding
Source={RelativeSource
AncestorType={x:Type vm:JegyzetekViewModel}},
Path=JegyzetTorleseCommand}"
CommandParameter="{Binding .}" />
</SwipeItems>
</SwipeView.RightItems>
<Border StrokeShape="RoundRectangle 8"
StrokeThickness="0"
BackgroundColor="{AppThemeBinding
Light=White, Dark=#1C1C1E}"
Margin="0,6"
Padding="16">
<Border.Shadow>
<Shadow Brush="Gray"
Opacity="0.15"
Radius="6" />
</Border.Shadow>
<Border.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding
Source={RelativeSource
AncestorType={x:Type vm:JegyzetekViewModel}},
Path=JegyzetMegnyitasaCommand}"
CommandParameter="{Binding .}" />
</Border.GestureRecognizers>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto,Auto">
<Label Text="{Binding Cim}"
FontSize="16"
FontAttributes="Bold"
LineBreakMode="TailTruncation" />
<Image Grid.Column="1"
Source="{Binding Kedvenc,
Converter={StaticResource BoolToIconConverter}}"
WidthRequest="20"
HeightRequest="20" />
<Label Grid.Row="1"
Text="{Binding Tartalom}"
FontSize="13"
TextColor="Gray"
MaxLines="2"
LineBreakMode="TailTruncation" />
<HorizontalStackLayout Grid.Row="2"
Grid.ColumnSpan="2"
Spacing="8"
Margin="0,8,0,0">
<Label Text="{Binding Kategoria}"
FontSize="11"
TextColor="{StaticResource Primary}"
BackgroundColor="{AppThemeBinding
Light=#E8F0FE, Dark=#2C2C2E}"
Padding="8,4" />
<Label Text="{Binding Letrehozva,
StringFormat='{0:yyyy.MM.dd}'}"
FontSize="11"
TextColor="Gray"
VerticalTextAlignment="Center" />
</HorizontalStackLayout>
</Grid>
</Border>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Betöltésjelző -->
<ActivityIndicator Grid.RowSpan="2"
IsRunning="{Binding Betoltes}"
IsVisible="{Binding Betoltes}"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Grid>
</ContentPage>
Figyeld meg az EmptyView-t: ha a lista üres, a felhasználó nem egy kopár, semmitmondó képernyőt kap, hanem egy szöveget meg egy gombot. Apróság, de hidd el, a felhasználói élményt tekintve rengeteget számít.
Teljesítményoptimalizálási tippek
Az SQLite alapból gyors, de pár trükkel még tovább tudsz rajta javítani. Nézzük a legfontosabbakat.
1. WAL mód engedélyezése
A Write-Ahead Logging (WAL) mód talán a legfontosabb optimalizálás, amit megtehetsz. Alapból az SQLite hagyományos rollback journal-t használ, ahol az írás blokkolja az olvasókat. WAL módban viszont a változtatások egy külön fájlba kerülnek, így olvasás és írás simán futhat párhuzamosan.
// WAL mód engedélyezése — ezt az adatbázis-szolgáltatás
// inicializálásakor érdemes megtenni
await kapcsolat.EnableWriteAheadLoggingAsync();
2. Kötegelt műveletek tranzakciókkal
Ha egyszerre több elemet szúrsz be vagy frissítesz, mindenképp használj tranzakciót. A különbség brutális:
// Kötegelt beszúrás tranzakcióval — sokkal gyorsabb,
// mint egyesével beszúrni
public async Task JegyzetekKotegesenHozzaadasa(
List<Jegyzet> jegyzetLista)
{
var db = await GetKapcsolat();
await db.RunInTransactionAsync(tranzakcio =>
{
foreach (var jegyzet in jegyzetLista)
{
jegyzet.Letrehozva = DateTime.UtcNow;
tranzakcio.Insert(jegyzet);
}
});
}
// Alternatíva: az InsertAllAsync metódus
public async Task GyorsKotegesBeszuras(
IEnumerable<Jegyzet> jegyzetLista)
{
var db = await GetKapcsolat();
await db.InsertAllAsync(jegyzetLista);
}
Hogy érzékeltesd a különbséget: 1000 elem egyesével beszúrva jellemzően 3-5 másodperc. Tranzakcióba csomagolva? 50-100 milliszekundum. Nem elírás — tényleg ekkora a különbség, és először engem is meglepett.
3. Indexek használata
A gyakran keresett oszlopokra tegyél indexet. Az [Indexed] attribútumot már láttuk, de összetett indexet is csinálhatsz:
// Összetett index létrehozása kézi SQL-lel
public async Task IndexekLetrehozasa()
{
var db = await GetKapcsolat();
await db.ExecuteAsync(
"CREATE INDEX IF NOT EXISTS idx_jegyzet_kategoria_datum " +
"ON Jegyzet (Kategoria, Letrehozva DESC)"
);
}
4. Lapozás nagy adathalmazokhoz
Ha több ezer rekordod van, ne töltsd be mindet egyszerre. Komolyan, ne. Használj lapozást:
// Lapozásos lekérdezés — egyszerre csak 20 elemet töltünk be
public async Task<List<Jegyzet>> JegyzetekLapozva(
int oldalSzam, int oldalMeret = 20)
{
var db = await GetKapcsolat();
return await db.Table<Jegyzet>()
.OrderByDescending(j => j.Letrehozva)
.Skip(oldalSzam * oldalMeret)
.Take(oldalMeret)
.ToListAsync();
}
Az EF Core alternatíva
Ha mégis az Entity Framework Core mellett döntesz — mondjuk mert komplex relációkra és migrációkra van szükséged —, így néz ki a beállítás:
# EF Core + SQLite csomagok telepítése
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
using Microsoft.EntityFrameworkCore;
// DbContext az EF Core-hoz
public class JegyzetContext : DbContext
{
public DbSet<Jegyzet> Jegyzetek { get; set; }
public JegyzetContext(
DbContextOptions<JegyzetContext> options)
: base(options)
{
}
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Jegyzet>(entity =>
{
entity.HasIndex(j => j.Kategoria);
entity.Property(j => j.Cim)
.HasMaxLength(200)
.IsRequired();
});
}
}
// Regisztráció a MauiProgram.cs-ben
builder.Services.AddDbContext<JegyzetContext>(options =>
{
var dbPath = Path.Combine(
FileSystem.AppDataDirectory, "jegyzetek_ef.db3");
options.UseSqlite($"Data Source={dbPath}");
});
Egy apróság, amit ne felejts el: iOS-en meg kell hívnod az SQLitePCL.Batteries_V2.Init()-et a konstruktorban, különben az SQLite inicializálása el fog szállni. Ezen már páran elcsúsztak.
Offline szinkronizálási stratégiák
A mobilappok egyik legnagyobb kihívása az offline működés kezelése. Mutatok egy egyszerű, de jól működő szinkronizálási mintát, ami helyi SQLite-ot használ gyorsítótárként és szinkronizál a távoli API-val:
// Szinkronizálási szolgáltatás — helyi adatbázis + távoli API
public class SzinkronSzolgaltatas
{
private readonly JegyzetAdatbazis _helyiDb;
private readonly HttpClient _httpClient;
private readonly IConnectivity _halozat;
public SzinkronSzolgaltatas(
JegyzetAdatbazis helyiDb,
HttpClient httpClient,
IConnectivity halozat)
{
_helyiDb = helyiDb;
_httpClient = httpClient;
_halozat = halozat;
}
// Jegyzet mentése — mindig a helyi adatbázisba ír először,
// majd szinkronizál, ha van hálózat
public async Task<bool> JegyzetMentese(Jegyzet jegyzet)
{
// 1. Mindig mentünk helyben
if (jegyzet.Id == 0)
await _helyiDb.JegyzetHozzaadasa(jegyzet);
else
await _helyiDb.JegyzetFrissitese(jegyzet);
// 2. Ha van hálózat, szinkronizálunk
if (_halozat.NetworkAccess == NetworkAccess.Internet)
{
try
{
await SzinkronizalasAServerre(jegyzet);
return true; // Sikeres szinkronizálás
}
catch (HttpRequestException)
{
// A hálózati hiba nem akadályozza
// a helyi mentést
return false;
}
}
return false; // Nincs hálózat, csak helyi mentés
}
// Függőben lévő változások szinkronizálása
public async Task FuggoValtozasokSzinkronizalasa()
{
if (_halozat.NetworkAccess != NetworkAccess.Internet)
return;
// Itt implementálnád a szinkronizálási logikát:
// pl. utolsó szinkronizálás időbélyege alapján
// kiválogatod a módosított rekordokat és feltöltöd
}
private async Task SzinkronizalasAServerre(Jegyzet jegyzet)
{
var json = JsonSerializer.Serialize(jegyzet);
var content = new StringContent(
json, Encoding.UTF8, "application/json");
await _httpClient.PostAsync(
"api/jegyzetek/szinkron", content);
}
}
Ez az úgynevezett „Local-First" megközelítés: az app mindig a helyi adatbázisból dolgozik, a szinkronizálás meg csak a háttérben megy, ha van rá lehetőség. A felhasználó szemszögéből ez azt jelenti, hogy az app mindig azonnal reagál. Nincs várakozás a hálózatra, nincs pörgő spinner — és szerintem ez az, ami igazán megkülönbözteti a jó mobilappokat a többitől.
Gyakori hibák és megoldásaik
Az SQLite .NET MAUI-ban való használata során van pár tipikus buktató, amikbe előbb-utóbb bele fogsz futni. Jobb, ha előre tudsz róluk.
Szálkezelési problémák
Az SQLite alapértelmezetten nem szálbiztos. Ha több szálról próbálsz egyszerre írni, SQLiteException: database is locked hibát kapsz — garantálom, hogy legalább egyszer találkozol vele a karriered során. A megoldás az SQLiteAsyncConnection és a SharedCache flag együttes használata, ahogy az adatbázis-szolgáltatásunkban is csináltuk.
Az adatbázis-fájl elérési útvonala
Mindig a FileSystem.AppDataDirectory mappát használd. Ez platformfüggetlen, és biztosítja, hogy az adatbázis az app privát tárhelyén legyen. Más alkalmazások nem tudnak hozzáférni (ami nyilván fontos).
Memóriaszivárgás megelőzése
Ne hozz létre minden művelethez új SQLiteAsyncConnection példányt. Használj singleton mintát — pontosan úgy, ahogy a JegyzetAdatbazis osztályunkban tettük. Ezt a hibát rengetegen elkövetik, aztán csodálkoznak, hogy az app memóriahasználata folyamatosan nő.
Gyakran ismételt kérdések (FAQ)
Melyik SQLite csomagot használjam .NET MAUI-ban?
A Microsoft hivatalosan a sqlite-net-pcl NuGet csomagot ajánlja. Tudom, a „pcl" a nevében kicsit zavaró, de nyugi, teljesen kompatibilis a .NET MAUI-val. A legújabb verziója (1.9.172) elérhető az SQLite-net GitHub oldalon. Ha alacsonyabb szintű ADO.NET hozzáférésre van szükséged, akkor a Microsoft.Data.Sqlite csomag is szóba jöhet, de a legtöbb esetben az sqlite-net-pcl bőven elég.
Hogyan tároljam biztonságosan az érzékeny adatokat helyi SQLite adatbázisban?
Érzékeny adatok (felhasználói tokenek, személyes adatok) esetén használd a sqlite-net-sqlcipher csomagot — ez AES-256 titkosítást ad az adatbázis-fájlnak. Kisebb adatokhoz (jelszavak, API kulcsok) viszont inkább a SecureStorage API-t javaslom, ami a platform natív kulcstárolóját használja (Keychain iOS-en, Keystore Androidon). Ne rakj jelszavakat sima SQLite-ba, kérlek.
Lehetséges egyszerre SQLite-net és EF Core használata ugyanabban a projektben?
Technikailag igen, de ne csináld. A két könyvtár különböző SQLite kapcsolatokat kezel, és ez szálkezelési problémákat okozhat. Válassz egyet a projekt elején és maradj annál. Ha egyszerű, gyors hozzáférés kell, menj az SQLite-net-tel. Ha komplex relációkat és migrációkat kezelsz, válaszd az EF Core-t.
Hogyan kezeljek adatbázis-séma változásokat az alkalmazás frissítésekor?
Az SQLite-net CreateTableAsync metódusa automatikusan hozzáadja az új oszlopokat, ha a modellosztály változik. Ez jó hír. A rossz hír az, hogy meglévő oszlopok törlését vagy átnevezését viszont nem tudja automatikusan kezelni. Ilyenkor kézi migrációt kell írnod az ExecuteAsync-kel, vagy ha ez túl gyakran előfordul, fontold meg az EF Core-ra való átállást, aminek van beépített migrációs eszköztára.
Mekkora adathalmazzal tud hatékonyan dolgozni az SQLite mobilon?
Meglepően naggyal. Több százezer rekorddal is simán elboldogul, ha van rendes index és lapozást használsz a megjelenítésnél. Az 1 millió rekord feletti tartományban már komolyabb optimalizálásra lesz szükséged (pl. FTS5 teljes szöveges keresés, részleges indexek), de őszintén: a legtöbb mobilapp meg sem közelíti ezt a határt.