Bases de datos locales Xamarin.Forms

El motor de base de datos de SQLite permite a las aplicaciones Xamarin.Forms cargar y guardar objetos de datos en código compartido. La aplicación de ejemplo usa una tabla de base de datos SQLite para almacenar elementos de tareas pendientes. En este artículo se describe cómo usar SQLite.Net en código compartido para almacenar y recuperar información en una base de datos local.

Capturas de pantalla de la aplicación Todolist en iOS y Android

Integre SQLite.NET en aplicaciones móviles siguiendo estos pasos:

  1. Instala el paquete NuGet .
  2. Configurar constantes.
  3. Crear una clase de acceso a la base de datos.
  4. Acceso a los datos en Xamarin.Forms.
  5. Configuración avanzada.

Instalar el paquete de SQLite NuGet

Use el administrador de paquetes NuGet para buscar sqlite-net-pcl y agregar la versión más reciente al proyecto de código compartido.

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

Independientemente del nombre del paquete, utilice el paquete de NuGet sqlite-net-pcl incluso en los proyectos de .NET Standard.

Importante

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

Configurar constantes de 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
    {
        get
        {
            var basePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            return Path.Combine(basePath, DatabaseFilename);
        }
    }
}

El archivo de constantes especifica los valores de enumeración SQLiteOpenFlag predeterminados que se usan para inicializar la conexión de base de datos. La enumeración SQLiteOpenFlag 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 está cifrado hasta que se abre, pero después es accesible incluso si el usuario bloquea el dispositivo.
  • ProtectionCompleteUntilFirstUserAuthentication: el archivo está cifrado 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 tengas que especificar marcas diferentes en función de cómo se usará la base de datos. Para obtener más información sobre SQLiteOpenFlags, consulta Abrir una nueva conexión de base de datos en sqlite.org.

Crear 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 gestió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 Todo define una clase TodoItemDatabase para este propósito.

Inicialización diferida

TodoItemDatabase usa la inicialización diferida asincrónica, representada por la clase AsyncLazy<T> personalizada, para retrasar la inicialización de la base de datos hasta que se tenga acceso a ella por primera vez:

public class TodoItemDatabase
{
    static SQLiteAsyncConnection Database;

    public static readonly AsyncLazy<TodoItemDatabase> Instance = new AsyncLazy<TodoItemDatabase>(async () =>
    {
        var instance = new TodoItemDatabase();
        CreateTableResult result = await Database.CreateTableAsync<TodoItem>();
        return instance;
    });

    public TodoItemDatabase()
    {
        Database = new SQLiteAsyncConnection(Constants.DatabasePath, Constants.Flags);
    }

    //...
}

El campo Instance se usa para crear la tabla de base de datos para el objeto TodoItem, si aún no existe, y devuelve TodoItemDatabase como singleton. El campo Instance de tipo AsyncLazy<TodoItemDatabase> se construye la primera vez que se espera. Si varios subprocesos intentan acceder al campo simultáneamente, todos usarán la construcción única. A continuación, cuando finalice la construcción, todas las operaciones await se completan. Además, las operaciones await después de completar la construcción continúan inmediatamente, ya que el valor está disponible.

Nota:

La conexión de base de datos es un campo estático que garantiza que se use una conexión de base de datos única durante la vida útil de la aplicación. El uso de una conexión estática y persistente ofrece un mejor rendimiento que abrir y cerrar conexiones varias veces durante una sola sesión de aplicación.

Inicialización diferida asincrónica

Para iniciar la inicialización de la base de datos, evitar el bloqueo de la ejecución y tener la oportunidad de detectar excepciones, la aplicación de ejemplo usa la inicialización diferida asincrónica, representada por la clase AsyncLazy<T>:

public class AsyncLazy<T>
{
    readonly Lazy<Task<T>> instance;

    public AsyncLazy(Func<T> factory)
    {
        instance = new Lazy<Task<T>>(() => Task.Run(factory));
    }

    public AsyncLazy(Func<Task<T>> factory)
    {
        instance = new Lazy<Task<T>>(() => Task.Run(factory));
    }

    public TaskAwaiter<T> GetAwaiter()
    {
        return instance.Value.GetAwaiter();
    }
}

La clase AsyncLazy combina los tipos Lazy<T> y Task<T> para crear una tarea inicializada diferida que representa la inicialización de un recurso. El delegado de fábrica que se pasa al constructor puede ser sincrónico o asincrónico. Los delegados de fábrica se ejecutarán en un subproceso de grupo de subprocesos y no se ejecutarán más de una vez (incluso cuando varios subprocesos intenten iniciarlos simultáneamente). Cuando se completa un delegado de fábrica, el valor inicializado diferido está disponible y los métodos que esperan a la instancia AsyncLazy<T> reciben el valor. Para más información, vea AsyncLazy.

Métodos de manipulación de datos

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

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

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

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

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

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

Acceso a datos en Xamarin.Forms

La clase TodoItemDatabase expone el campo Instance, a través del cual se pueden invocar las operaciones de acceso a datos de la clase TodoItemDatabase:

async void OnSaveClicked(object sender, EventArgs e)
{
    var todoItem = (TodoItem)BindingContext;
    TodoItemDatabase database = await TodoItemDatabase.Instance;
    await database.SaveItemAsync(todoItem);

    // Navigate backwards
    await Navigation.PopAsync();
}

Configuración avanzada

SQLite proporciona una API sólida con más características de las que se tratan en este artículo y en 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, consulta SQLite Documentation en sqlite.org.

Registro de escritura previa

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 después 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. Pero 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, llama al método EnableWriteAheadLoggingAsync en la instancia SQLiteAsyncConnection:

await Database.EnableWriteAheadLoggingAsync();

Para obtener más información, consulta SQLite Write-Ahead Logging 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.
  • Debes realizar una copia de seguridad o una copia de la base de datos.
  • Debes 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 las conexiones de base de datos deben cerrarse antes de intentar mover el archivo de base de datos.
  • Si usas el Registro de escritura previa, SQLite creará un archivo de acceso a memoria compartida (.shm) y un archivo .wal (Write Ahead Log). Asegúrate de aplicar también los cambios a estos archivos.

Para obtener más información, consulte Control de archivos en Xamarin.Forms.