Zašto svaka mobilna aplikacija treba lokalnu bazu podataka
Ruku na srce — mobilni korisnici nemaju strpljenja. Očekuju da aplikacija radi odmah, bez čekanja, čak i kad signal jedva postoji. Zamislite situaciju: korisnik sjedi u vlaku, ulazi u tunel, i pokušava spremiti bilješku ili pregledati svoj popis zadataka. Ako vaša aplikacija zahtijeva stalnu internetsku vezu za svaku sitnicu, upravo ste izgubili tog korisnika. Vjerojatno zauvijek.
Lokalna baza podataka rješava točno taj problem.
Umjesto oslanjanja isključivo na server, podaci se pohranjuju izravno na uređaju. Korisnik može čitati, pisati i ažurirati podatke bez ikakve mrežne veze, a kad se veza uspostavi — sinkronizacija se tiho odvija u pozadini. Ovaj pristup poznat je kao offline-first arhitektura i, iskreno, postao je industrijski standard za svaku ozbiljniju mobilnu aplikaciju.
U .NET MAUI ekosustavu imate dva glavna pristupa za rad s lokalnom SQLite bazom: direktnu uporabu sqlite-net-pcl biblioteke ili korištenje Entity Framework Core (EF Core) kao ORM sloja. Oba pristupa imaju svoje prednosti i idealne scenarije. U ovom vodiču pokrit ćemo oboje — s konkretnim primjerima koda koje možete odmah copy-paste u svoje projekte.
Odabir pristupa: sqlite-net-pcl ili Entity Framework Core
Prije nego što uopće počnemo pisati kod, morate odlučiti koji pristup odgovara vašem projektu. Oba koriste SQLite ispod haube, ali razlikuju se u razini apstrakcije, mogućnostima i (naravno) kompleksnosti.
Kada koristiti sqlite-net-pcl
Biblioteka sqlite-net-pcl je lagana, godinama provjerena i savršena za jednostavne do srednje složene scenarije. Izravno radite s tablicama i upitima, a API je dovoljno intuitivan da vas neće usporiti:
- Jednostavni CRUD scenariji — lista zadataka, bilješke, korisničke postavke
- Manje tablica — aplikacije s 5-10 tablica bez složenih relacija
- Minimalan overhead — biblioteka dodaje gotovo ništa na veličinu aplikacije
- Asinkroni API —
SQLiteAsyncConnectionpruža non-blocking operacije iz kutije
Kada koristiti Entity Framework Core
EF Core donosi punu snagu ORM-a — migracije sheme, LINQ upite, change tracking i relacije između entiteta. Ima smisla kad vam treba nešto ozbiljnije:
- Složene relacije — trebate one-to-many, many-to-many veze između entiteta
- Migracije baze — vaša shema se razvija s vremenom i trebate verzioniranje
- Dijeljeni modeli — koristite iste entitete na klijentu i serveru
- Tim dolazi iz ASP.NET Core svijeta — EF Core API je poznat i produktivan od prvog dana
Usporedba performansi
Za jednostavne operacije, sqlite-net-pcl je nešto brži jer nema overhead change trackinga i mapiranja relacija. Ali ta razlika se mjeri u mikrosekundama po operaciji — za većinu aplikacija to je potpuno irelevantno. EF Core nadoknađuje s produktivnošću: LINQ upiti, automatske migracije i lazy/eager loading značajno smanjuju količinu koda koji morate ručno pisati.
Pristup 1: Rad s sqlite-net-pcl bibliotekom
Ajmo krenuti s prvim pristupom. Biblioteka sqlite-net-pcl je najpopularniji izbor za SQLite u .NET MAUI aplikacijama — koristi je većina tutoriala, a i Microsoftova službena dokumentacija je preporučuje.
Instalacija i konfiguracija
Dodajte NuGet pakete u svoj .NET MAUI projekt:
dotnet add package sqlite-net-pcl
dotnet add package SQLitePCLRaw.bundle_green
Paket SQLitePCLRaw.bundle_green osigurava da SQLite native biblioteke budu ispravno uključene na svim platformama — posebno na iOS-u gdje je to znalo stvarati probleme.
Definirajte konstantu za putanju baze podataka:
public static class DatabaseConstants
{
public const string DatabaseFilename = "mojaaplikacija.db3";
public const SQLiteOpenFlags Flags =
SQLiteOpenFlags.ReadWrite |
SQLiteOpenFlags.Create |
SQLiteOpenFlags.SharedCache;
public static string DatabasePath =>
Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);
}
Zastavica SharedCache omogućuje dijeljenje podatkovnog i shema cachea između više veza na istu bazu, čime se smanjuje memorijska potrošnja. FileSystem.AppDataDirectory je .NET MAUI API koji vraća ispravnu putanju za pohranu podataka na svakoj platformi — bez ikakvog platformski specifičnog koda.
Definiranje modela podataka
Modeli su obične C# klase s atributima koji opisuju strukturu tablice. Ništa komplicirano:
using SQLite;
public class Zadatak
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
[MaxLength(200), NotNull]
public string Naziv { get; set; } = string.Empty;
public string? Opis { get; set; }
public bool Zavrseno { get; set; }
public DateTime KreiranoUtc { get; set; } = DateTime.UtcNow;
public DateTime? AzuriranoUtc { get; set; }
[Indexed]
public int KategorijaId { get; set; }
}
Primijetite atribut [Indexed] na KategorijaId — on stvara indeks koji ubrzava filtriranje po kategorijama. To je jedna od onih malih optimizacija koje čine veliku razliku kad vam tablica naraste.
Kreiranje servisa za pristup bazi
Ovo je ključni dio. Obavezno koristite SQLiteAsyncConnection umjesto sinkrone varijante — asinkrone operacije ne blokiraju UI nit, a to je kritično za responzivnost aplikacije:
public class ZadatakServis
{
private SQLiteAsyncConnection? _baza;
private async Task<SQLiteAsyncConnection> DohvatiBazu()
{
if (_baza is not null)
return _baza;
_baza = new SQLiteAsyncConnection(
DatabaseConstants.DatabasePath,
DatabaseConstants.Flags);
await _baza.EnableWriteAheadLoggingAsync();
await _baza.CreateTableAsync<Zadatak>();
return _baza;
}
public async Task<List<Zadatak>> DohvatiSveAsync()
{
var baza = await DohvatiBazu();
return await baza.Table<Zadatak>()
.OrderByDescending(z => z.KreiranoUtc)
.ToListAsync();
}
public async Task<List<Zadatak>> DohvatiPoKategorijiAsync(int kategorijaId)
{
var baza = await DohvatiBazu();
return await baza.Table<Zadatak>()
.Where(z => z.KategorijaId == kategorijaId)
.ToListAsync();
}
public async Task<int> SpremiAsync(Zadatak zadatak)
{
var baza = await DohvatiBazu();
if (zadatak.Id != 0)
{
zadatak.AzuriranoUtc = DateTime.UtcNow;
return await baza.UpdateAsync(zadatak);
}
return await baza.InsertAsync(zadatak);
}
public async Task<int> ObrisiAsync(Zadatak zadatak)
{
var baza = await DohvatiBazu();
return await baza.DeleteAsync(zadatak);
}
}
Obratite pažnju na EnableWriteAheadLoggingAsync() — WAL (Write-Ahead Logging) značajno poboljšava performanse jer čitači ne blokiraju pisce i obrnuto. Ovo je posebno bitno kod aplikacija koje istovremeno čitaju i pišu podatke, recimo pri sinkronizaciji s pozadinskim servisom.
Registracija u DI kontejneru
Registrirajte servis kao singleton u MauiProgram.cs:
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
builder.Services.AddSingleton<ZadatakServis>();
return builder.Build();
}
Singleton je ispravan životni vijek jer želite jednu instancu SQLiteAsyncConnection kroz cijelu aplikaciju. Višestruke veze na istu bazu mogu uzrokovati probleme sa zaključavanjem — naučio sam to na teži način.
Pristup 2: Rad s Entity Framework Core
Ako trebate migracije, relacije između entiteta i punu snagu LINQ upita, EF Core je logičan izbor. Evo kako ga postaviti u .NET MAUI projektu.
Instalacija NuGet paketa
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package SQLitePCLRaw.bundle_e_sqlite3
Paket SQLitePCLRaw.bundle_e_sqlite3 osigurava kompatibilnost s iOS-om i macOS-om gdje su nativne SQLite biblioteke malo drugačije bundleane nego što biste očekivali.
Definiranje entiteta i DbContexta
public class Projekt
{
public int Id { get; set; }
public string Naziv { get; set; } = string.Empty;
public string? Opis { get; set; }
public DateTime KreiranoUtc { get; set; } = DateTime.UtcNow;
// Navigacijska svojstva
public ICollection<ZadatakItem> Zadaci { get; set; } = new List<ZadatakItem>();
}
public class ZadatakItem
{
public int Id { get; set; }
public string Naziv { get; set; } = string.Empty;
public bool Zavrseno { get; set; }
public int Prioritet { get; set; }
// Strani ključ
public int ProjektId { get; set; }
public Projekt Projekt { get; set; } = null!;
}
EF Core automatski prepoznaje konvencije imenovanja — ProjektId se mapira kao strani ključ prema tablici Projekt. Navigacijska svojstva omogućuju pristup povezanim entitetima bez ručnog pisanja JOIN upita, što je jedna od stvari koje volim kod EF Core-a.
public class AppDbContext : DbContext
{
public DbSet<Projekt> Projekti => Set<Projekt>();
public DbSet<ZadatakItem> Zadaci => Set<ZadatakItem>();
public AppDbContext()
{
SQLitePCL.Batteries_V2.Init();
Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
var putanja = Path.Combine(FileSystem.AppDataDirectory, "projekti.db3");
options.UseSqlite($"Data Source={putanja}");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ZadatakItem>()
.HasIndex(z => z.ProjektId);
modelBuilder.Entity<ZadatakItem>()
.HasOne(z => z.Projekt)
.WithMany(p => p.Zadaci)
.HasForeignKey(z => z.ProjektId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Poziv SQLitePCL.Batteries_V2.Init() u konstruktoru je obavezan — bez njega ćete na iOS-u dobiti runtime iznimku koja vam neće reći apsolutno ništa korisno o pravom uzroku. Database.EnsureCreated() kreira bazu i tablice ako ne postoje. Za produkcijske aplikacije s ažuriranjima sheme, ipak razmotrite korištenje pravih migracija.
Servis s EF Core upitima
public class ProjektServis
{
private readonly IDbContextFactory<AppDbContext> _kontekstFactory;
public ProjektServis(IDbContextFactory<AppDbContext> kontekstFactory)
{
_kontekstFactory = kontekstFactory;
}
public async Task<List<Projekt>> DohvatiSveProjekteAsync()
{
using var kontekst = await _kontekstFactory.CreateDbContextAsync();
return await kontekst.Projekti
.Include(p => p.Zadaci)
.OrderByDescending(p => p.KreiranoUtc)
.ToListAsync();
}
public async Task<Projekt?> DohvatiProjektAsync(int id)
{
using var kontekst = await _kontekstFactory.CreateDbContextAsync();
return await kontekst.Projekti
.Include(p => p.Zadaci.OrderBy(z => z.Prioritet))
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task DodajProjektAsync(Projekt projekt)
{
using var kontekst = await _kontekstFactory.CreateDbContextAsync();
kontekst.Projekti.Add(projekt);
await kontekst.SaveChangesAsync();
}
public async Task AzurirajZadatakAsync(ZadatakItem zadatak)
{
using var kontekst = await _kontekstFactory.CreateDbContextAsync();
kontekst.Zadaci.Update(zadatak);
await kontekst.SaveChangesAsync();
}
}
Zašto IDbContextFactory umjesto izravnog injektiranja AppDbContext? Zato što je ovo preporučeni pristup za mobilne aplikacije — svaka operacija dobiva vlastitu instancu konteksta. Time izbjegavate probleme s konkurentnim pristupom i zastarjelim change tracking podacima koji vas mogu satima zbunjivati.
Registracija EF Core u DI kontejneru
builder.Services.AddDbContextFactory<AppDbContext>();
builder.Services.AddSingleton<ProjektServis>();
EF Core migracije u mobilnoj aplikaciji
Jedna od najvećih prednosti EF Core-a je sustav migracija. Ali u mobilnom kontekstu postoje neke specifičnosti koje morate razumjeti prije nego se zaletite.
Problem: dotnet ef alati ne rade izravno s MAUI projektom
E sad, ovo zna iznenaditi ljude. EF Core CLI alati (dotnet ef migrations add) očekuju konzolnu ili web aplikaciju kao startup projekt. .NET MAUI projekt ne može služiti tu ulogu. Rješenje? Pomoćni konzolni projekt:
# Struktura rješenja
MojaAplikacija.sln
├── MojaAplikacija/ # .NET MAUI projekt
├── MojaAplikacija.Data/ # Class library s DbContext i entitetima
└── MojaAplikacija.Migrations/ # Konzolni projekt za migracije
Konzolni projekt referencira MojaAplikacija.Data i služi isključivo za generiranje migracija:
dotnet ef migrations add PocetnaMigracija \
-s MojaAplikacija.Migrations \
-p MojaAplikacija.Data
Primjena migracija pri pokretanju aplikacije
Umjesto EnsureCreated(), u produkciji koristite Migrate():
public AppDbContext()
{
SQLitePCL.Batteries_V2.Init();
Database.Migrate(); // Primjenjuje sve pending migracije
}
Metoda Migrate() provjerava koje su migracije već primijenjene i izvršava samo nove. Slobodno je pozovite pri svakom pokretanju — ako nema novih migracija, operacija je gotovo trenutna.
Optimizacija performansi SQLite baze
Lokalna baza podataka može postati i usko grlo ako se ne koristi pravilno. Evo optimizacija koje stvarno čine razliku u praksi.
Write-Ahead Logging (WAL)
WAL je najvažnija pojedinačna optimizacija za SQLite u mobilnom kontekstu. Bez njega, SQLite zaključava cijelu bazu tijekom pisanja — čitanje mora čekati dok se pisanje završi. S WAL-om, promjene se prvo zapisuju u zasebnu WAL datoteku, a čitači nastavljaju neometano čitati iz glavne baze.
Za sqlite-net-pcl:
await baza.EnableWriteAheadLoggingAsync();
Za EF Core, dodajte u connection string:
options.UseSqlite($"Data Source={putanja};Cache=Shared");
Indeksi na pravim mjestima
Svaki stupac koji koristite u WHERE ili ORDER BY klauzulama trebao bi imati indeks. Bez indeksa, SQLite mora skenirati cijelu tablicu — za 100 redaka to je OK, ali za 10.000 postaje ozbiljan problem.
// sqlite-net-pcl pristup
public class Zadatak
{
[Indexed]
public int KategorijaId { get; set; }
[Indexed]
public DateTime KreiranoUtc { get; set; }
}
// EF Core pristup
modelBuilder.Entity<Zadatak>()
.HasIndex(z => new { z.KategorijaId, z.KreiranoUtc });
Kompozitni indeks na KategorijaId i KreiranoUtc pokriva upit "dohvati zadatke iz kategorije X sortirane po datumu". To je jedan od najčešćih upita u aplikacijama s listama, pa ga vrijedi optimizirati od starta.
Skupne operacije umjesto pojedinačnih
Umjesto unosa 100 redaka jednog po jednog (čest rookie mistake), koristite skupne operacije:
// sqlite-net-pcl — skupni unos unutar transakcije
var baza = await DohvatiBazu();
await baza.RunInTransactionAsync(trans =>
{
foreach (var zadatak in zadaci)
{
trans.Insert(zadatak);
}
});
// EF Core — AddRange
using var kontekst = await _kontekstFactory.CreateDbContextAsync();
kontekst.Zadaci.AddRange(zadaci);
await kontekst.SaveChangesAsync();
Transakcije grupiraju više operacija u jednu atomičnu jedinicu — ili sve uspiju ili se sve poništavaju. Performansna razlika je, bez pretjerivanja, dramatična: 100 pojedinačnih INSERT-ova traje otprilike 10 puta dulje od istog broja INSERT-ova unutar jedne transakcije.
Lazy inicijalizacija veze
Kreiranje SQLite veze uključuje I/O operacije na datotečnom sustavu. Lazy inicijalizacija odgađa taj trošak do trenutka kad je veza stvarno potrebna:
private readonly Lazy<Task<SQLiteAsyncConnection>> _lazyBaza;
public ZadatakServis()
{
_lazyBaza = new Lazy<Task<SQLiteAsyncConnection>>(async () =>
{
var baza = new SQLiteAsyncConnection(
DatabaseConstants.DatabasePath,
DatabaseConstants.Flags);
await baza.EnableWriteAheadLoggingAsync();
await baza.CreateTableAsync<Zadatak>();
return baza;
});
}
private Task<SQLiteAsyncConnection> DohvatiBazu() => _lazyBaza.Value;
Implementacija offline-first arhitekture
Pravu vrijednost lokalna baza pokazuje kad je kombinirate s offline-first pristupom. Princip je jednostavan: korisnik uvijek radi s lokalnim podacima, a sinkronizacija s serverom odvija se u pozadini. Korisnik to ne bi trebao ni primijetiti.
Osnovna struktura offline servisa
public class OfflineZadatakServis
{
private readonly ZadatakServis _lokalniServis;
private readonly IApiKlijent _apiKlijent;
private readonly IConnectivity _povezanost;
public OfflineZadatakServis(
ZadatakServis lokalniServis,
IApiKlijent apiKlijent,
IConnectivity povezanost)
{
_lokalniServis = lokalniServis;
_apiKlijent = apiKlijent;
_povezanost = povezanost;
}
public async Task<List<Zadatak>> DohvatiZadatkeAsync()
{
// Uvijek čitamo iz lokalne baze — brzo i pouzdano
return await _lokalniServis.DohvatiSveAsync();
}
public async Task SpremiZadatakAsync(Zadatak zadatak)
{
// Prvo spremi lokalno
await _lokalniServis.SpremiAsync(zadatak);
// Ako ima veze, pokušaj sinkronizirati
if (_povezanost.NetworkAccess == NetworkAccess.Internet)
{
await PokusajSinkronizacijuAsync(zadatak);
}
}
private async Task PokusajSinkronizacijuAsync(Zadatak zadatak)
{
try
{
await _apiKlijent.PostZadatakAsync(zadatak);
}
catch (HttpRequestException)
{
// Sinkronizacija nije uspjela — zadatak ostaje
// u lokalnoj bazi i bit će sinkroniziran kasnije
}
}
}
.NET MAUI pruža IConnectivity sučelje za provjeru mrežnog stanja — injektirajte ga iz DI kontejnera. Ključni princip koji vrijedi ponoviti: nikad ne blokirajte korisnika zato što nema internet. Lokalna baza je izvor istine, sinkronizacija je sekundarna.
Red čekanja za sinkronizaciju
Za robusniju offline podršku, implementirajte red čekanja koji prati nesinkronizirane promjene. Ovo je obrazac koji sam koristio u nekoliko produkcijskih aplikacija i radi odlično:
public class SinkronizacijskiRedak
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string TipEntiteta { get; set; } = string.Empty;
public int EntitetId { get; set; }
public string Operacija { get; set; } = string.Empty; // "Insert", "Update", "Delete"
public string JsonPodaci { get; set; } = string.Empty;
public DateTime KreiranoUtc { get; set; } = DateTime.UtcNow;
public int BrojPokusaja { get; set; }
}
public class SinkronizacijskiServis
{
private readonly SQLiteAsyncConnection _baza;
private readonly IApiKlijent _apiKlijent;
public async Task DodajUSyncRed(string tipEntiteta, int id,
string operacija, object podaci)
{
var redak = new SinkronizacijskiRedak
{
TipEntiteta = tipEntiteta,
EntitetId = id,
Operacija = operacija,
JsonPodaci = JsonSerializer.Serialize(podaci)
};
await _baza.InsertAsync(redak);
}
public async Task IzvediSinkronizacijuAsync()
{
var neobradjeni = await _baza.Table<SinkronizacijskiRedak>()
.Where(r => r.BrojPokusaja < 5)
.OrderBy(r => r.KreiranoUtc)
.ToListAsync();
foreach (var redak in neobradjeni)
{
try
{
await _apiKlijent.SinkronizirajAsync(
redak.TipEntiteta, redak.Operacija, redak.JsonPodaci);
await _baza.DeleteAsync(redak);
}
catch (HttpRequestException)
{
redak.BrojPokusaja++;
await _baza.UpdateAsync(redak);
}
}
}
}
Ograničenje na 5 pokušaja sprečava beskonačne petlje za operacije koje nikako ne uspijevaju (npr. server odbija podatke zbog validacijske greške). U tom slučaju, obavijestite korisnika da neke promjene trebaju ručnu intervenciju.
Povezivanje s ViewModelom pomoću MVVM uzorka
Lokalna baza sama po sebi nije previše korisna ako nije pravilno spojena s UI slojem. Evo kako povezati podatkovni sloj s ViewModelom koristeći CommunityToolkit.Mvvm:
public partial class ZadaciViewModel : ObservableObject
{
private readonly OfflineZadatakServis _servis;
[ObservableProperty]
private ObservableCollection<Zadatak> _zadaci = new();
[ObservableProperty]
private bool _ucitavanje;
[ObservableProperty]
private string _noviNaziv = string.Empty;
public ZadaciViewModel(OfflineZadatakServis servis)
{
_servis = servis;
}
[RelayCommand]
private async Task UcitajZadatkeAsync()
{
if (Ucitavanje) return;
try
{
Ucitavanje = true;
var rezultat = await _servis.DohvatiZadatkeAsync();
Zadaci = new ObservableCollection<Zadatak>(rezultat);
}
finally
{
Ucitavanje = false;
}
}
[RelayCommand]
private async Task DodajZadatakAsync()
{
if (string.IsNullOrWhiteSpace(NoviNaziv)) return;
var zadatak = new Zadatak { Naziv = NoviNaziv.Trim() };
await _servis.SpremiZadatakAsync(zadatak);
Zadaci.Insert(0, zadatak);
NoviNaziv = string.Empty;
}
[RelayCommand]
private async Task OznaciZavrsenim(Zadatak zadatak)
{
zadatak.Zavrseno = !zadatak.Zavrseno;
await _servis.SpremiZadatakAsync(zadatak);
}
}
Source generatori iz CommunityToolkit.Mvvm stvaraju svu infrastrukturu umjesto vas — INotifyPropertyChanged, komande i binding svojstva — sve iz par atributa. Umjesto stotina linija boilerplate koda, fokusirate se na poslovnu logiku. Kad sam prvi put vidio koliko koda to eliminira, bio sam oduševljen.
Rukovanje shemom baze kroz verzije aplikacije
Ovo je tema koja se često zanemari, a onda vas uhvati nespremne. Korisnici ne ažuriraju aplikaciju svi u isto vrijeme. Kad objavite verziju 2.0 s novim stupcima, neki korisnici još koriste verziju 1.0. Vaša baza mora podnijeti obje situacije bez problema.
Pristup s sqlite-net-pcl: ručno verzioniranje
public class DatabaseMigrator
{
private readonly SQLiteAsyncConnection _baza;
private const int TrenutnaVerzija = 3;
public async Task IzvediMigracijeAsync()
{
var verzija = await DohvatiVerzijuBazeAsync();
if (verzija < 1)
{
await _baza.CreateTableAsync<Zadatak>();
}
if (verzija < 2)
{
await _baza.ExecuteAsync(
"ALTER TABLE Zadatak ADD COLUMN Prioritet INTEGER DEFAULT 0");
}
if (verzija < 3)
{
await _baza.ExecuteAsync(
"CREATE INDEX IF NOT EXISTS IX_Zadatak_Prioritet ON Zadatak(Prioritet)");
}
await PostaviVerzijuBazeAsync(TrenutnaVerzija);
}
private async Task<int> DohvatiVerzijuBazeAsync()
{
try
{
var rezultat = await _baza.ExecuteScalarAsync<int>(
"PRAGMA user_version");
return rezultat;
}
catch
{
return 0;
}
}
private async Task PostaviVerzijuBazeAsync(int verzija)
{
await _baza.ExecuteAsync($"PRAGMA user_version = {verzija}");
}
}
SQLite-ov PRAGMA user_version je savršen za praćenje verzije sheme — to je integer vrijednost pohranjena u samoj bazi koja preživljava sve operacije. Jednostavno i elegantno. Svaka migracija provjerava trenutnu verziju i izvršava samo korake koje korisnik još nije dobio.
Česte zamke i kako ih izbjeći
Kroz rad s SQLite-om u mobilnim aplikacijama, neke pogreške se ponavljaju iznova i iznova. Evo onih najčešćih da vam uštedim živce.
Ne miješajte sinkrone i asinkrone operacije
Pozivanje .Result ili .GetAwaiter().GetResult() na asinkronim operacijama može uzrokovati deadlockove — posebno na iOS-u gdje je main thread dispatcher osjetljiviji. Uvijek koristite await. Bez iznimaka.
Ne zaboravite iOS inicijalizaciju
Ako koristite EF Core, SQLitePCL.Batteries_V2.Init() mora se pozvati prije bilo kakve operacije s bazom. Bez toga dobivate na iOS-u kriptičnu runtime iznimku koja ne govori ništa korisno o pravom uzroku. Klasična zamka za nove developere.
Pazite na veličinu baze
SQLite nema automatsko čišćenje — obrisani redovi ostavljaju prazan prostor u datoteci. Periodički pokrenite VACUUM za smanjenje veličine:
await baza.ExecuteAsync("VACUUM");
Ali ne previše često — VACUUM zahtijeva dvostruku veličinu baze u slobodnom prostoru jer kreira novu kopiju. Jednom mjesečno ili pri većem brisanju podataka je sasvim dovoljno.
Testirajte na stvarnim uređajima
Emulatori i simulatori koriste brzi SSD disk vašeg računala. Na stvarnom uređaju srednje klase, I/O operacije znaju biti osjetno sporije. Obavezno testirajte performanse baze na najslabijem uređaju koji podržavate — iznenađenja su garantirana.
Često postavljana pitanja
Ima li .NET MAUI ugrađenu podršku za SQLite?
Nema — trebate dodati NuGet pakete treće strane. Najpopularniji su sqlite-net-pcl za izravan pristup i Microsoft.EntityFrameworkCore.Sqlite za ORM pristup. Oba su stabilna, aktivno održavana i korištena u produkcijskim aplikacijama bez problema.
Mogu li koristiti EF Core migracije u .NET MAUI aplikaciji?
Da, ali s jednom napomenom — EF Core CLI alati ne mogu koristiti .NET MAUI projekt kao startup projekt. Rješenje je kreirati zasebni konzolni projekt za generiranje migracija, dok se migracije primjenjuju pozivom Database.Migrate() pri pokretanju aplikacije.
Koliko je SQLite baza sigurna na mobilnom uređaju?
SQLite datoteka je po defaultu nešifrirana i čitljiva s root pristupom uređaju. Za osjetljive podatke koristite SQLiteOpenFlags.ProtectionComplete koji šifrira datoteku dok je uređaj zaključan. Za punu enkripciju u mirovanju, pogledajte SQLCipher — komercijalno proširenje koje transparentno šifrira cijelu bazu AES-256 enkripcijom.
Koja je razlika između sqlite-net-pcl i Microsoft.Data.Sqlite?
sqlite-net-pcl je ORM lite biblioteka s atributima za mapiranje klasa na tablice i ugrađenim CRUD operacijama. Microsoft.Data.Sqlite je ADO.NET provider s nižom razinom pristupa putem sirovih SQL upita. Za većinu .NET MAUI projekata, sqlite-net-pcl ili EF Core su produktivniji izbor.
Kako implementirati sinkronizaciju između lokalne SQLite baze i servera?
Najjednostavniji pristup je red čekanja za sinkronizaciju: svaka lokalna promjena zapisuje se u posebnu tablicu s tipom operacije i podacima. Kad se uspostavi internetska veza, pozadinski servis obrađuje red i šalje promjene na server. Za složenije scenarije s konfliktima, razmotrite Azure Mobile Apps DataSync Framework ili biblioteku NubeSync koja podržava rješavanje konflikata na razini pojedinačnih polja.