Partilhar via


Xamarin.Forms Bancos de dados locais

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 de tarefas. Este artigo descreve como usar SQLite.Net em código compartilhado para armazenar e recuperar informações em um banco de dados local.

Capturas de tela do aplicativo Todolist no iOS e Android

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

  1. Instale o pacote NuGet.
  2. Configure as constantes.
  3. Crie uma classe de acesso ao banco de dados.
  4. Acesse dados em 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

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

Configurar as constantes do aplicativo

O projeto de exemplo inclui um Constants.cs que fornece os 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 do banco de dados. A enumeração SQLiteOpenFlag dá suporte aos seguintes 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 de multithreading.
  • 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 habilitada.
  • ProtectionComplete: O arquivo é criptografado e inacessível enquanto o dispositivo estiver 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 Opening A New Database Connection no sqlite.org.

Criar uma classe de acesso ao banco de dados

Uma classe de 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 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 retorna a TodoItemDatabase como um 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. Então, quando a construção for concluída, todas as await operações serã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 com o banco de dados é um campo estático que garante que uma única conexão com o 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 iniciaçã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 and Task<T> para criar uma tarefa inicializada lentamente 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 representantes 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, confira AsyncLazy.

Métodos de manipulação de dados

A classe TodoItemDatabase 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);
    }
}

Acesse os dados em Xamarin.Forms

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

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 abrangem recursos importantes para escalabilidade.

Para obter mais informações, consulte a SQLite Documentation no sqlite.org.

Registro em logs write-ahead

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.

O WAL (registro write-ahead) grava as alterações primeiro em um arquivo WAL separado. 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 checkpoint.

O WAL pode ser mais rápido para bancos de dados locais porque leitores e gravadores não se bloqueiam mutuamente, 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 inclui a operação extra de checkpointing.

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

await Database.EnableWriteAheadLoggingAsync();

Para obter mais informações, consulte SQLite Write-Ahead Logging no 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 versionar, 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:

  • Todas as conexões de banco de dados devem ser fechadas antes de tentar mover o arquivo de banco de dados.
  • Se você usar o registro write-ahead, o SQLite criará um arquivo de Acesso à Memória Compartilhada (.shm) e um arquivo de registro write ahead (.wal). Verifique se você aplicou quaisquer alterações a esses arquivos também.

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