Xamarin.Forms Bancos de dados locais

Baixar exemplo Baixar o exemplo

O mecanismo de banco de dados SQLite permite que Xamarin.Forms os aplicativos carreguem e salvem objetos de dados em código compartilhado. O aplicativo de exemplo usa uma tabela de banco de dados SQLite para armazenar itens pendentes. Este artigo descreve como usar SQLite.Net em código compartilhado para armazenar e recuperar informações em um banco de dados local.

Screenshots of the Todolist app on iOS and Android

Integre SQLite.NET em aplicativos móveis seguindo estas etapas:

  1. Instale o pacote NuGet.
  2. Configurar constantes.
  3. Crie uma classe de acesso de banco de dados.
  4. Acessar dados no Xamarin.Forms.
  5. Configuração avançada.

Instalar o pacote NuGet do SQLite

Use o gerenciador de pacotes NuGet para pesquisar sqlite-net-pcl e adicionar a versão mais recente ao projeto de código compartilhado.

Há diversos pacotes NuGet com nomes semelhantes. O pacote correto tem estes atributos:

  • ID: sqlite-net-pcl
  • Autores: SQLite-net
  • Proprietários: praeclarum
  • Link do NuGet:sqlite-net-pcl

Apesar do nome do pacote, use o pacote NuGet sqlite-net-pcl, mesmo em projetos do .NET Standard.

Importante

SQLite.NET é uma biblioteca de terceiros com suporte do repositório praeclarum/sqlite-net.

Configurar constantes de aplicativo

O projeto de exemplo inclui um arquivo Constants.cs que fornece dados comuns de configuração:

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);
        }
    }
}

O arquivo de constantes especifica valores de enumeração padrão SQLiteOpenFlag que são usados para inicializar a conexão de banco de dados. A SQLiteOpenFlag enumeração dá suporte a esses valores:

  • Create: a conexão criará automaticamente o arquivo de banco de dados se ele não existir.
  • FullMutex: a conexão é aberta no modo de threading serializado.
  • NoMutex: a conexão é aberta no modo multi-threading.
  • PrivateCache: a conexão não participará do cache compartilhado, mesmo que esteja habilitada.
  • ReadWrite: a conexão pode ler e gravar dados.
  • SharedCache: a conexão participará do cache compartilhado, se estiver habilitado.
  • ProtectionComplete: o arquivo é criptografado e inacessível enquanto o dispositivo está bloqueado.
  • ProtectionCompleteUnlessOpen: o arquivo é criptografado até ser aberto, mas pode ser acessado mesmo se o usuário bloquear o dispositivo.
  • ProtectionCompleteUntilFirstUserAuthentication: o arquivo é criptografado até que o usuário tenha inicializado e desbloqueado o dispositivo.
  • ProtectionNone: o arquivo de banco de dados não está criptografado.

Talvez seja necessário especificar sinalizadores diferentes dependendo de como seu banco de dados será usado. Para obter mais informações sobre SQLiteOpenFlags, consulte Abrindo uma nova conexão de banco de dados no sqlite.org.

Criar uma classe de acesso ao banco de dados

Uma classe wrapper de banco de dados abstrai a camada de acesso a dados do restante do aplicativo. Essa classe centraliza a lógica de consulta e simplifica o gerenciamento da inicialização do banco de dados, facilitando a refatoração ou a expansão das operações de dados à medida que o aplicativo cresce. O aplicativo Todo define uma TodoItemDatabase classe para essa finalidade.

Inicialização lenta

O TodoItemDatabase usa a inicialização lenta assíncrona, representada pela classe personalizada AsyncLazy<T> , para atrasar a inicialização do banco de dados até que ele seja acessado pela primeira 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);
    }

    //...
}

O Instance campo é usado para criar a tabela de banco de dados para o TodoItem objeto, se ele ainda não existir e retornará um TodoItemDatabase como singleton. O Instance campo do tipo AsyncLazy<TodoItemDatabase> é construído na primeira vez que é aguardado. Se vários threads tentarem acessar o campo simultaneamente, todos eles usarão a construção única. Em seguida, quando a construção for concluída, todas as await operações são concluídas. Além disso, todas as await operações após a conclusão da construção continuam imediatamente, pois o valor está disponível.

Observação

A conexão de banco de dados é um campo estático que garante que uma única conexão de banco de dados seja usada durante a vida útil do aplicativo. O uso de uma conexão estática persistente oferece melhor desempenho do que abrir e fechar conexões várias vezes durante uma única sessão de aplicativo.

Inicialização lenta assíncrona

Para iniciar a inicialização do banco de dados, evitar o bloqueio da execução e ter a oportunidade de capturar exceções, o aplicativo de exemplo usa a initalização lenta assíncrona, representada pela AsyncLazy<T> classe :

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();
    }
}

A AsyncLazy classe combina os Lazy<T> tipos e Task<T> para criar uma tarefa inicializada lenta que representa a inicialização de um recurso. O delegado de fábrica que é passado para o construtor pode ser síncrono ou assíncrono. Os delegados de fábrica serão executados em um thread do pool de threads e não serão executados mais de uma vez (mesmo quando vários threads tentarem iniciá-los simultaneamente). Quando um delegado de fábrica é concluído, o valor inicializado lentamente está disponível e todos os métodos que aguardam a AsyncLazy<T> instância recebem o valor. Para obter mais informações, consulte AsyncLazy.

Métodos de manipulação de dados

A TodoItemDatabase classe inclui métodos para os quatro tipos de manipulação de dados: criar, ler, editar e excluir. A biblioteca SQLite.NET fornece um ORM (Mapa Relacional de Objeto) simples que permite armazenar e recuperar objetos sem gravar instruções 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);
    }
}

Acessar dados em Xamarin.Forms

A TodoItemDatabase classe expõe o Instance campo, por meio do qual as operações de acesso a dados na TodoItemDatabase classe podem ser invocadas:

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();
}

Configuração avançada

O SQLite fornece uma API robusta com mais recursos do que os abordados neste artigo e no aplicativo de exemplo. As seções a seguir abordam recursos importantes para escalabilidade.

Para obter mais informações, consulte Documentação do SQLite sobre sqlite.org.

Registro em log de gravação antecipada

Por padrão, o SQLite usa um diário de reversão tradicional. Uma cópia do conteúdo do banco de dados inalterado é gravada em um arquivo de reversão separado e, em seguida, as alterações são gravadas diretamente no arquivo de banco de dados. O COMMIT ocorre quando o diário de reversão é excluído.

Write-Ahead Logging (WAL) grava as alterações em um arquivo WAL separado primeiro. No modo WAL, um COMMIT é um registro especial, acrescentado ao arquivo WAL, que permite que várias transações ocorram em um único arquivo WAL. Um arquivo WAL é mesclado de volta ao arquivo de banco de dados em uma operação especial chamada ponto de verificação.

O WAL pode ser mais rápido para bancos de dados locais porque leitores e gravadores não bloqueiam uns aos outros, permitindo que as operações de leitura e gravação sejam simultâneas. No entanto, o modo WAL não permite alterações no tamanho da página, adiciona associações de arquivos adicionais ao banco de dados e adiciona a operação de ponto de verificação extra.

Para habilitar o WAL no SQLite.NET, chame o EnableWriteAheadLoggingAsync método na SQLiteAsyncConnection instância:

await Database.EnableWriteAheadLoggingAsync();

Para obter mais informações, consulte SQLite Write-Ahead Log on sqlite.org.

Copiar um banco de dados

Há vários casos em que pode ser necessário copiar um banco de dados SQLite:

  • Um banco de dados foi enviado com seu aplicativo, mas deve ser copiado ou movido para o armazenamento gravável no dispositivo móvel.
  • Você precisa fazer um backup ou cópia do banco de dados.
  • Você precisa ver, mover ou renomear o arquivo de banco de dados.

Em geral, mover, renomear ou copiar um arquivo de banco de dados é o mesmo processo que qualquer outro tipo de arquivo com algumas considerações adicionais:

  • Todos os conexões de banco de dados devem ser fechados antes de tentar mover o arquivo de banco de dados.
  • Se você usar o log Write-Ahead, o SQLite criará um arquivo de Acesso à Memória Compartilhada (.shm) e um arquivo (Log Write Ahead) (.wal). Verifique se você também aplica alterações a esses arquivos.

Para obter mais informações, consulte Tratamento de arquivos em Xamarin.Forms.