Γιατί η Τοπική Βάση Δεδομένων Είναι Κρίσιμη για Mobile Εφαρμογές
Ας ξεκινήσουμε με μια αλήθεια που πολλοί developers αγνοούν στην αρχή: αν η mobile εφαρμογή σας βασίζεται αποκλειστικά σε REST API κλήσεις για κάθε λειτουργία, έχετε πρόβλημα. Και μάλιστα σοβαρό. Ο χρήστης μπαίνει στο μετρό, χάνει σήμα, και η εφαρμογή σας γίνεται ουσιαστικά άχρηστη. Σύμφωνα με έρευνες, πάνω από 50% των χρηστών προτιμούν εφαρμογές που λειτουργούν χωρίς σύνδεση στο internet.
Στο .NET MAUI, η SQLite είναι ο de facto τρόπος τοπικής αποθήκευσης δεδομένων. Ελαφριά, ενσωματωμένη, δεν χρειάζεται server, και τρέχει σε όλες τις πλατφόρμες — Android, iOS, Windows, macOS. Σε αυτόν τον οδηγό θα εξετάσουμε δύο προσεγγίσεις: τη sqlite-net-pcl (micro-ORM) και το Entity Framework Core (full ORM), μαζί με ολοκληρωμένα patterns για offline-first αρχιτεκτονική.
Προσέγγιση 1: SQLite-net — Το Ελαφρύ Micro-ORM
Εγκατάσταση και Αρχική Ρύθμιση
Η sqlite-net-pcl είναι η πιο δημοφιλής βιβλιοθήκη για SQLite στο .NET MAUI και ειλικρινά, για τις περισσότερες περιπτώσεις είναι ό,τι χρειάζεστε. Παρέχει ένα απλό, asynchronous API πάνω από τη SQLite, χωρίς τον overhead ενός full ORM.
Εγκαταστήστε τα NuGet packages:
dotnet add package sqlite-net-pcl
dotnet add package SQLitePCLRaw.bundle_green
Ορίστε τα constants της βάσης σε ένα κεντρικό σημείο — αυτό γλιτώνει πολύ μπέρδεμα αργότερα:
public static class DatabaseConstants
{
public const string DatabaseFilename = "app_data.db3";
public const SQLiteOpenFlags Flags =
SQLiteOpenFlags.ReadWrite |
SQLiteOpenFlags.Create |
SQLiteOpenFlags.SharedCache;
public static string DatabasePath =>
Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);
}
Δημιουργία Data Models
Τα μοντέλα σας χρησιμοποιούν attributes για να ορίσουν το σχήμα της βάσης. Σε αντίθεση με το EF Core, εδώ δεν έχετε DbContext — τα πάντα ορίζονται μέσω attributes:
using SQLite;
public class TodoItem
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
[MaxLength(250)]
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public bool IsCompleted { get; set; }
[Indexed]
public DateTime CreatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
}
public class Category
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
[MaxLength(100), Unique]
public string Name { get; set; } = string.Empty;
public int SortOrder { get; set; }
}
Database Service με Async Operations
Τώρα ας φτιάξουμε ένα service class που διαχειρίζεται τη σύνδεση και τις CRUD λειτουργίες. Ένα σημαντικό σημείο: η χρήση SQLiteAsyncConnection είναι κρίσιμη. Ποτέ μη χρησιμοποιήσετε σύγχρονες λειτουργίες στο UI thread, αλλιώς η εφαρμογή σας θα κολλάει.
public class TodoDatabaseService
{
private SQLiteAsyncConnection _database;
private async Task<SQLiteAsyncConnection> GetConnectionAsync()
{
if (_database is not null)
return _database;
_database = new SQLiteAsyncConnection(
DatabaseConstants.DatabasePath,
DatabaseConstants.Flags);
// Ενεργοποίηση Write-Ahead Logging για καλύτερη απόδοση
await _database.EnableWriteAheadLoggingAsync();
// Δημιουργία πινάκων
await _database.CreateTableAsync<TodoItem>();
await _database.CreateTableAsync<Category>();
return _database;
}
// Create
public async Task<int> AddTodoAsync(TodoItem item)
{
var db = await GetConnectionAsync();
item.CreatedAt = DateTime.UtcNow;
return await db.InsertAsync(item);
}
// Read - Όλα τα items
public async Task<List<TodoItem>> GetTodosAsync()
{
var db = await GetConnectionAsync();
return await db.Table<TodoItem>()
.OrderByDescending(t => t.CreatedAt)
.ToListAsync();
}
// Read - Φιλτράρισμα
public async Task<List<TodoItem>> GetPendingTodosAsync()
{
var db = await GetConnectionAsync();
return await db.Table<TodoItem>()
.Where(t => !t.IsCompleted)
.OrderBy(t => t.CreatedAt)
.ToListAsync();
}
// Read - Μεμονωμένο item
public async Task<TodoItem> GetTodoByIdAsync(int id)
{
var db = await GetConnectionAsync();
return await db.Table<TodoItem>()
.FirstOrDefaultAsync(t => t.Id == id);
}
// Update
public async Task<int> UpdateTodoAsync(TodoItem item)
{
var db = await GetConnectionAsync();
return await db.UpdateAsync(item);
}
// Delete
public async Task<int> DeleteTodoAsync(TodoItem item)
{
var db = await GetConnectionAsync();
return await db.DeleteAsync(item);
}
// Σύνθετο query με raw SQL
public async Task<List<TodoItem>> SearchTodosAsync(string query)
{
var db = await GetConnectionAsync();
return await db.QueryAsync<TodoItem>(
"SELECT * FROM TodoItem WHERE Title LIKE ? OR Description LIKE ?",
$"%{query}%", $"%{query}%");
}
}
Εγγραφή στο DI Container
Καταχωρήστε το service στο MauiProgram.cs ώστε να είναι διαθέσιμο σε όλα τα ViewModels:
// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Database service ως Singleton
builder.Services.AddSingleton<TodoDatabaseService>();
// ViewModels
builder.Services.AddTransient<TodoListViewModel>();
builder.Services.AddTransient<TodoDetailViewModel>();
return builder.Build();
}
Προσέγγιση 2: Entity Framework Core με SQLite
Πότε να Επιλέξετε EF Core Αντί για sqlite-net
Εδώ αρχίζει να γίνεται ενδιαφέρον. Το EF Core αξίζει τον κόπο όταν η εφαρμογή σας έχει:
- Σύνθετες σχέσεις: One-to-many, many-to-many, navigation properties
- Ανάγκη για migrations: Εξέλιξη σχήματος βάσης χωρίς απώλεια δεδομένων
- Σύνθετα queries: Full LINQ support με joins, grouping, projections
- Κοινό κώδικα: Αν μοιράζεστε μοντέλα μεταξύ backend (ASP.NET Core) και mobile
Αν η εφαρμογή σας χρειάζεται απλά CRUD σε 2-3 πίνακες, η sqlite-net-pcl είναι μια χαρά. Μην περιπλέκετε τα πράγματα χωρίς λόγο. Για πιο σύνθετα σενάρια όμως, το EF Core αξίζει τον μικρό overhead.
Εγκατάσταση EF Core
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
Σημείωση: Στο .NET 10, η τελευταία σταθερή έκδοση είναι η Microsoft.EntityFrameworkCore.Sqlite 10.0.3 (Φεβρουάριος 2026). Βεβαιωθείτε ότι η έκδοση του package ταιριάζει με το .NET SDK σας.
Ορισμός Data Models με Σχέσεις
public class Project
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public bool IsArchived { get; set; }
// Navigation property - One-to-Many
public ICollection<TaskItem> Tasks { get; set; } = new List<TaskItem>();
}
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Notes { get; set; } = string.Empty;
public TaskPriority Priority { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? DueDate { get; set; }
// Foreign Key
public int ProjectId { get; set; }
public Project Project { get; set; } = null!;
}
public enum TaskPriority
{
Low,
Medium,
High,
Critical
}
Δημιουργία DbContext
Ο DbContext είναι η καρδιά του EF Core — διαχειρίζεται τη σύνδεση, τα queries, και το change tracking. Ας δούμε πώς στήνεται:
public class AppDbContext : DbContext
{
public DbSet<Project> Projects => Set<Project>();
public DbSet<TaskItem> Tasks => Set<TaskItem>();
private readonly string _dbPath;
public AppDbContext()
{
_dbPath = Path.Combine(
FileSystem.AppDataDirectory, "taskmanager.db3");
}
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite($"Data Source={_dbPath}");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Ρύθμιση σχέσεων
modelBuilder.Entity<TaskItem>()
.HasOne(t => t.Project)
.WithMany(p => p.Tasks)
.HasForeignKey(t => t.ProjectId)
.OnDelete(DeleteBehavior.Cascade);
// Index για γρήγορη αναζήτηση
modelBuilder.Entity<TaskItem>()
.HasIndex(t => t.ProjectId);
modelBuilder.Entity<TaskItem>()
.HasIndex(t => t.IsCompleted);
// Seed data
modelBuilder.Entity<Project>().HasData(
new Project
{
Id = 1,
Name = "Προσωπικά",
Description = "Προσωπικές εργασίες",
CreatedAt = DateTime.UtcNow
});
}
}
Εγγραφή DbContext στο DI Container
// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Εγγραφή DbContext
builder.Services.AddDbContext<AppDbContext>();
// Repository pattern
builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
builder.Services.AddScoped<ITaskRepository, TaskRepository>();
return builder.Build();
}
Repository Pattern με EF Core
Αυτό είναι κάτι που κατά τη γνώμη μου κάθε MAUI project θα έπρεπε να χρησιμοποιεί. Το Repository pattern αποσυνδέει τα ViewModels από το EF Core. Αύριο αν θέλετε να αλλάξετε τη βάση σε REST API, τα ViewModels δεν θα χρειαστεί να αλλάξουν καθόλου:
public interface IProjectRepository
{
Task<List<Project>> GetAllAsync();
Task<Project?> GetByIdWithTasksAsync(int id);
Task AddAsync(Project project);
Task UpdateAsync(Project project);
Task DeleteAsync(int id);
}
public class ProjectRepository : IProjectRepository
{
private readonly AppDbContext _context;
public ProjectRepository(AppDbContext context)
{
_context = context;
_context.Database.EnsureCreated();
}
public async Task<List<Project>> GetAllAsync()
{
return await _context.Projects
.Include(p => p.Tasks)
.OrderByDescending(p => p.CreatedAt)
.AsNoTracking()
.ToListAsync();
}
public async Task<Project?> GetByIdWithTasksAsync(int id)
{
return await _context.Projects
.Include(p => p.Tasks
.OrderBy(t => t.IsCompleted)
.ThenBy(t => t.Priority))
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task AddAsync(Project project)
{
project.CreatedAt = DateTime.UtcNow;
_context.Projects.Add(project);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(Project project)
{
_context.Projects.Update(project);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var project = await _context.Projects.FindAsync(id);
if (project is not null)
{
_context.Projects.Remove(project);
await _context.SaveChangesAsync();
}
}
}
Migrations στο .NET MAUI — Η Πρακτική Λύση
Εδώ βρίσκεται ένα από τα πιο ενοχλητικά σημεία: τα EF Core migration tools δεν υποστηρίζουν απευθείας τα multi-targeted MAUI projects. Η λύση (που δυστυχώς δεν είναι τόσο κομψή) είναι να δημιουργήσετε ένα ξεχωριστό console project μόνο για τα migrations:
# Δημιουργία helper console project
dotnet new console -n MigrationHelper
cd MigrationHelper
dotnet add reference ../YourMauiApp/YourMauiApp.csproj
dotnet add package Microsoft.EntityFrameworkCore.Design
# Εκτέλεση migration
dotnet ef migrations add InitialCreate --project ../YourMauiApp --startup-project .
dotnet ef database update --project ../YourMauiApp --startup-project .
Σημαντικό στο EF Core 10: Αν το project σας χρησιμοποιεί <TargetFrameworks> (πληθυντικός), πρέπει να ορίσετε ρητά το framework με την παράμετρο --framework.
Σύγκριση: sqlite-net-pcl vs Entity Framework Core
Πριν αποφασίσετε, δείτε μια γρήγορη σύγκριση:
| Χαρακτηριστικό | sqlite-net-pcl | EF Core + SQLite |
|---|---|---|
| Μέγεθος package | ~150 KB | ~5+ MB |
| Ταχύτητα εκκίνησης | Πολύ γρήγορη | Πιο αργή (reflection, model building) |
| Learning curve | Χαμηλή | Μέτρια-Υψηλή |
| Σχέσεις πινάκων | Χειροκίνητα (manual joins) | Navigation properties, Include() |
| Migrations | Δεν υποστηρίζονται | Πλήρης υποστήριξη |
| LINQ | Βασικό (Table queries) | Πλήρες (joins, grouping, projections) |
| Change tracking | Δεν υπάρχει | Αυτόματο |
| Ιδανικό για | Απλά apps, caching, μικρά datasets | Σύνθετα data models, enterprise apps |
Offline-First Αρχιτεκτονική: Σχεδιάζοντας για Αξιοπιστία
Τι Σημαίνει Offline-First
Ας ξεκαθαρίσουμε κάτι: η offline-first αρχιτεκτονική δεν σημαίνει απλά «αποθήκευσε κάτι τοπικά κι ελπίζω να πάει καλά». Σημαίνει ότι η τοπική βάση γίνεται η πρωταρχική πηγή αλήθειας (source of truth), και ο συγχρονισμός με τον server γίνεται στο background, όταν υπάρχει σύνδεση.
Αυτή η αλλαγή νοοτροπίας είναι σημαντική.
Υλοποίηση με Connectivity API και Sync Service
Το .NET MAUI παρέχει το IConnectivity API για να παρακολουθείτε την κατάσταση δικτύου σε πραγματικό χρόνο. Δείτε πώς στήνεται ένα βασικό sync service:
public class SyncService
{
private readonly AppDbContext _localDb;
private readonly IApiService _apiService;
private readonly IConnectivity _connectivity;
public SyncService(
AppDbContext localDb,
IApiService apiService,
IConnectivity connectivity)
{
_localDb = localDb;
_apiService = apiService;
_connectivity = connectivity;
}
public async Task SyncAsync()
{
if (_connectivity.NetworkAccess != NetworkAccess.Internet)
return;
try
{
// 1. Push τοπικές αλλαγές στον server
await PushLocalChangesAsync();
// 2. Pull νέα δεδομένα από τον server
await PullRemoteChangesAsync();
}
catch (HttpRequestException)
{
// Αποτυχία δικτύου — θα ξαναπροσπαθήσουμε αργότερα
}
}
private async Task PushLocalChangesAsync()
{
var pendingChanges = await _localDb.Tasks
.Where(t => t.IsPendingSync)
.ToListAsync();
foreach (var task in pendingChanges)
{
var success = await _apiService.UpsertTaskAsync(task);
if (success)
{
task.IsPendingSync = false;
task.LastSyncedAt = DateTime.UtcNow;
}
}
await _localDb.SaveChangesAsync();
}
private async Task PullRemoteChangesAsync()
{
var lastSync = await _localDb.SyncMetadata
.Select(s => s.LastPullAt)
.FirstOrDefaultAsync();
var remoteChanges = await _apiService
.GetChangedTasksSinceAsync(lastSync);
foreach (var remoteTask in remoteChanges)
{
var localTask = await _localDb.Tasks
.FindAsync(remoteTask.Id);
if (localTask is null)
{
_localDb.Tasks.Add(remoteTask);
}
else if (!localTask.IsPendingSync)
{
_localDb.Entry(localTask)
.CurrentValues.SetValues(remoteTask);
}
// Αν το local item έχει pending changes,
// κρατάμε την τοπική version (conflict resolution)
}
await _localDb.SaveChangesAsync();
}
}
Στρατηγικές Επίλυσης Συγκρούσεων (Conflict Resolution)
Σε offline-first apps, οι συγκρούσεις δεν είναι θέμα «αν» θα συμβούν — είναι θέμα «πότε». Οι πιο συνηθισμένες στρατηγικές αντιμετώπισης:
- Last Write Wins: Η πιο πρόσφατη αλλαγή κερδίζει. Απλό στην υλοποίηση, αλλά υπάρχει κίνδυνος απώλειας δεδομένων
- Client Wins / Server Wins: Σταθερή προτεραιότητα σε μία πλευρά — ξέρετε πάντα τι θα συμβεί
- Merge: Συγχώνευση αλλαγών σε επίπεδο πεδίου. Πιο σύνθετο αλλά δεν χάνονται δεδομένα
- Manual Resolution: Ειδοποιείτε τον χρήστη να επιλέξει ποια version θέλει — ιδανικό για κρίσιμα δεδομένα
Η εμπειρία μου δείχνει ότι για τις περισσότερες mobile εφαρμογές, ένας συνδυασμός «Last Write Wins» για απλά πεδία και «Manual Resolution» για κρίσιμα δεδομένα λειτουργεί αρκετά καλά.
Βελτιστοποίηση Απόδοσης SQLite στο .NET MAUI
Write-Ahead Logging (WAL)
Το WAL mode είναι απαραίτητο για mobile εφαρμογές. Μην το παραλείψετε. Γράφει τις αλλαγές σε ξεχωριστό αρχείο πριν τις εφαρμόσει στη βάση, επιτρέποντας ταυτόχρονη ανάγνωση και εγγραφή:
// Με sqlite-net-pcl
await connection.EnableWriteAheadLoggingAsync();
// Με EF Core — στο OnConfiguring
optionsBuilder.UseSqlite($"Data Source={_dbPath};Cache=Shared");
Σωστή Χρήση Indexes
Τα indexes επιταχύνουν δραματικά τα queries, αλλά μην τα βάζετε παντού γιατί επιβραδύνουν τα inserts και updates. Χρησιμοποιήστε τα μόνο σε στήλες που εμφανίζονται σε WHERE, ORDER BY και JOIN:
// Με sqlite-net-pcl
public class TodoItem
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
[Indexed] // Αναζήτηση με βάση κατηγορία
public int CategoryId { get; set; }
[Indexed] // Φιλτράρισμα κατά κατάσταση
public bool IsCompleted { get; set; }
}
// Με EF Core — στο OnModelCreating
modelBuilder.Entity<TaskItem>()
.HasIndex(t => new { t.ProjectId, t.IsCompleted })
.HasDatabaseName("IX_Task_Project_Status");
Batch Operations
Αυτό είναι ένα κλασικό λάθος που βλέπω συχνά: μεμονωμένες εγγραφές μέσα σε loop. Αντί γι' αυτό, χρησιμοποιήστε batch inserts:
// sqlite-net-pcl — Batch insert
await db.RunInTransactionAsync(tran =>
{
foreach (var item in items)
{
tran.Insert(item);
}
});
// EF Core — Batch insert
_context.Tasks.AddRange(taskList);
await _context.SaveChangesAsync();
Αποφυγή Κοινών Λαθών
- Μη χρησιμοποιείτε σύγχρονες κλήσεις στο UI thread: Χρησιμοποιείτε πάντα
Asyncμεθόδους — το έχω πει ήδη, αλλά αξίζει να επαναληφθεί - Κλείστε τις συνδέσεις: Με EF Core, χρησιμοποιήστε
usingή αφήστε τον DI container να τις διαχειριστεί - Μην αποθηκεύετε μεγάλα BLOBs: Αρχεία πάνω από μερικά KB αποθηκεύστε τα στο filesystem και κρατήστε μόνο το path στη βάση
- Προσοχή στο iOS Keychain restore: Μετά από factory reset, τα SQLite αρχεία μπορεί να μην αποκαθίστανται — φροντίστε να έχετε initial setup logic
Πρακτικό Παράδειγμα: ViewModel με Τοπική Βάση
Ας δούμε τώρα πώς δένει όλο αυτό μαζί. Ένα ViewModel που χρησιμοποιεί repository pattern, CommunityToolkit.Mvvm, τοπική βάση, και υποστήριξη offline:
[QueryProperty(nameof(ProjectId), nameof(ProjectId))]
public partial class TaskListViewModel : ObservableObject
{
private readonly ITaskRepository _taskRepository;
private readonly SyncService _syncService;
public TaskListViewModel(
ITaskRepository taskRepository,
SyncService syncService)
{
_taskRepository = taskRepository;
_syncService = syncService;
}
[ObservableProperty]
private int projectId;
[ObservableProperty]
private ObservableCollection<TaskItem> tasks = new();
[ObservableProperty]
private bool isLoading;
[RelayCommand]
private async Task LoadTasksAsync()
{
IsLoading = true;
try
{
// Πάντα φόρτωση από τοπική βάση πρώτα
var items = await _taskRepository
.GetByProjectAsync(ProjectId);
Tasks = new ObservableCollection<TaskItem>(items);
// Sync στο background
_ = _syncService.SyncAsync();
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private async Task ToggleCompletedAsync(TaskItem task)
{
task.IsCompleted = !task.IsCompleted;
task.IsPendingSync = true;
await _taskRepository.UpdateAsync(task);
// Trigger sync αν υπάρχει σύνδεση
_ = _syncService.SyncAsync();
}
}
Νέα Χαρακτηριστικά EF Core 10 για SQLite
Το Entity Framework Core 10, που κυκλοφόρησε μαζί με το .NET 10 τον Νοέμβριο 2025, φέρνει αρκετές βελτιώσεις που αξίζει να γνωρίζετε:
- Έλεγχος AUTOINCREMENT: Μπορείτε πλέον να απενεργοποιήσετε το AUTOINCREMENT σε SQLite — χρήσιμο για tables που δεν χρειάζονται αυστηρά αυξανόμενα IDs
- Βελτιωμένο LINQ: Νέες μέθοδοι
LeftJoinκαιRightJoinγια πιο ξεκάθαρο κώδικα - ExecuteUpdateAsync απλοποιημένο: Δέχεται κανονικά lambda expressions αντί μόνο expression trees
- Διόρθωση timezone: Η
Microsoft.Data.Sqlite 10.0αντιμετωπίζει πλέον timestamps χωρίς offset ως UTC — προσοχή αν βασιζόσασταν στην παλιά συμπεριφορά (local timezone)
Συχνές Ερωτήσεις (FAQ)
Μπορώ να χρησιμοποιήσω ταυτόχρονα sqlite-net και EF Core στο ίδιο project;
Τεχνικά ναι, αλλά σας συμβουλεύω να μην το κάνετε. Κάθε βιβλιοθήκη διαχειρίζεται τη σύνδεση SQLite διαφορετικά, και η ταυτόχρονη πρόσβαση στο ίδιο αρχείο βάσης μπορεί να φέρει database locking issues. Επιλέξτε μία προσέγγιση και μείνετε σε αυτή.
Πόσα δεδομένα μπορεί να αποθηκεύσει η SQLite σε mobile συσκευή;
Η SQLite υποστηρίζει βάσεις μέχρι 281 TB θεωρητικά, αλλά στην πράξη ο περιορισμός σε mobile είναι ο αποθηκευτικός χώρος της συσκευής. Για βέλτιστη απόδοση, κρατήστε τη βάση κάτω από 50-100 MB. Αν χρειάζεστε περισσότερα, σκεφτείτε pagination, archiving, ή αποθήκευση μόνο πρόσφατων εγγραφών τοπικά.
Πώς κάνω backup/restore της τοπικής βάσης;
Η SQLite αποθηκεύει τα πάντα σε ένα αρχείο (π.χ. app_data.db3). Για backup, αρκεί να αντιγράψετε αυτό το αρχείο. Βεβαιωθείτε ότι κλείνετε τη σύνδεση πρώτα ή χρησιμοποιήστε το VACUUM INTO command για ασφαλές hot backup. Μπορείτε επίσης να αξιοποιήσετε cloud storage APIs (iCloud, Google Drive) για αυτόματο backup.
Τι γίνεται με την κρυπτογράφηση;
Η SQLite δεν παρέχει native κρυπτογράφηση. Η λύση είναι το SQLCipher (μέσω του NuGet package sqlite-net-sqlcipher) που προσφέρει πλήρη κρυπτογράφηση AES-256. Αν αποθηκεύετε ευαίσθητα δεδομένα χρηστών, αυτό δεν είναι προαιρετικό. Χρησιμοποιήστε το SecureStorage για τα κλειδιά κρυπτογράφησης.
Χρειάζεται η εφαρμογή μου EF Core migrations αν χρησιμοποιώ EnsureCreated();
Καλή ερώτηση. Το Database.EnsureCreated() δημιουργεί τη βάση μόνο αν δεν υπάρχει, αλλά δεν εφαρμόζει migrations. Αν το χρησιμοποιήσετε και αργότερα αλλάξετε το μοντέλο, τα migrations δεν θα δουλέψουν. Για production εφαρμογές, προτιμήστε migrations ή υλοποιήστε χειροκίνητο schema versioning.