Bases de datos locales de .NET MAUI

Browse sample. Examina la muestra.

El motor de base de datos SQLite permite que las aplicaciones de interfaz de usuario de aplicaciones multiplataforma de .NET (.NET MAUI) carguen y guarden objetos de datos en código compartido. Puede integrar SQLite.NET en aplicaciones .NET MAUI para almacenar y recuperar información en una base de datos local siguiendo estos pasos:

  1. Instale el paquete NuGet.
  2. Configure constantes.
  3. Cree una clase de acceso a la base de datos.
  4. Obtener acceso a los datos.
  5. Configuración avanzada.

En este artículo se usa el paquete NuGet sqlite-net-pcl para proporcionar acceso a la base de datos de SQLite a una tabla para almacenar elementos de tareas pendientes. Una alternativa es usar el paquete NuGet Microsoft.Data.Sqlite, que es un proveedor de ADO.NET ligero para SQLite. Microsoft.Data.Sqlite implementa las abstracciones de ADO.NET comunes para funcionalidades como conexiones, comandos y lectores de datos.

Instalación del paquete NuGet de SQLite

Use el administrador de paquetes NuGet para buscar el paquete sqlite-net-pcl y agregar la versión más reciente al proyecto de aplicación MAUI de .NET.

Hay varios paquetes NuGet con nombres similares. El paquete correcto tiene estos atributos:

  • Id.: sqlite-net-pcl
  • Autores: SQLite-net
  • Propietarios: praeclarum
  • Vínculo de NuGet:sqlite-net-pcl

A pesar del nombre del paquete, use el paquete NuGet sqlite-net-pcl en proyectos .NET MAUI.

Importante

SQLite.NET es una biblioteca de terceros compatible con el repositorio praeclarum/sqlite-net.

Instalación de SQLitePCLRaw.bundle_green

Además de sqlite-net-pcl, debe instalar temporalmente la dependencia subyacente que expone SQLite en cada plataforma:

  • Identificador: SQLitePCLRaw.bundle_green
  • Versión:>= 2.1.0
  • Autores: Eric Sink
  • Propietarios: Eric Sink
  • Vínculo de NuGet:SQLitePCLRaw.bundle_green

Configuración de constantes de aplicación

Los datos de configuración, como el nombre de archivo y la ruta de acceso de la base de datos, se pueden almacenar como constantes en la aplicación. El proyecto de ejemplo incluye un archivo Constants.cs que proporciona datos de configuración comunes:

public static class Constants
{
    public const string DatabaseFilename = "TodoSQLite.db3";

    public const SQLite.SQLiteOpenFlags Flags =
        // open the database in read/write mode
        SQLite.SQLiteOpenFlags.ReadWrite |
        // create the database if it doesn't exist
        SQLite.SQLiteOpenFlags.Create |
        // enable multi-threaded database access
        SQLite.SQLiteOpenFlags.SharedCache;

    public static string DatabasePath =>
        Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);
}

En este ejemplo, el archivo de constantes especifica valores de enumeración predeterminados SQLiteOpenFlag que se usan para inicializar la conexión de base de datos. La SQLiteOpenFlag enumeración admite estos valores:

  • Create: la conexión creará automáticamente el archivo de base de datos si no existe.
  • FullMutex: la conexión se abre en modo de subproceso serializado.
  • NoMutex: la conexión se abre en modo multiproceso.
  • PrivateCache: la conexión no participará en la caché compartida, incluso si está habilitada.
  • ReadWrite: la conexión puede leer y escribir datos.
  • SharedCache: la conexión participará en la caché compartida, si está habilitada.
  • ProtectionComplete: el archivo está cifrado y es inaccesible mientras el dispositivo está bloqueado.
  • ProtectionCompleteUnlessOpen: el archivo se cifra hasta que se abre, pero después es accesible incluso si el usuario bloquea el dispositivo.
  • ProtectionCompleteUntilFirstUserAuthentication: el archivo se cifra hasta después de que el usuario haya arrancado y desbloqueado el dispositivo.
  • ProtectionNone: el archivo de base de datos no está cifrado.

Es posible que tenga que especificar marcas diferentes en función de cómo se usará la base de datos. Para obtener más información sobre SQLiteOpenFlags, vea Abrir una nueva Conectar de base de datos en sqlite.org.

Creación de una clase de acceso de base de datos

Una clase contenedora de base de datos abstrae la capa de acceso a datos del resto de la aplicación. Esta clase centraliza la lógica de consulta y simplifica la administración de la inicialización de la base de datos, lo que facilita la refactorización o expansión de las operaciones de datos a medida que crece la aplicación. La aplicación de ejemplo define una TodoItemDatabase clase para este propósito.

Inicialización diferida

TodoItemDatabase usa la inicialización diferida asincrónica para retrasar la inicialización de la base de datos hasta que se accede por primera vez, con un método simple Init al que llama cada método de la clase :

public class TodoItemDatabase
{
    SQLiteAsyncConnection Database;

    public TodoItemDatabase()
    {
    }

    async Task Init()
    {
        if (Database is not null)
            return;

        Database = new SQLiteAsyncConnection(Constants.DatabasePath, Constants.Flags);
        var result = await Database.CreateTableAsync<TodoItem>();
    }
    ...
}

Métodos de manipulación de datos

La TodoItemDatabase clase incluye métodos para los cuatro tipos de manipulación de datos: crear, leer, editar y eliminar. La biblioteca SQLite.NET proporciona un mapa relacional de objetos simple (ORM) que permite almacenar y recuperar objetos sin escribir instrucciones SQL.

En el ejemplo siguiente se muestran los métodos de manipulación de datos en la aplicación de ejemplo:

public class TodoItemDatabase
{
    ...
    public async Task<List<TodoItem>> GetItemsAsync()
    {
        await Init();
        return await Database.Table<TodoItem>().ToListAsync();
    }

    public async Task<List<TodoItem>> GetItemsNotDoneAsync()
    {
        await Init();
        return await Database.Table<TodoItem>().Where(t => t.Done).ToListAsync();

        // SQL queries are also possible
        //return await Database.QueryAsync<TodoItem>("SELECT * FROM [TodoItem] WHERE [Done] = 0");
    }

    public async Task<TodoItem> GetItemAsync(int id)
    {
        await Init();
        return await Database.Table<TodoItem>().Where(i => i.ID == id).FirstOrDefaultAsync();
    }

    public async Task<int> SaveItemAsync(TodoItem item)
    {
        await Init();
        if (item.ID != 0)
            return await Database.UpdateAsync(item);
        else
            return await Database.InsertAsync(item);
    }

    public async Task<int> DeleteItemAsync(TodoItem item)
    {
        await Init();
        return await Database.DeleteAsync(item);
    }
}

Acceso a los datos

La TodoItemDatabase clase se puede registrar como singleton que se puede usar en toda la aplicación si usa la inserción de dependencias. Por ejemplo, puede registrar las páginas y la clase de acceso a la base de datos como servicios en el IServiceCollection objeto, en MauiProgram.cs, con los AddSingleton métodos y AddTransient :

builder.Services.AddSingleton<TodoListPage>();
builder.Services.AddTransient<TodoItemPage>();

builder.Services.AddSingleton<TodoItemDatabase>();

Estos servicios se pueden insertar automáticamente en constructores de clase y acceder a ellos:

TodoItemDatabase database;

public TodoItemPage(TodoItemDatabase todoItemDatabase)
{
    InitializeComponent();
    database = todoItemDatabase;
}

async void OnSaveClicked(object sender, EventArgs e)
{
    if (string.IsNullOrWhiteSpace(Item.Name))
    {
        await DisplayAlert("Name Required", "Please enter a name for the todo item.", "OK");
        return;
    }

    await database.SaveItemAsync(Item);
    await Shell.Current.GoToAsync("..");
}

Como alternativa, se pueden crear nuevas instancias de la clase de acceso a la base de datos:

TodoItemDatabase database;

public TodoItemPage()
{
    InitializeComponent();
    database = new TodoItemDatabase();
}

Para obtener más información sobre la inserción de dependencias en aplicaciones MAUI de .NET, consulte Inserción de dependencias.

Configuración avanzada

SQLite proporciona una API sólida con más características de las que se tratan en este artículo y la aplicación de ejemplo. En las secciones siguientes se tratan las características que son importantes para la escalabilidad.

Para obtener más información, consulte la documentación de SQLite sobre sqlite.org.

Registro de escritura anticipada

De forma predeterminada, SQLite usa un diario de reversión tradicional. Una copia del contenido de la base de datos sin cambios se escribe en un archivo de reversión independiente y, a continuación, los cambios se escriben directamente en el archivo de base de datos. Commit se produce cuando se elimina el diario de reversión.

El registro de escritura previa (WAL) escribe primero los cambios en un archivo WAL independiente. En el modo WAL, commit es un registro especial, anexado al archivo WAL, que permite que varias transacciones se produzcan en un único archivo WAL. Un archivo WAL se combina de nuevo en el archivo de base de datos en una operación especial denominada punto de control.

WAL puede ser más rápido para las bases de datos locales porque los lectores y escritores no se bloquean entre sí, lo que permite que las operaciones de lectura y escritura sean simultáneas. Sin embargo, el modo WAL no permite cambios en el tamaño de página, agrega asociaciones de archivos adicionales a la base de datos y agrega la operación de punto de control adicional.

Para habilitar WAL en SQLite.NET, llame al EnableWriteAheadLoggingAsync método en la SQLiteAsyncConnection instancia de :

await Database.EnableWriteAheadLoggingAsync();

Para obtener más información, consulte Registro de escritura anticipada de SQLite en sqlite.org.

Copia de una base de datos

Hay varios casos en los que puede ser necesario copiar una base de datos de SQLite:

  • Una base de datos se ha enviado con la aplicación, pero debe copiarse o moverse al almacenamiento que se puede escribir en el dispositivo móvil.
  • Debe realizar una copia de seguridad o una copia de la base de datos.
  • Debe actualizar, mover o cambiar el nombre del archivo de base de datos.

En general, mover, cambiar el nombre o copiar un archivo de base de datos es el mismo proceso que cualquier otro tipo de archivo con algunas consideraciones adicionales:

  • Todos los conexione de base de datos deben cerrarse antes de intentar mover el archivo de base de datos.
  • Si usa el registro de escritura previa, SQLite creará un archivo de acceso a memoria compartida (.shm) y un archivo (Registro de escritura anticipada) (.wal). Asegúrese de aplicar también los cambios a estos archivos.