Xamarin.Forms 本地数据库

SQLite 数据库引擎允许 Xamarin.Forms 应用程序在共享代码中加载和保存数据对象。 示例应用程序使用 SQLite 数据库表存储待办事项。 本文介绍如何使用共享代码中的 SQLite.Net 在本地数据库中存储和检索信息。

iOS 和 Android 上的 Todolist 应用的屏幕截图

按照以下步骤将 SQLite.NET 集成到移动应用中:

  1. 安装 NuGet 包
  2. 配置常量
  3. 创建数据库访问类
  4. 访问 Xamarin.Forms 中的数据
  5. 高级配置。

安装 SQLite NuGet 包

使用 NuGet 包管理器搜索 sqlite-net-pcl,并将最新版本添加到共享代码项目

许多 NuGet 包都有着类似的名称。 正确的包具有以下属性:

  • ID: sqlite net pcl
  • 作者:SQLite-net
  • 所有者:praeclarum
  • NuGet 链接:sqlite-net-pcl

不管包名称,即便在 .NET Standard 项目中也使用 sqlite-net-pcl NuGet 包

重要

SQLite.NET 是 praeclarum/sqlite-net 存储库支持的第三方库。

配置应用常量

示例项目包括一个 Constants.cs 文件,该文件提供常见配置数据:

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

常量文件指定用于初始化数据库连接的默认 SQLiteOpenFlag 枚举值。 SQLiteOpenFlag 枚举支持以下值:

  • Create:连接将自动创建数据库文件(如果不存在)。
  • FullMutex:连接在序列化线程模式下打开。
  • NoMutex:连接在多线程模式下打开。
  • PrivateCache:即使连接已启用,连接也不会参与共享缓存。
  • ReadWrite:连接可以读取和写入数据。
  • SharedCache:如果启用了共享缓存,则连接将参与共享缓存。
  • ProtectionComplete:设备锁定时,文件会被加密且不可访问。
  • ProtectionCompleteUnlessOpen:文件会被加密,直到文件打开,但之后即使用户锁定设备,也可以访问该文件。
  • ProtectionCompleteUntilFirstUserAuthentication:文件会被加密,直到用户启动并解锁设备。
  • ProtectionNone:数据库文件不会被加密。

可能需要根据数据库的使用方式指定不同的标记。 有关 SQLiteOpenFlags 的详细信息,请参阅 sqlite.org 上的打开新数据库连接

创建数据库访问类

数据库包装类从应用的其余部分抽取数据访问层。 该类集中查询逻辑并且简化数据库初始化的管理,使得应用增长时重构或扩展数据操作更容易。 Todo 应用为此目的定义了一个 TodoItemDatabase 类。

延缓初始化

TodoItemDatabase 使用由自定义 AsyncLazy<T> 类表示的异步延迟初始化来延迟数据库的初始化,直到首次访问数据库:

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

    //...
}

Instance 字段用于为 TodoItem 对象创建数据库表(如果尚不存在),并返回 TodoItemDatabase 作为单一实例。 类型为 AsyncLazy<TodoItemDatabase>Instance 字段是在首次等待时构造的。 如果多个线程同时尝试访问该字段,它们都将使用单一构造。 然后,当构造完成时,所有 await 操作都会完成。 此外,构造完成后的任何 await 操作都会立即继续,因为该值可用。

注意

数据库连接是一个静态字段,可确保将单一数据库连接用于应用的生命周期。 使用持久性静态连接比在单个应用会话期间多次打开和关闭连接提供的性能更好。

异步延迟初始化

为了启动数据库初始化,避免阻塞执行,并有机会捕获异常,示例应用程序使用由 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();
    }
}

AsyncLazy 类合并了 Lazy<T>Task<T> 类型,以创建一个延迟初始化任务来表示资源初始化。 传递给构造函数的工厂委托可以是同步的,也可以是异步的。 工厂委托将在线程池线程上运行,并且不会多次执行(即使多个线程尝试同时启动它们)。 工厂委托完成后,延迟初始化的值将可用,并且任何等待 AsyncLazy<T> 实例的方法都会接收该值。 有关详细信息,请参阅 AsyncLazy

数据操作方法

TodoItemDatabase 类包括四种数据操作方法类型:创建、读取、编辑和删除。 SQLite.NET 库提供一个简单的对象关系映射 (ORM),无需编写 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);
    }
}

访问 Xamarin.Forms 中的数据

TodoItemDatabase 类公开 Instance 字段,通过该字段可以调用 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();
}

高级配置

SQLite 提供一个强大的 API,其功能远超本文和示例应用所涵盖的范围。 下列部分介绍对可扩展性非常重要的功能。

有关详细信息,请参阅 sqlite.org 上的 SQLite 文档

预写日志

默认情况下,SQLite 使用传统的回滚日志。 未更改的数据库内容副本会写入单独的回滚文件,然后更改内容直接写入数据库文件。 删除回滚日志时,会发生 COMMIT。

预写日志 (WAL) 首先将更改写入单独的 WAL 文件中。 在 WAL 模式下,COMMIT 是附加到 WAL 文件的特殊记录,允许在单个 WAL 文件中发生多个事务。 WAL 文件会在称为检查点的特殊操作中合并回数据库文件。

WAL 对于本地数据库来说速度更快,因为读取器和编写器不会相互阻塞,读写操作可以并发进行。 但是,WAL 模式不允许更改页面大小,会向数据库添加其他的文件关联,并增加额外的检查点操作。

如果要在 SQLite.NET 中启用 WAL,请调用 SQLiteAsyncConnection 实例上的 EnableWriteAheadLoggingAsync 方法:

await Database.EnableWriteAheadLoggingAsync();

有关详细信息,请参阅 sqlite.org 上的 SQLite 预写日志

复制数据库

在有几种情况下,可能需要复制 SQLite 数据库:

  • 数据库已随应用程序一起提供,但必须复制或移动到移动设备上的可写存储。
  • 需要创建数据库的备份或副本。
  • 需要对数据库文件进行版本控制、移动或重命名。

通常说来,移动、重命名或复制数据库文件的过程与任何其他文件类型相同,但需要注意一些其他注意事项:

  • 尝试移动数据库文件之前,应关闭所有数据库连接。
  • 如果使用预写日志,SQLite 将创建一个共享内存访问 (.shm) 文件和一个(预写日志)(.wal) 文件。 请确保也为这些文件应用了更改。

有关详细信息,请参阅 Xamarin.Forms 中的文件处理