65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 和 VB 中使用 SQLite

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (20投票s)

2018 年 7 月 1 日

CPOL

25分钟阅读

viewsIcon

63381

downloadIcon

2089

DotNet.Core.SQLite - 一个轻量级高性能的 SQLite 操作封装库,包含 C# 和 VB 的使用示例

有关 zip 文件内容的详细说明,请参阅 包含的项目 (示例代码)...

目录

背景

在过去几年里,我与 SQLite 进行了大量的合作,并构建了一个名为 DotNet.Core.SQLite 的库封装。我喜欢尽可能贴近底层进行操作,以确保获得最佳性能。

该库已经发展成为一个稳定的解决方案,我觉得其他人也可以从中受益。因此,我在这篇文章中将其公之于众。

引言

这不是一篇 SQLite 入门文章,也不是高级文章。它假定您已经有使用 SQLite 或其他数据库的经验,但会涵盖一些如何使用 SQLite 的代码示例。本文介绍了 DotNet.Core.SQLite 库中的功能以及如何使用它们。

如果您刚开始学习,可以在 CodeProject 或其他地方找到其他文章。

如果您期望本文涵盖 SQLite 的 Entity Framework,抱歉,本文不适合您。

本文共三部分。第一部分介绍如何使用它,第二部分对一些核心功能进行基准测试,第三部分深入探讨库代码的各个部分。

我必须提前为文章的长度道歉。它是在我业余时间的 6 个月内完成的,详细介绍了 DotNet.Core.SQLite 库,涵盖了使用、基准测试以及内部工作原理。

概述

这个 DotNet.Core.SQLite 库并非标准 SQLite 库方法和类的替代品,而是补充。因此,您可以将部分内容与现有代码结合使用,或者用这些 DotNet.Core.SQLite 库类和扩展方法替换整个逻辑。

其中包括一套我用于日志记录、调试和通用开发中的类和扩展。DotNet.Core.SQLite 库支持以下功能:

  1. 通过 DBContextDbRegistry 类支持多数据库
  2. 通过 DbResourceContext<T> -> DbTableContext<T> & DbViewContext<T> 支持表和视图上下文及方法
  3. 完整的异步支持
  4. 用于事务(带自动回滚)的工作单元
  5. 支持 SQLite 保存点 (嵌套事务)
  6. 完整的 Pragma 命令支持 - GetPragmaSetPragmaGetPragmas (批量查询)
  7. 自定义跟踪和日志记录支持 - 通过自定义类选择要跟踪的事件类型
  8. 数据库架构、设置和连接状态信息报告
  9. DDL (数据定义语言) 生成

DotNet.Core.SQLite 库和以下文章将为您提供实现同样目标的工具和示例代码。

要求

DotNet.Core.SQLite 库使用以下技术构建:

  • .NET Framework 4.5 版本 (基准测试使用 4.6.1)
  • C# 语言版本 7.0
  • System.Data.SQLite 版本 1.0.106

官方网站:http://www.sqlite.org & System.Data.SQLite

Nuget:System.Data.SQLite

工具

以下是用于操作 SQLite 的第三方工具列表:

如何使用

在查看 DotNet.Core.SQLite 库的构建方式之前,让我们先看看它的使用方法有多简单。

连接和查询数据库

在我们查看 DotNet.Core.SQLite 库如何简化数据库、表、视图和查询的操作之前,让我们回顾一下通常是如何完成的。

要手动连接和执行查询,通常需要编写如下代码:

var connectionString = "data source=northwind.db;version=3";

using (var conn = new SQLiteConnection(connectionString))
using (var cmd = new SQLiteCommand("SELECT COUNT (*) FROM Products", conn))
{
    conn.Open();
    using (var coreReader = cmd.ExecuteReader())
    {
        coreReader.Read();
        var productCount = coreReader.HasRows ? coreReader.GetInt64(0) : 0;
        Console.WriteLine($"\n** Total Products: {productCount}\n");
    }
}
Dim connectionString = "data source=northwind.db;version=3"

Using conn = New SQLiteConnection(connectionString)
    Using cmd = New SQLiteCommand("SELECT COUNT (*) FROM Products", conn)
        conn.Open()

        Using coreReader = cmd.ExecuteReader()
            coreReader.Read()
            Dim productCount = If(coreReader.HasRows, coreReader.GetInt64(0), 0)
            Console.WriteLine($"** Total Products: {productCount}")
        End Using
    End Using
End Using

使用 DotNet.Core.SQLite 库,我们将做几乎相同的事情:

var connectionString = "data source=northwind.db;version=3";

using (var dbCtx = new DbContext(connectionString))
using (var conn = dbCtx.ConnectionFactory())
using (var cmd = new SQLiteCommand("SELECT COUNT (*) FROM Products", conn))
{
    conn.Open();
    using (var coreReader = cmd.ExecuteReader())
    {
        coreReader.Read();
        var productCount = coreReader.HasRows ? coreReader.GetInt64(0) : 0;
        Console.WriteLine($"\n** Total Products: {productCount}\n");
    }
}
Dim connectionString = "data source=northwind.db;version=3"

Using dbCtx = New DbContext(connectionString)
    Using conn = dbCtx.ConnectionFactory()
        Using cmd = New SQLiteCommand("SELECT COUNT (*) FROM Products", conn)
            conn.Open()

            Using coreReader = cmd.ExecuteReader()
                coreReader.Read()
                Dim productCount = If(coreReader.HasRows, coreReader.GetInt64(0), 0)
                Console.WriteLine($"** Total Products: {productCount}")
            End Using
        End Using
    End Using
End Using

上面使用 DotNet.Core.SQLite 库的示例在封装普通代码时看起来代码量更多。除非是为了添加跟踪 (见下文) 而处理遗留代码,否则您不会这样做。它所演示的,以及接下来的部分,是 DotNet.Core.SQLite 库如何与现有的 SQLite 库无缝协同工作,允许选择集成级别而不会影响性能。

下一部分将介绍 DotNet.Core.SQLite 库如何简化上述示例。

那么,我们为什么需要 DbContext?

DbContext 代表一个数据库,并保存连接详细信息和 SqliteEventTraceBase 类的引用 (下一节将详细介绍)。DbContext 有许多扩展方法,用于简化对单个数据库的基本和复杂操作。您可以拥有多个 DbContext 对象,每个 数据库 (带有附加的数据库) 一个。

为了简化许多任务,DotNet.Core.SQLite 库提供了许多扩展方法。因此,我们可以通过调用 SQLiteConnection 扩展方法 RecordCount 来简化这一点:

var connectionString = "data source=northwind.db;version=3";

using (var dbCtx = new DbContext(connectionString))
using (var conn = dbCtx.ConnectionFactory()) // Create & Trace (if listening)
{
    var productCount = conn.RecordCount("Products");
    Console.WriteLine($"\n** Total Products: {productCount}\n");
}
Dim connectionString = "data source=northwind.db;version=3"

Using dbCtx = New DbContext(connectionString)
    Using conn = dbCtx.ConnectionFactory()
        Dim productCount = conn.RecordCount("Products")
        Console.WriteLine($"** Total Products: {productCount}")
    End Using
End Using

或者,我们可以通过调用 DbContext 扩展方法 RecordCount 来进一步简化:

var connectionString = "data source=northwind.db;version=3";

using (var dbCtx = new DbContext(connectionString))
{
    var productCount = dbCtx.RecordCount("Products");
    Console.WriteLine($"\n** Total Products: {productCount}\n");
}
Dim connectionString = "data source=northwind.db;version=3"

Using dbCtx = New DbContext(connectionString)
    Dim productCount = dbCtx.RecordCount("Products")
    Console.WriteLine($"** Total Products: {productCount}")
End Using

如果配置了表、视图或查询的 DbResourceContext<T> (见下文),那么我们可以将其缩减为一行代码:

var productCount = dbRegistry.Resolve<Product>().Count();
Dim productCount = dbRegistry.Resolve(Of Product)().Count()

这将定位与 DbContext 关联的 Product POCO 的 DbResourceContext<T> 对象,打开连接,执行查询,释放资源,并返回结果。稍后,本文还将介绍另一个适用于 SQLiteConnection 并且支持自定义查询的语法。

上面所有 5 个版本的代码的输出将是:

** Total Products: 77

RecordCount 扩展方法作用于 DbContextSQLiteConnection 类。具体使用哪一个取决于单个命令是针对连接执行,还是需要针对单个连接执行多个命令。还有许多其他扩展方法可以执行相同的操作。下一节将展示这为什么很重要。

DbContext 类包含许多用于设置连接字符串的构造函数和方法。上面,我们在 DbContext 类的构造函数中传递了连接字符串。

SQLiteConnectionStringBuilder 支持

除了传递连接字符串,还支持 SQLite SQLiteConnectionStringBuilder 类:

var builder = new SQLiteConnectionStringBuilder
{
    Version = 3,
    DataSource = "northwind.db"
};

using (var dbCtx = new DbContext(builder))
{
    //...
}
Dim builder = New SQLiteConnectionStringBuilder With {
        .Version = 3,
        .DataSource = "northwind.db",
    }

Using dbCtx = New DbContext(connectionString)
    '...
End Using

var builder = new SQLiteConnectionStringBuilder
{
    Version = 3,
    DataSource = "northwind.db"
};

using (var dbCtx = new DbContext())
{
    dbCtx.SetConnectionString(builder);
    //...
}
Dim builder = New SQLiteConnectionStringBuilder With {
        .Version = 3,
        .DataSource = "northwind.db",
    }

Using dbCtx = New DbContext()
    dbx.SetConnectionString(builder)
    '...
End Using

我们也可以对 ConnectionString 做同样的事情:

var connectionString = "data source=northwind.db;version=3";

using (var dbCtx = new DbContext())
{
    dbCtx.SetConnectionString(connectionString);
    //...
}
Dim connectionString = "data source=northwind.db;version=3"

Using dbCtx = New DbContext()
    dbCtx.SetConnectionString(connectionString)
    '...
End Using

带跟踪的连接和查询数据库

要优化或调试任何代码,能够看到第三方库内部发生的情况并不总是容易的。幸运的是,SQLite 有一些我们可以订阅的事件,以了解正在发生的事情以及可以在何处应用优化。

namespace System.Data.SQLite
{
    public sealed class SQLiteConnection : DbConnection, ICloneable, IDisposable
    {
        //
        // Summary:
        //     This event is raised when events related to 
        //     the lifecycle of a SQLiteConnection object occur.
        public static event SQLiteConnectionEventHandler Changed;
        //
        // Summary:
        //     This event is raised whenever SQLite is committing a transaction. 
        //     Return non-zero to trigger a rollback.
        public event SQLiteCommitHandler Commit;
        //
        // Summary:
        //     This event is raised whenever 
        //     SQLite makes an update/delete/insert into the database
        //     on this connection. It only applies to the given connection.
        public event SQLiteUpdateEventHandler Update;
        //
        // Summary:
        //     This event is raised whenever SQLite encounters an action 
        //     covered by the authorizer during query preparation. 
        //     Changing the value of the System.Data.SQLite.AuthorizerEventArgs.ReturnCode
        //     property will determine if the specific action will be allowed, 
        //     ignored, or denied.
        //     For the entire duration of the event, the associated connection and statement
        //     objects must not be modified, either directly or indirectly, 
        //     by the called code.
        public event SQLiteAuthorizerEventHandler Authorize;
        //
        // Summary:
        //     This event is raised periodically during long running queries. 
        //     Changing the value of the System.Data.SQLite.ProgressEventArgs.ReturnCode 
        //     property will determine if the operation in progress 
        //     will continue or be interrupted. For the entire duration of the event, 
        //     the associated connection and statement objects must not
        //     be modified, either directly or indirectly, by the called code.
        public event SQLiteProgressEventHandler Progress;
        //
        // Summary:
        //     This event is raised whenever the database is opened or closed.
        public override event StateChangeEventHandler StateChange;
        //
        // Summary:
        //     This event is raised whenever SQLite statement first begins executing on this
        //     connection. It only applies to the given connection.
        public event SQLiteTraceEventHandler Trace;
        //
        // Summary:
        //     This event is raised whenever SQLite is rolling back a transaction.
        public event EventHandler RollBack;
    }
}

跟踪是通过 Tracing 类和 Writer 类完成的。有几个预定义的类可以使用,或者您可以实现自己的类,使用提供的基类。

要使用跟踪,我们将修改上面的代码如下:

var connectionString = "data source=northwind.db;version=3";

// Handles output from trace
var writer = new SimpleConsoleTextWriter();

// Set what to trace and where to output the results
var trace = new SqliteCsvEventTrace(writer, SqliteConnectionTraceType.Full);

using (var dbCtx = new DbContext(connectionString, trace))
{
    // ...
}
Dim connectionString = "data source=northwind.db;version=3"

' Handles output from trace
Dim writer = New SimpleConsoleTextWriter()

' Set what to trace and where to output the results
Dim trace = New SqliteCsvEventTrace(writer, SqliteConnectionTraceType.Full)

Using dbCtx = New DbContext(connectionString, trace)
    '...
End Using

现在,如果我们重新编写上面最后一个示例中的代码,它看起来会像这样:

var connectionString = "data source=northwind.db;version=3";

// Handles output from trace
var writer = new SimpleConsoleTextWriter();

// Set what to trace and where to output the results
var trace = new SqliteCsvEventTrace(writer, SqliteConnectionTraceType.Full);

using (var dbCtx = new DbContext(connectionString, trace))
{
    var result = dbCtx.RecordCount("Products");
    Console.WriteLine($"\n** Total Products: {result}");
}
Dim connectionString = "data source=northwind.db;version=3"

' Handles output from trace
Dim writer = New SimpleConsoleTextWriter()

' Set what to trace and where to output the results
Dim trace = New SqliteCsvEventTrace(writer, SqliteConnectionTraceType.Full)

Using dbCtx = New DbContext(connectionString, trace)
    Dim productCount = dbCtx.RecordCount("Products")
    Console.WriteLine()
    Console.WriteLine($"** Total Products : {productCount}")
    End Using
End Using

RecordCount 扩展方法作用于 DbContext,然后如果存在 Trace,它将自动监听 SQLite 的事件。

输出将是:

_TRACE: "northwind.db","Listening","74DB74"
_TRACE: "74DB74","Event","Opening",""
_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Event","ConnectionString","version=3;data source=northwind.db"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "C:\SQLiteWrapper\Samples\Tracing\bin\Debug\northwind.db"
_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","DisposingCommand",""
_TRACE: "74DB74","State","Open"
_TRACE: "74DB74","Event","Opened",""
_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","NewDataReader",""
_TRACE: "74DB74","Auth","","Select","","","Ok"
_TRACE: "74DB74","Auth","","Function","","count","Ok"
_TRACE: "74DB74","Auth","","Read","Products","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle","SELECT Count(*) FROM Products"
_TRACE: "74DB74","Event","DisposingDataReader",""
_TRACE: "74DB74","Event","ClosingDataReader",""
_TRACE: "74DB74","Event","DisposingCommand",""
_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","State","Closed"
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Disposing"

** Total Products: 77

现在我们可以确切地看到 SQLite 引擎在做什么。每个连接都有一个 Id 来跟踪与该连接关联的所有输出。

使用跟踪优化代码的示例

为了演示跟踪的好处,以下优化示例将逐步介绍查询两个表的记录计数的过程。

我们启用跟踪,并针对 DbContext 为每个表调用 RecordCount

var connectionString = "data source=northwind.db;version=3";

// Handles output from trace
var writer = new SimpleConsoleTextWriter();

// Set what to trace and where to output the results
var trace = new SqliteCsvEventTrace(writer, SqliteConnectionTraceType.Full);

using (var dbCtx = new DbContext(connectionString, trace))
{
    var productCount = dbCtx.RecordCount("Products");
    var supplierCount = dbCtx.RecordCount("Supplierss");

    Console.WriteLine($"\n** Total Products : {productCount}");
    Console.WriteLine($"** Total Suppliers: {supplierCount}");
}
Dim connectionString = "data source=northwind.db;version=3"

' Handles output from trace
Dim writer = New SimpleConsoleTextWriter()

' Set what to trace and where to output the results
Dim trace = New SqliteCsvEventTrace(writer, SqliteConnectionTraceType.Full)

Using dbCtx = New DbContext(connectionString, trace)
    Dim productCount = dbCtx.RecordCount("Products")
    Dim supplierCount = dbCtx.RecordCount("Suppliers")

    Console.WriteLine()
    Console.WriteLine($"** Total Products : {productCount}")
    Console.WriteLine($"** Total Suppliers: {supplierCount}")
End Using

跟踪输出将是:

_TRACE: "northwind.db","Listening","74DB74"
_TRACE: "74DB74","Event","Opening",""
_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Event","ConnectionString","data source=northwind.db;version=3"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "C:\SQLiteWrapper\Samples\Tracing\bin\Debug\northwind.db"
_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","DisposingCommand",""
_TRACE: "74DB74","State","Open"
_TRACE: "74DB74","Event","Opened","data source=northwind.db;version=3"
_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","NewDataReader",""
_TRACE: "74DB74","Auth","","Select","","","Ok"
_TRACE: "74DB74","Auth","","Function","","count","Ok"
_TRACE: "74DB74","Auth","","Read","Products","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle","SELECT Count(*) FROM Products"
_TRACE: "74DB74","Event","DisposingDataReader",""
_TRACE: "74DB74","Event","ClosingDataReader",""
_TRACE: "74DB74","Event","DisposingCommand",""
_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","State","Closed"
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Disposing"

_TRACE: "northwind.db","Listening","F96FC5"
_TRACE: "F96FC5","Event","Opening",""
_TRACE: "F96FC5","Event","Closing",""
_TRACE: "F96FC5","Event","Closed",""
_TRACE: "F96FC5","Event","ConnectionString","version=3;data source=northwind.db"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "C:\SQLiteWrapper\Samples\Tracing\bin\Debug\northwind.db"
_TRACE: "F96FC5","Event","NewCommand",""
_TRACE: "F96FC5","Event","DisposingCommand",""
_TRACE: "F96FC5","State","Open"
_TRACE: "F96FC5","Event","Opened","data source=northwind.db;version=3"
_TRACE: "F96FC5","Event","NewCommand",""
_TRACE: "F96FC5","Event","NewDataReader",""
_TRACE: "F96FC5","Auth","","Select","","","Ok"
_TRACE: "F96FC5","Auth","","Function","","count","Ok"
_TRACE: "F96FC5","Auth","","Read","Suppliers","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle","SELECT Count(*) FROM Suppliers"
_TRACE: "F96FC5","Event","DisposingDataReader",""
_TRACE: "F96FC5","Event","ClosingDataReader",""
_TRACE: "F96FC5","Event","DisposingCommand",""
_TRACE: "F96FC5","Event","Closing",""
_TRACE: "F96FC5","State","Closed"
_TRACE: "F96FC5","Event","Closed",""
_TRACE: "F96FC5","Disposing"

** Total Products : 77
** Total Suppliers: 30

代码看起来很简单,但是创建、执行和销毁了 **两个** SQLiteConnection 对象和 **两个** SQLiteCommand 命令。

我们可以通过在 **单个** SQLiteConnection 上调用 RecordCount 扩展方法来优化代码,如下所示:

using (var dbCtx = new DbContext(connectionString, trace))
{
    using (var conn = dbCtx.ConnectionFactory())
    {
        var productCount = conn.RecordCount("Products");
        var supplierCount = conn.RecordCount("Suppliers");

        Console.WriteLine($"\n** Total Products : {productCount}");
        Console.WriteLine($"** Total Suppliers: {supplierCount}\n");
    }
}
Dim connectionString = "data source=northwind.db;version=3"

' Handles output from trace
Dim writer = New SimpleConsoleTextWriter()

' Set what to trace and where to output the results
Dim trace = New SqliteCsvEventTrace(writer, SqliteConnectionTraceType.Full)

Using dbCtx = New DbContext(connectionString, trace)
    Using conn = dbCtx.ConnectionFactory()
        Dim productCount = conn.RecordCount("Products")
        Dim supplierCount = conn.RecordCount("Suppliers")

        Console.WriteLine()
        Console.WriteLine($"** Total Products : {productCount}")
        Console.WriteLine($"** Total Suppliers: {supplierCount}")
        Console.WriteLine()
    End Using
End Using

我们调用 DbContextConnectionFactory 具体方法来监听 SQLite 事件并返回一个 SQLiteConnection 对象。

现在输出是:

_TRACE: "northwind.db","Listening","74DB74"
_TRACE: "74DB74","Event","Opening",""
_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Event","ConnectionString","data source=northwind.db;version=3"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "C:\SQLiteWrapper\Samples\Tracing\bin\Debug\northwind.db"
_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","DisposingCommand",""
_TRACE: "74DB74","State","Open"
_TRACE: "74DB74","Event","Opened","data source=northwind.db;version=3"

_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","NewDataReader",""
_TRACE: "74DB74","Auth","","Select","","","Ok"
_TRACE: "74DB74","Auth","","Function","","count","Ok"
_TRACE: "74DB74","Auth","","Read","Products","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle","SELECT Count(*) FROM Products"
_TRACE: "74DB74","Event","DisposingDataReader",""
_TRACE: "74DB74","Event","ClosingDataReader",""
_TRACE: "74DB74","Event","DisposingCommand",""

_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","NewDataReader",""
_TRACE: "74DB74","Auth","","Select","","","Ok"
_TRACE: "74DB74","Auth","","Function","","count","Ok"
_TRACE: "74DB74","Auth","","Read","Suppliers","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle","SELECT Count(*) FROM Suppliers"
_TRACE: "74DB74","Event","DisposingDataReader",""
_TRACE: "74DB74","Event","ClosingDataReader",""
_TRACE: "74DB74","Event","DisposingCommand",""

** Total Products : 77
** Total Suppliers: 30

_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","State","Closed"
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Disposing"

现在只打开了一个连接,并使用 **单个** SQLiteConnection 执行了 **两个** SQLiteCommand 命令。

我们可以通过将两个 SQL 查询传递给 **单个** SQLiteCommand 来进一步优化:

var query = @"SELECT COUNT (*) FROM Products;
              SELECT COUNT (*) FROM Suppliers;";

using (var dbCtx = new DbContext(connectionString, trace))
using (var conn = dbCtx.ConnectionFactory())
using (var cmd = new SQLiteCommand(query, conn))
{
    conn.Open();
    using (var coreReader = cmd.ExecuteReader())
    {
        coreReader.Read();
        var result = coreReader.HasRows ? coreReader.GetInt64(0) : 0;
        Console.WriteLine($"\n** Total Products: {result}\n");

        coreReader.NextResult();

        coreReader.Read();
        result = coreReader.HasRows ? coreReader.GetInt64(0) : 0;
        Console.WriteLine($"** Total Suppliers: {result}\n");
    }
}
Dim query = "SELECT COUNT (*) FROM Products;
             SELECT COUNT (*) FROM Suppliers;"

Using dbCtx = New DbContext(connectionString, trace)
    Using conn = dbCtx.ConnectionFactory()
        Using cmd = New SQLiteCommand(query, conn)
            conn.Open()

            Using coreReader = cmd.ExecuteReader()
                coreReader.Read()
                Dim result = If(coreReader.HasRows, coreReader.GetInt64(0), 0)
                Console.WriteLine()
                Console.WriteLine($"** Total Products: {result}")
                Console.WriteLine()

                coreReader.NextResult()

                coreReader.Read()
                result = If(coreReader.HasRows, coreReader.GetInt64(0), 0)
                Console.WriteLine()
                Console.WriteLine($"** Total Suppliers: {result}")
                Console.WriteLine()
            End Using
        End Using
    End Using
End Using

现在输出是:

_TRACE: "northwind.db","Listening","74DB74"
_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","Opening",""
_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Event","ConnectionString","data source=northwind.db;version=3"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "C:\SQLiteWrapper\Samples\Tracing\bin\Debug\northwind.db"
_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","DisposingCommand",""
_TRACE: "74DB74","State","Open"
_TRACE: "74DB74","Event","Opened","data source=northwind.db;version=3"
_TRACE: "74DB74","Event","NewDataReader",""
_TRACE: "74DB74","Auth","","Select","","","Ok"
_TRACE: "74DB74","Auth","","Function","","count","Ok"
_TRACE: "74DB74","Auth","","Read","Products","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle","SELECT COUNT (*) FROM Products;
                              SELECT COUNT (*) FROM Suppliers;"

** Total Products: 77

_TRACE: "74DB74","Auth","","Select","","","Ok"
_TRACE: "74DB74","Auth","","Function","","count","Ok"
_TRACE: "74DB74","Auth","","Read","Suppliers","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle","SELECT COUNT (*) FROM Suppliers;"

** Total Suppliers: 30

_TRACE: "74DB74","Event","DisposingDataReader",""
_TRACE: "74DB74","Event","ClosingDataReader",""
_TRACE: "74DB74","Event","DisposingCommand",""
_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","State","Closed"
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Disposing"

虽然最后一个示例的代码有点冗长,但结果如跟踪输出所示,只执行了一个 SQLiteCommand,使用的是 **单个** SQLiteConnection。它还演示了 DotNet.Core.SQLite 库在需要时如何与标准的 SQLite 代码无缝协同工作。

在上面的示例中,启用了完全跟踪。您可以通过在跟踪构造函数中设置选项标志来选择所需的跟踪级别:

[Flags]
public enum SqliteConnectionTraceType
{
    None = 0,
    Authorize = 1,
    Commit = 2,
    Progress = 4,
    RollBack = 8,
    StateChange = 16,
    Trace = 32,
    Update = 64,
    Events = 128,
    Compact = Commit | RollBack | StateChange | Update,
    Detailed = Compact | Events,
    Full = Detailed | Authorize | Progress
}
<Flags>
Public Enum SqliteConnectionTraceType
    None = 0
    Authorize = 1
    Commit = 2
    Progress = 4
    RollBack = 8
    StateChange = 16
    Trace = 32
    Update = 64
    Events = 128
    Compact = Commit Or RollBack Or StateChange Or Update
    Detailed = Compact Or Events
    Full = Detailed Or Authorize Or Progress
End Enum

这是一个非常简单的示例,说明了使用该库跟踪事件有多么容易,但在构建和调试应用程序或查看最终用户计算机上发生的情况时,它非常有益。

表、SQL 数据查询、视图和 POCOs

通常,在处理表/视图时,您会编写类似如下的代码:

List<Category> Categories = new List<Category>();

var connectionString = "data source=northwind.db;version=3";

using (var conn = new SQLiteConnection(connectionString))

// Get all categories
using (var cmd = new SQLiteCommand("SELECT * FROM Categories;", conn))
{
    conn.Open();
    using (var coreReader = cmd.ExecuteReader())
    {
        while (coreReader.Read())
        {
            var category = new Category
            {
                Id = (long)coreReader["CategoryID"],
                Name = (string)coreReader["CategoryName"],
                Description = (string)coreReader["Description"],
                Picture = (byte[])coreReader["Picture"]
            };
            Categories.Add(category);
        }
    }
}
Dim Categories As List(Of Category) = New List(Of Category)()

Dim connectionString = "data source=northwind.db;version=3"

Using conn = New SQLiteConnection(connectionString)

    ' Get all categories
    Using cmd = New SQLiteCommand("SELECT * FROM Categories;", conn)
        conn.Open()

        Using coreReader = cmd.ExecuteReader()
            While coreReader.Read()
                Dim category = New Category With {
                    .Id = CLng(coreReader("CategoryID")),
                    .Name = CStr(coreReader("CategoryName")),
                    .Description = CStr(coreReader("Description")),
                    .Picture = CType(coreReader("Picture"), Byte())
                }
                Categories.Add(category)
            End While
        End Using
    End Using
End Using

这是 Category 类:

public class Category
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public byte[] Picture { get; set; }
}
Public Class Category
    Public Property Id As Long
    Public Property Name As String
    Public Property Description As String
    Public Property Picture As Byte()
End Class

DotNet.Core.SQLite 库提供了一组用于封装对表和视图操作的基类,以简化表/视图的创建以及 CRUD 操作。DotNet.Core.SQLite 库不使用反射 (报告除外),以保持性能紧凑。例如:

public class CategoriesContext : DbTableContext<Category>
{
    public const string Name = "Categories";
    protected override string Identifier => Name;

    private const string AddSqlFieldList = Name +
        " (CategoryName, Description, Picture)" +
        " VALUES (@CategoryName, @Description, @Picture);";

    private const string UpdateSqlFieldList = Name + 
        " (CategoryID, CategoryName, Description, Picture)" +
        " VALUES (@CategoryID, @CategoryName, @Description, @Picture);";

    protected override void CheckAndCreateTable(SQLiteConnection conn)
    {
        conn.CheckAndCreateTable(Name,
            $@"CREATE TABLE [{Name}] (
                [CategoryID] integer PRIMARY KEY AUTOINCREMENT NOT NULL,
                [CategoryName] nvarchar(15) NOT NULL COLLATE NOCASE,
                [Description] nvarchar COLLATE NOCASE,
                [Picture] varbinary);");
    }

    protected override void CheckAndCreateIndicies(SQLiteConnection conn)
    {
        var ix = $"IX_{Name}_CategoryName";
        conn.CheckAndCreateIndex(ix, $"CREATE INDEX [{ix}] ON [{Name}] ([CategoryName]);");
    }

    protected override void CheckAndCreateTriggers(SQLiteConnection conn)
    {
        // not required
    }

    protected override Category FromSQLiteDataReader(SQLiteDataReader coreReader)
        => new Category
        {
            Id = coreReader.GetLong("CategoryID", 0L),
            Name = coreReader.GetString("CategoryName"),
            Description = coreReader.GetString("Description"),
            Picture = coreReader.GetBinary("Picture")
        };

    protected override SqlQuery ToSqlQuery(Category data, ToSqlQueryType type)
        => (type == ToSqlQueryType.Update)
        ? new SqlQuery(GetMethod(type) + UpdateSqlFieldList, new Dictionary<string, object>
        {
            ["@CategoryID"] = data.Id,
            ["@CategoryName"] = data.Name.ToSqLiteText(),
            ["@Description"] = data.Description.ToSqLiteText(),
            ["@Picture"] = data.Picture
        })
        : new SqlQuery(GetMethod(type) + AddSqlFieldList, new Dictionary<string, object>
        {
            ["@CategoryName"] = data.Name.ToSqLiteText(),
            ["@Description"] = data.Description.ToSqLiteText(),
            ["@Picture"] = data.Picture
        });

    protected override SqlQuery ToDeleteSqlQuery(Category data)
        => new SqlQuery(SqlDeleteAllPrefix + Name + " WHERE CategoryID = @CategoryID",
                        new Dictionary<string, object> { ["@CategoryID"] = data.Id });
}
Friend Class CategoriesContext : Inherits DbTableContext(Of Category)

    Public Const Name As String = "Categories"

    Protected Overrides ReadOnly Property Identifier As String
        Get
            Return Name
        End Get
    End Property

    Private Const AddSqlFieldList As String = Name & 
        " (CategoryName, Description, Picture)" &
        " VALUES (@CategoryName, @Description, @Picture);"

    Private Const UpdateSqlFieldList As String = Name &
        " (CategoryID, CategoryName, Description, Picture)" &
        " VALUES (@CategoryID, @CategoryName, @Description, @Picture);"

    Protected Overrides Sub CheckAndCreateTable(ByVal conn As SQLiteConnection)

        conn.CheckAndCreateTable(Name, $"CREATE TABLE [{Name}] (
                    [CategoryID] integer PRIMARY KEY AUTOINCREMENT NOT NULL,
                    [CategoryName] nvarchar(15) NOT NULL COLLATE NOCASE,
                    [Description] nvarchar COLLATE NOCASE,
                    [Picture] varbinary);")

    End Sub

    Protected Overrides Sub CheckAndCreateIndicies(ByVal conn As SQLiteConnection)
        Dim ix = $"IX_{Name}_CategoryName"
        conn.CheckAndCreateIndex(ix, $"CREATE INDEX [{ix}] ON [{Name}] ([CategoryName]);")

    End Sub

    Protected Overrides Sub CheckAndCreateTriggers(ByVal conn As SQLiteConnection)
        ' Not required
    End Sub

    Protected Overrides Function FromSQLiteDataReader(ByVal coreReader As SQLiteDataReader)
        As Category

        Return New Category With {
            .Id = coreReader.GetLong("CategoryID", 0L),
            .Name = coreReader.GetString("CategoryName"),
            .Description = coreReader.GetString("Description"),
            .Picture = coreReader.GetBinary("Picture")
        }

    End Function

    Protected Overrides Function ToSqlQuery(ByVal data As Category,
                                            ByVal type As ToSqlQueryType) As SqlQuery

        Return If((type = ToSqlQueryType.Update), 
            New SqlQuery(GetMethod(type) & UpdateSqlFieldList,
                New Dictionary(Of String, Object) From {
                    {"@CategoryID", data.Id},
                    {"@CategoryName", data.Name.ToSqLiteText()},
                    {"@Description", data.Description.ToSqLiteText()},
                    {"@Picture", data.Picture}
            }),
            New SqlQuery(GetMethod(type) & AddSqlFieldList,
                New Dictionary(Of String, Object) From {
                    {"@CategoryName", data.Name.ToSqLiteText()},
                    {"@Description", data.Description.ToSqLiteText()},
                    {"@Picture", data.Picture}
            }))

    End Function

    Protected Overrides Function ToDeleteSqlQuery(ByVal data As Category) As SqlQuery
        Return New SqlQuery(SqlDeleteAllPrefix & Name & " WHERE CategoryID = @CategoryID",
            New Dictionary(Of String, Object) From {{"@CategoryID", data.Id}})

    End Function

End Class

首先,在代码的某个地方,注册数据库和 CategoriesContext 类。

// Using a library registery service to link tables/views to a database with lookup
var dbRegistry = new DbRegistry();

// Create the database context
var dbCtx = new DbContext("data source=northwind.db;version=3");

// Register the database
var dbKey = dbRegistry.Register(dbCtx);

// Register the Table/View Context
dbRegistry.Register(new CategoriesContext(), dbKey);
' Using a library registery service to link tables/views to a database with lookup
Dim dbRegistry = New DbRegistry()

' Create the database context
Dim dbCtx = New DbContext("data source=northwind.db;version=3")

' Register the database
Dim dbKey = dbRegistry.Register(dbCtx)

' Register the Table/View Context
dbRegistry.Register(New CategoriesContext(), dbKey)

现在我们可以用一行代码获取数据:

// Get all categories
var categories = dbRegistry.Resolve<Category>().Get();
' Get all categories 
Dim categories = dbRegistry.Resolve(Of Category)().Get()

如果您想获取部分数据,可以这样做:

// Get the data
var query = "SELECT * FROM Categories WHERE CategoryName Like 'c%'";
var categories = dbRegistry.Resolve<Category>().Get(query);
' Get the data 
Dim query = "SELECT * FROM Categories WHERE CategoryName Like 'c%'";
Dim categories = dbRegistry.Resolve(Of Category)().Get(query)

如果您不想使用 DbRegistry 服务,可以直接将 SQLiteConnection 对象传递给 DbTableContext<T>Get 方法:

using (var conn = dbCtx.ConnectionFactory())
{
    var categoriesCtx = new CategoriesContext();
    var categories = categoriesCtx.Get(conn);
}
Using conn = dbCtx.ConnectionFactory()
    Dim categoriesCtx = New CategoriesContext()
    Dim categories = categoriesCtx.Get(conn)
End Using

CategoriesContext 将自动处理所有数据库操作。CategoriesContext 继承自 DbTableContext<T> 基类。对于视图,您将使用 DbViewContext<T> 基类。这两个 DbXxxxContext<T> 类之间的区别在于 DbViewContext<T> 是只读的。

DbTableContext<T>DbViewContext<T> 都继承自基类 DbResourceContext<T>,该基类支持以下操作:

命令 表格 视图 返回类型
CheckOrCreateTable() Y Y -
Count() Y Y long
Count(string query) Y Y long
Count(SqlQuery query) Y Y long
Count(SQLiteConnection conn) Y Y long
Count(SQLiteConnection conn, string query) Y Y long
Count(SQLiteConnection conn, SqlQuery query) Y Y long
Count(IUnitOfWork unitOfWork) Y Y long
Count(IUnitOfWork unitOfWork, string query) Y Y long
Count(IUnitOfWork unitOfWork, SqlQuery query) Y Y long
GetResourceCounts() - - Dictionary<string, int>
Get() Y Y IEnumerable<T>
Get(string query) Y Y IEnumerable<T>
Get(SqlQuery query) Y Y IEnumerable<T>
Get(SQLiteConnection conn) Y Y IEnumerable<T>
Get(SQLiteConnection conn, string query) Y Y IEnumerable<T>
Get(SQLiteConnection conn, SqlQuery query) Y Y IEnumerable<T>
Get(IUnitOfWork unitOfWork) Y Y IEnumerable<T>
Get(IUnitOfWork unitOfWork, string query) Y Y IEnumerable<T>
Get(IUnitOfWork unitOfWork, SqlQuery query) Y Y IEnumerable<T>
GetAsync() Y Y Task<IEnumerable<T>>
GetAsync(string query) Y Y Task<IEnumerable<T>>
GetAsync(SqlQuery query) Y Y Task<IEnumerable<T>>
GetAsync(SQLiteConnection conn) Y Y Task<IEnumerable<T>>
GetAsync(SQLiteConnection conn, string query) Y Y Task<IEnumerable<T>>
GetAsync(SQLiteConnection conn, SqlQuery query) Y Y Task<IEnumerable<T>>
GetAsync(IUnitOfWork unitOfWork) Y Y Task<IEnumerable<T>>
GetAsync(IUnitOfWork unitOfWork, string query) Y Y Task<IEnumerable<T>>
GetAsync(IUnitOfWork unitOfWork, SqlQuery query) Y Y Task<IEnumerable<T>>
AddAsync(DbQuery<t> dbQuery) Y - Task<int>
AddAsync(SQLiteConnection conn, DbQuery<t> dbQuery) Y - Task<int>
AddAsync(IUnitOfWork unitOfWork, DbQuery<t> dbQuery) Y - Task<int>
UpdateAsync(DbQuery<t> dbQuery) Y - Task<int>
UpdateAsync(SQLiteConnection conn, DbQuery<t> dbQuery) Y - Task<int>
UpdateAsync(IUnitOfWork unitOfWork, DbQuery<t> dbQuery) Y - Task<int>
DeleteAsync(DbQuery<t> dbQuery) Y - Task<int>
DeleteAsync(SQLiteConnection conn, DbQuery<t> dbQuery) Y - Task<int>
DeleteAsync(IUnitOfWork unitOfWork, DbQuery<t> dbQuery) Y - Task<int>
KillAsync() Y - Task<int>

事务

SQLite 中进行批量操作时,处理事务可能需要编写如下代码:

var builder = new SQLiteConnectionStringBuilder
{
    Version = 3,
    DataSource = "northwind.db",
    JournalMode = SQLiteJournalModeEnum.Wal
};

const string insertSql = "INSERT INTO Categories (CategoryName, Description, Picture)"
    + " VALUES (@CategoryName, @Description, @Picture);";

const string deleteSql = "DELETE FROM Categories WHERE CategoryID = @CategoryID;";

using (var conn = new SQLiteConnection(builder.ConnectionString))
{
    await conn.OpenAsync().ConfigureAwait(false);
    using (var trans = conn.BeginTransaction())
    {
        try
        {
            using (var addCmd = new SQLiteCommand(insertSql, conn))
            using (var delCmd = new SQLiteCommand(deleteSql, conn))
            {
                for (int i = 0; i < 50000; i++)
                {
                    addCmd.Parameters.AddWithValue
                                      ("@CategoryName", "test category " + i);
                    addCmd.Parameters.AddWithValue
                                      ("@Description", "this is a test category");
                    addCmd.Parameters.AddWithValue("@Picture", new byte[0]);
                    await addCmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                    delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId);
                    await delCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                }
            }

            trans.Commit();
        }
        catch (Exception ex)
        {
            // failed, so log here and the transaction
            // will auto rollback (see UnitOfWork class)
            // write detailed exception to trace output
            Console.WriteLine("!! ERROR: " + ex.Message + " ... "
                + ex.InnerException.Message.Replace("\r\n", " >> "));
            trans.Rollback();
        }
    }
}
Dim builder = New SQLiteConnectionStringBuilder With {
        .Version = 3,
        .DataSource = "northwind.db",
        .JournalMode = SQLiteJournalModeEnum.Wal
    }

Const insertSql As String = "INSERT INTO Categories (CategoryName, Description, Picture)" &
                            " VALUES (@CategoryName, @Description, @Picture);"

Const deleteSql As String = "DELETE FROM Categories WHERE CategoryID = @CategoryID;"

Using conn = New SQLiteConnection(builder.ConnectionString)
    Await conn.OpenAsync().ConfigureAwait(False)

    Using trans = conn.BeginTransaction()
        Try
            Using addCmd = New SQLiteCommand(insertSql, conn)
                Using delCmd = New SQLiteCommand(deleteSql, conn)

                    For i As Integer = 0 To 50000 - 1
                        addCmd.Parameters.AddWithValue
                               ("@CategoryName", "test category " & i)
                        addCmd.Parameters.AddWithValue
                               ("@Description", "this is a test category")
                        addCmd.Parameters.AddWithValue("@Picture", New Byte(-1) {})
                        Await addCmd.ExecuteNonQueryAsync().ConfigureAwait(False)

                        delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId)
                        Await delCmd.ExecuteNonQueryAsync().ConfigureAwait(False)
                    Next
                End Using
            End Using

            trans.Commit()

        Catch ex As Exception
            ' failed, so log here and the transaction
            ' will auto rollback (see UnitOfWork class)
            ' write detailed exception to trace output
            Console.WriteLine("!! ERROR: " & ex.Message & " ... " &
                ex.InnerException.Message.Replace(vbCrLf, " >> "))
            trans.Rollback()
        End Try

    End Using
End Using

包含的 UnitOfWork 类将简化事务的处理。UnitOfWork 类封装了一个通用的共享连接,自动开始事务,要求调用 SaveChanges 方法来提交更改,并在提交失败时自动回滚。

SQLiteConnection 对象传递给构造函数时,如果连接未打开,则会打开连接并开始事务。当 UnitOfWork 类被释放时,如果需要,会自动调用回滚。

使用 UnitOfWork 类非常简单。此外,UnitOfWork 类不特定于 DotNet.Core.SQLite 库,因此也可以与您现有的 SQLite 代码一起使用,只需添加两行代码 - 一行用于初始化 UnitOfWork 类,另一行用于 SaveChanges 方法进行提交。请参见下文:

using (var conn = new SQLiteConnection(builder.ConnectionString))
{
    await conn.OpenAsync().ConfigureAwait(false);
    using (var uow = new UnitOfWork(conn))
    {
        try
        {
            using (var addCmd = new SQLiteCommand(insertSql, conn))
            using (var delCmd = new SQLiteCommand(deleteSql, conn))
            {
                for (int i = 0; i < 50000; i++)
                {
                    addCmd.Parameters.AddWithValue
                                      ("@CategoryName", "test category " + i);
                    addCmd.Parameters.AddWithValue
                                      ("@Description", "this is a test category");
                    addCmd.Parameters.AddWithValue("@Picture", new byte[0]);
                    await addCmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                    delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId);
                    await delCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                }
            }

            uow.SaveChanges();
        }
        catch (Exception ex)
        {
            // failed, so log here and the transaction
            // will auto rollback (see UnitOfWork class)
            // write detailed exception to trace output
            Console.WriteLine("!! ERROR: " + ex.Message + " ... "
                + ex.InnerException.Message.Replace("\r\n", " >> "));
        }
    }
}
Dim builder = New SQLiteConnectionStringBuilder With {
        .Version = 3,
        .DataSource = "northwind.db",
        .JournalMode = SQLiteJournalModeEnum.Wal
    }

Const insertSql As String = "INSERT INTO Categories (CategoryName, Description, Picture)" &
                            " VALUES (@CategoryName, @Description, @Picture);"

Const deleteSql As String = "DELETE FROM Categories WHERE CategoryID = @CategoryID;"

Using conn = New SQLiteConnection(builder.ConnectionString)
    Await conn.OpenAsync().ConfigureAwait(False)

    Using uow = New UnitOfWork(conn)
        Try
            Using addCmd = New SQLiteCommand(insertSql, conn)
                Using delCmd = New SQLiteCommand(deleteSql, conn)

                    For i As Integer = 0 To 50000 - 1
                        addCmd.Parameters.AddWithValue
                               ("@CategoryName", "test category " & i)
                        addCmd.Parameters.AddWithValue
                               ("@Description", "this is a test category")
                        addCmd.Parameters.AddWithValue("@Picture", New Byte(-1) {})
                        Await addCmd.ExecuteNonQueryAsync().ConfigureAwait(False)

                        delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId)
                        Await delCmd.ExecuteNonQueryAsync().ConfigureAwait(False)
                    Next
                End Using
            End Using

            uow.SaveChanges()

        Catch ex As Exception
            ' failed, so log here and the transaction
            ' will auto rollback (see UnitOfWork class)
            ' write detailed exception to trace output
            Console.WriteLine("!! ERROR: " & ex.Message & " ... " &
                ex.InnerException.Message.Replace(vbCrLf, " >> "))
        End Try

    End Using
End Using

这是一个使用上面 dbRegistry 代码的示例,它将在跟踪下批量添加和删除表中的条目:

var categories = dbRegistry.Resolve<Category>();

using (var uow = categories.UnitOfWorkFactory())
{
    try
    {
        for (int i = 0; i < 50000; i++)
        {
            var newCat = new Category
            {
                CategoryName = "test category " + i,
                Description = "this is a test category",
                Picture = new byte[0]
            };

            await categories.AddAsync(uow, new DbQuery<Category>(newCat));
            newCat.CategoryID = uow.Connection.LastInsertRowId;

            await categories.DeleteAsync(uow, new DbQuery<Category>(newCat));
        }

        uow.SaveChanges();
    }
    catch (Exception ex)
    {
            // failed, so log here and the transaction
            // will auto rollback (see UnitOfWork class)
            // write detailed exception to trace output
        writer.WriteLine("!! ERROR: " + ex.Message
            + (ex.InnerException?.Message?.Replace("\r\n", " >> "));
    }
}
Dim categories = dbRegistry.Resolve(Of Category)()

Using uow = categories.UnitOfWorkFactory()
    Try
        For i As Integer = 0 To 50000 - 1
            Dim newCat = New Category With {
                    .Name = "test category " & i,
                    .Description = "this is a test category",
                    .Picture = New Byte(-1) {}
                }

            Await categories.AddAsync(uow,
                New DbQuery(Of Category)(newCat)).ConfigureAwait(False)
            newCat.Id = uow.Connection.LastInsertRowId

            Await categories.DeleteAsync(uow,
                New DbQuery(Of Category)(newCat)).ConfigureAwait(False)
        Next

        uow.SaveChanges()

    Catch ex As Exception
        ' failed, so log here and the transaction
        ' will auto rollback (see UnitOfWork class)
        ' write detailed exception to trace output
        Console.WriteLine("!! ERROR: " & ex.Message & " ... " &
            ex.InnerException.Message.Replace(vbCrLf, " >> "))
    End Try
End Using

要查看自动回滚,我们可以强制在添加记录时发生错误:

var categories = dbRegistry.Resolve<Category>();

using (var uow = categories.UnitOfWorkFactory())
{
    try
    {
        for (int i = 0; i < 50000; i++)
        {
            var newCat = new Category
            {
                // force an error by leaving the property CategoryName empty (null)
                Description = "this is a test category",
                Picture = new byte[0]
            };

            await categories.AddAsync(uow, new DbQuery<Category>(newCat));
            newCat.CategoryID = uow.Connection.LastInsertRowId;
        }

        // Never reached as the add record failed...
        uow.SaveChanges();
    }
    catch (Exception ex)
    {
            // failed, so log here and the transaction
            // will auto rollback (see UnitOfWork class)
            // write detailed exception to trace output
        writer.WriteLine("!! ERROR: " + ex.Message
            + (ex.InnerException?.Message?.Replace("\r\n", " >> "));
    }
}
Dim categories = dbRegistry.Resolve(Of Category)()

Using uow = categories.UnitOfWorkFactory()
    Try
        For i As Integer = 0 To 50000 - 1
            ' force an error by leaving the property CategoryName empty (null)
            Dim newCat = New Category With {
                    .Description = "this is a test category",
                    .Picture = New Byte(-1) {}
                }

            Await categories.AddAsync(uow,
                New DbQuery(Of Category)(newCat)).ConfigureAwait(False)
            newCat.Id = uow.Connection.LastInsertRowId

            Await categories.DeleteAsync(uow,
                New DbQuery(Of Category)(newCat)).ConfigureAwait(False)
        Next

        uow.SaveChanges()

    Catch ex As Exception
        ' failed, so log here and the transaction
        ' will auto rollback (see UnitOfWork class)
        ' write detailed exception to trace output
        Console.WriteLine("!! ERROR: " & ex.Message & " ... " &
            ex.InnerException.Message.Replace(vbCrLf, " >> "))
    End Try
End Using

启用跟踪后,我们可以在跟踪日志中看到自动回滚:

_TRACE: "northwind.db","Listening","2EC5D9C"
_TRACE: "2EC5D9C","Event","Opening",""
_TRACE: "2EC5D9C","Event","Closing",""
_TRACE: "2EC5D9C","Event","Closed",""
_TRACE: "2EC5D9C","Event","ConnectionString",
        "version=3;data source=northwind.db;journal mode=Wal"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "C:\SQLiteWrapper\Samples\Tracing\bin\Debug\northwind.db"
_TRACE: "2EC5D9C","Event","NewCommand",""
_TRACE: "2EC5D9C","Event","NewDataReader",""
_TRACE: "northwind.db","Event","NewCriticalHandle","PRAGMA journal_mode=Wal"
_TRACE: "2EC5D9C","Event","DisposingDataReader",""
_TRACE: "2EC5D9C","Event","ClosingDataReader",""
_TRACE: "2EC5D9C","Event","DisposingCommand",""
_TRACE: "2EC5D9C","State","Open"
_TRACE: "2EC5D9C","Event","Opened","version=3;data source=northwind.db;journal mode=Wal"
_TRACE: "2EC5D9C","Event","NewCommand",""
_TRACE: "2EC5D9C","Event","NewDataReader",""

_TRACE: "2EC5D9C","Auth","","Transaction","BEGIN","","Ok"

_TRACE: "northwind.db","Event","NewCriticalHandle","BEGIN IMMEDIATE;"
_TRACE: "2EC5D9C","Event","DisposingDataReader",""
_TRACE: "2EC5D9C","Event","ClosingDataReader",""
_TRACE: "2EC5D9C","Event","DisposingCommand",""
_TRACE: "2EC5D9C","Event","NewTransaction",""

_TRACE: "2EC5D9C","Event","NewCommand",""
_TRACE: "2EC5D9C","Event","NewDataReader",""
_TRACE: "2EC5D9C","Auth","","Insert","Categories","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "INSERT INTO Categories (CategoryName, Description, Picture) 
         VALUES (@CategoryName, @Description, @Picture);"
_TRACE: "2EC5D9C","Event","DisposingCommand",""

_TRACE: !! ERROR: One or more errors occurred. 
           constraint failed >> NOT NULL constraint failed: Categories.CategoryName

_TRACE: "2EC5D9C","Event","NewCommand",""
_TRACE: "2EC5D9C","Event","NewDataReader",""

_TRACE: "2EC5D9C","Auth","","Transaction","ROLLBACK","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle","ROLLBACK;"
_TRACE: "2EC5D9C","RollBack"

_TRACE: "2EC5D9C","Event","DisposingDataReader",""
_TRACE: "2EC5D9C","Event","ClosingDataReader",""
_TRACE: "2EC5D9C","Event","DisposingCommand",""

其他数据库函数

DbContext 扩展方法

DbContext 类仅包含跟踪数据库连接所需的核心代码。所有可执行的操作都以扩展方法的形式编码。

当前操作列表包括:

命令 返回类型
AttatchDB -
CheckAndCreateIndex -
CheckAndCreateTable -
CheckAndCreateTrigger -
CheckAndCreateView -
CopyTable int
CreateDB -
CompactDB -
DetatchDB -
DropTable -
ExecuteNonQuery int
ExecuteNonQueryAsync Task<int>
ExecuteScalar -
ExecuteScalarAsync 任务
Exists bool
GenerateDDL 字符串
Get IEnumerable<T>
GetAsync Task<IEnumerable<T>>
GetConnectionParameters IEnumerable<KeyValuePair<string, string>>
GetPragma KeyValuePair<string, string>
GetResourceCounts IDictionary<string, long>
HasOpenConnections bool
IndexExists bool
IndicesExist IDictionary<string, bool>
LiveBackup bool
ManualReindex -
ManualReindexAll -
MoveTable int
RecordCount long
Reindex -
RenameTable -
报表 字符串
SetPragma KeyValuePair<string, string>
TableExists bool
TablesExist IDictionary<string, bool>
TriggerExists bool
TriggersExist IDictionary<string, bool>
ViewExists bool
ViewsExist IDictionary<string, bool>

SQLiteConnection 扩展方法

标准 SQLite SQLiteConnection 类也提供了一些扩展方法:

命令 返回类型
CheckAndCreateIndex -
CheckAndCreateTable -
CheckAndCreateTrigger -
CheckAndCreateView -
CommandFactory SQLiteCommand
ExecuteNonQuery int
ExecuteNonQueryAsync Task<int>
ExecuteScalar -
ExecuteScalarAsync 任务
GenerateDDL 字符串
Get IEnumerable<T>
GetAsync Task<IEnumerable<T>>
GetPragma KeyValuePair<string, string>
GetPragma KeyValuePair<string, string>
GetResourceCounts IDictionary<string, long>
IndexExists bool
IndicesExist IDictionary<string, bool>
IsOpen bool
OpenIfRequired -
OpenIfRequiredAsync 任务
RecordCount long
SetPragma KeyValuePair<string, string>
TableExists bool
TablesExist IDictionary<string, bool>
TriggerExists bool
TriggersExist IDictionary<string, bool>
ViewExists bool
ViewsExist IDictionary<string, bool>

备份和恢复数据库

SQLite 自带 BackupDatabase 方法。此方法将自动覆盖目标文件 (如果已存在)。为了简化使用,有一个针对 DbContext 的包装器扩展函数,名为 LiveBackup

备份数据库:

var connectionString = "data source=northwind.db;version=3";

using (var dbCtx = new DbContext(connectionString))
{
    var success = dbCtx.LiveBackup(
        System.IO.Path.Combine(dbCtx.DbPath,
                               "northwindEF.bak"));
}
Dim connectionString = "data source=northwind.db;version=3"

Using dbCtx = New DbContext(connectionString)
    Dim success = 
        dbCtx.LiveBackup(System.IO.Path.Combine(dbCtx.DbPath,
                         "northwindEF.bak"))
End Using

恢复:

var connectionString = "data source=northwind.bak;version=3";

using (var dbCtx = new DbContext(connectionString))
{
    var success = dbCtx.LiveBackup(
        System.IO.Path.Combine(dbCtx.DbPath,
                               "northwindEF.db"));
}
Dim connectionString = "data source=northwind.bak;version=3"

Using dbCtx = New DbContext(connectionString)
    Dim success = 
        dbCtx.LiveBackup(System.IO.Path.Combine(dbCtx.DbPath,
                         "northwindEF.db"))
End Using

如果在备份过程中需要进度反馈,可以添加回调函数,并设置 pageSizeretryMilliseconds 超时。

var connectionString = "data source=northwind.db;version=3";

using (var dbCtx = new DbContext(connectionString))
{
    var success = dbCtx.LiveBackup(
        System.IO.Path.Combine(dbCtx.DbPath, "northwindEF.bak"), 
        BackupProgress, 100);
}
Dim connectionString = "data source=northwind.db;version=3"

Using dbCtx = New DbContext(connectionString)
    Dim success = dbCtx.LiveBackup(System.IO.Path.Combine(dbCtx.DbPath,
        "northwindEF.bak"), AddressOf BackupProgress, 100)
End Using

回调方法:

private static bool BackupProgress(SQLiteConnection source,
                                   string sourceName,
                                   SQLiteConnection destination,
                                   string destinationName,
                                   int pages,
                                   int remainingPages,
                                   int totalPages,
                                   bool retry)
{
    Console.WriteLine($"___ Progress : {100 * (totalPages - remainingPages) / totalPages}%");
    return true;
}
Private Function BackupProgress(ByVal source As SQLiteConnection,
                                ByVal sourceName As String,
                                ByVal destination As SQLiteConnection,
                                ByVal destinationName As String,
                                ByVal pages As Integer,
                                ByVal remainingPages As Integer,
                                ByVal totalPages As Integer,
                                ByVal retry As Boolean) As Boolean
    Console.WriteLine($"___ Progress : {100 * (totalPages - remainingPages) / totalPages}%")
    Return True
End Function

如果启用了完整的跟踪,如上面 Tracing 部分所述,输出将如下所示:

_TRACE: "northwind.db","Listening","F96FC5"
_TRACE: "F96FC5","Event","Opening",""
_TRACE: "F96FC5","Event","Closing",""
_TRACE: "F96FC5","Event","Closed",""
_TRACE: "F96FC5","Event","ConnectionString","version=3;data source=northwind.db"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "C:\SQLiteWrapper\Samples\Tracing\bin\Debug\northwind.db"
_TRACE: "F96FC5","Event","NewCommand",""
_TRACE: "F96FC5","Event","DisposingCommand",""
_TRACE: "F96FC5","State","Open"
_TRACE: "F96FC5","Event","Opened",""
_TRACE: "2EC5D9C","Event","Opening",""
_TRACE: "2EC5D9C","Event","Closing",""
_TRACE: "2EC5D9C","Event","Closed",""
_TRACE: "2EC5D9C","Event","ConnectionString","version=3;data source=northwindEF.bak"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "C:\SQLiteWrapper\Samples\Tracing\bin\Debug\northwindEF.bak"
_TRACE: "2EC5D9C","Event","NewCommand",""
_TRACE: "2EC5D9C","Event","DisposingCommand",""
_TRACE: "2EC5D9C","Event","Opened",""
_TRACE: "northwind.db","Event","NewCriticalHandle",""
___ Progress : 12%
___ Progress : 24%
___ Progress : 36%
___ Progress : 49%
___ Progress : 61%
___ Progress : 73%
___ Progress : 86%
___ Progress : 98%
_TRACE: "2EC5D9C","Event","Closing",""
_TRACE: "2EC5D9C","Event","Closed",""
_TRACE: "F96FC5","Event","Closing",""
_TRACE: "F96FC5","State","Closed"
_TRACE: "F96FC5","Event","Closed",""
_TRACE: "F96FC5","Disposing"

复制表

CopyTable 方法允许灵活地复制所有数据或特定子集,并自定义目标表以及触发器和索引等相关资源。

首先,创建一个用于资源创建的方法:

private static void CreateProducts(SQLiteConnection conn, string tableName)
{
    var query = $@"CREATE TABLE [{tableName}](
                    [ProductID] integer primary key autoincrement NOT NULL,
                    [ProductName] nvarchar(40) NOT NULL COLLATE NOCASE,
                    [SupplierID] integer NULL,
                    [CategoryID] integer NULL,
                    [QuantityPerUnit] nvarchar(20) NULL COLLATE NOCASE,
                    [UnitPrice] money NULL CONSTRAINT [DF_Products_UnitPrice]  DEFAULT (0),
                    [UnitsInStock] smallint NULL CONSTRAINT [DF_Products_UnitsInStock]  
                                   DEFAULT (0),
                    [UnitsOnOrder] smallint NULL CONSTRAINT [DF_Products_UnitsOnOrder]  
                                   DEFAULT (0),
                    [ReorderLevel] smallint NULL CONSTRAINT [DF_Products_ReorderLevel]  
                                   DEFAULT (0),
                    [Discontinued] bit NOT NULL CONSTRAINT [DF_Products_Discontinued]  
                                   DEFAULT (0),
                    [DiscontinuedDate] datetime NULL,
                    CONSTRAINT [CK_Products_UnitPrice] CHECK  ([UnitPrice] >= 0),
                    CONSTRAINT [CK_ReorderLevel] CHECK ([ReorderLevel] >= 0),
                    CONSTRAINT [CK_UnitsInStock] CHECK ([UnitsInStock] >= 0),
                    CONSTRAINT [CK_UnitsOnOrder] CHECK ([UnitsOnOrder] >= 0),
                    CONSTRAINT [FK_Products_Categories] FOREIGN KEY([CategoryID])
                    REFERENCES [Categories] ([CategoryID]),
                    CONSTRAINT [FK_Products_Suppliers] FOREIGN KEY([SupplierID])
                    REFERENCES [Suppliers] ([SupplierID])
                );
                CREATE INDEX [IX_{tableName}_CategoriesProducts] ON [{tableName}]
                (
                    [CategoryID] ASC
                );
                CREATE INDEX [IX_{tableName}_CategoryID] ON [{tableName}]
                (
                    [CategoryID] ASC
                );
                CREATE INDEX [IX_{tableName}_ProductName] ON [{tableName}]
                (
                    [ProductName] ASC
                );
                CREATE INDEX [IX_{tableName}_SupplierID] ON [{tableName}]
                (
                    [SupplierID] ASC
                );
                CREATE INDEX [IX_{tableName}_SuppliersProducts] ON [{tableName}]
                (
                    [SupplierID] ASC
                );";

    conn.ExecuteNonQuery(new SqlQuery(query));
}
Dim query = $"CREATE TABLE [{tableName}](
        [ProductID] integer primary key autoincrement NOT NULL,
        [ProductName] nvarchar(40) NOT NULL COLLATE NOCASE,
        [SupplierID] integer NULL,
        [CategoryID] integer NULL,
        [QuantityPerUnit] nvarchar(20) NULL COLLATE NOCASE,
        [UnitPrice] money NULL CONSTRAINT [DF_Products_UnitPrice]  DEFAULT (0),
        [UnitsInStock] smallint NULL CONSTRAINT [DF_Products_UnitsInStock]  DEFAULT (0),
        [UnitsOnOrder] smallint NULL CONSTRAINT [DF_Products_UnitsOnOrder]  DEFAULT (0),
        [ReorderLevel] smallint NULL CONSTRAINT [DF_Products_ReorderLevel]  DEFAULT (0),
        [Discontinued] bit NOT NULL CONSTRAINT [DF_Products_Discontinued]  DEFAULT (0),
        [DiscontinuedDate] datetime NULL,
        CONSTRAINT [CK_Products_UnitPrice] CHECK  ([UnitPrice] >= 0),
        CONSTRAINT [CK_ReorderLevel] CHECK ([ReorderLevel] >= 0),
        CONSTRAINT [CK_UnitsInStock] CHECK ([UnitsInStock] >= 0),
        CONSTRAINT [CK_UnitsOnOrder] CHECK ([UnitsOnOrder] >= 0),
        CONSTRAINT [FK_Products_Categories] FOREIGN KEY([CategoryID])
        REFERENCES [Categories] ([CategoryID]),
        CONSTRAINT [FK_Products_Suppliers] FOREIGN KEY([SupplierID])
        REFERENCES [Suppliers] ([SupplierID])
    );
    CREATE INDEX [IX_{tableName}_CategoriesProducts] ON [{tableName}]
    (
        [CategoryID] ASC
    );
    CREATE INDEX [IX_{tableName}_CategoryID] ON [{tableName}]
    (
        [CategoryID] ASC
    );
    CREATE INDEX [IX_{tableName}_ProductName] ON [{tableName}]
    (
        [ProductName] ASC
    );
    CREATE INDEX [IX_{tableName}_SupplierID] ON [{tableName}]
    (
        [SupplierID] ASC
    );
    CREATE INDEX [IX_{tableName}_SuppliersProducts] ON [{tableName}]
    (
        [SupplierID] ASC
    );"
conn.ExecuteNonQuery(New SqlQuery(query))

接下来,执行表复制:

var oldTable = "Products";          // Source table name
var newTable = "ReorderProducts";   // Destination table name

// Create a delegate to wrap the call and pass to CopyTable & MoveTable
var createNewProducts = new Action<SQLiteConnection>
                        ((conn) => CreateProducts(conn, newTable));

// Copy condition (after 'WHERE' sql keyword)
var condition = new SqlQuery("UnitsInStock = @uis AND UnitsOnOrder = @uoo",
                             new Dictionary<string, object>
                             { { "@uis", 0 }, { "@uoo", 0 } });

// Perform copy to new table
using (var dbCtx = new DbContext(connectionString))
{
    var count1 = dbCtx.CopyTable(oldTable, newTable, createNewProducts, condition);
    Console.WriteLine($"{count1} records copied"); 
}
Dim oldTable = "Products"           ' Source table name
Dim newTable = "ReorderProducts"    ' Destination table name

' Create a delegate to wrap the call and pass to CopyTable & MoveTable
Dim createNewProducts = _
    New Action(Of SQLiteConnection)(Sub(conn) CreateProducts(conn, newTable))

' Copy condition (after 'WHERE' sql keyword)
Dim condition = New SqlQuery("UnitsInStock = @uis AND UnitsOnOrder = @uoo",
    New Dictionary(Of String, Object) From {{"@uis", 0}, {"@uoo", 0}})

' Perform copy to new table
Using dbCtx = New DbContext(connectionString)
    Dim count = dbCtx.CopyTable(oldTable, newTable, createNewProducts, condition)
    Console.WriteLine($"{count} records copied")
End Using

移动表

MoveTable 方法的工作方式与 CopyTable 方法相同,除了完成后会删除旧表 (并且可以选择删除所有关联的索引)。

var oldTable = "Products";          // Source table name
var newTable = "ReorderProducts";   // Destination table name

// Create a delegate to wrap the call and pass to CopyTable & MoveTable
var createNewProducts = 
    new Action<SQLiteConnection>((conn) => CreateProducts(conn, newTable));

// Copy condition (after 'WHERE' sql keyword)
SqlQuery condition = null;  // set to only do a partial move and delete remainder

// Perform move to new table
using (var dbCtx = new DbContext(connectionString))
{
    var count = dbCtx.MoveTable(oldTable, newTable, createNewProducts, condition);
    Console.WriteLine($"{count} records moved");
}
Dim oldTable = "Products"           ' Source table name
Dim newTable = "ReorderProducts"    ' Destination table name

' Create a delegate to wrap the call and pass to CopyTable & MoveTable
Dim createNewProducts = New Action(Of SQLiteConnection)_
                        (Sub(conn) CreateProducts(conn, newTable))

' Copy condition (after 'WHERE' sql keyword)
Dim condition As SqlQuery = Nothing ' set to only do a partial move and delete remainder

' Perform move to new table
Using dbCtx = New DbContext(connectionString)
    Dim count1 = dbCtx.MoveTable(oldTable, newTable, createNewProducts, condition)
    Console.WriteLine($"{count1} records moved")
End Using

删除表

DropTable 方法将删除表,并可以选择删除所有关联的索引。

var oldTable = "Products";          // Source table name

using (var dbCtx = new DbContext(connectionString))
{
    dbCtx.DropTable(oldTable, dropIndicies: true);
}
Dim oldTable = "Products"           ' Source table name

Using dbCtx = New DbContext(connectionString)
    dbCtx.DropTable(oldTable, dropIndicies:=True)
End Using

重命名表

RenameTable 方法将重命名表。

var oldTable = "Products";          // Source table name
var newTable = "ReorderProducts";   // Destination table name

using (var dbCtx = new DbContext(connectionString))
{
    dbCtx.RenameTable(oldTable, newTable);
}
Dim oldTable = "Products"           ' Source table name
Dim newTable = "ReorderProducts"    ' Destination table name

Using dbCtx = New DbContext(connectionString)
    dbCtx.RenameTable(oldTable, newTable)
End Using

压缩数据库

数据库的大小可能会随着时间的推移而迅速增长,变得碎片化,有时会影响性能。定期压缩数据库是个好主意,以保持性能。

using (var dbCtx = new DbContext(connectionString))
{
    dbCtx.CompactDB();
}
Using dbCtx = New DbContext(connectionString)
    dbCtx.CompactDB()
End Using

重新索引

当碎片化程度严重影响性能时,索引需要维护。这种碎片化程度可能因索引的大小和组成而异。应定期进行索引维护过程。但是,索引维护过程应仅重建/重新组织需要它的特定索引。

常规重新索引

SQLite 有一个 REINDEX SQL 命令。下面是一个简化其使用的包装器方法。

重新索引所有表:

using (var dbCtx = new DbContext(connectionString))
{
    dbCtx.Reindex();
}
Using dbCtx = New DbContext(connectionString)
    dbCtx.Reindex()
End Using

重新索引特定表,例如 "Categories":

using (var dbCtx = new DbContext(connectionString))
{
    dbCtx.Reindex("Categories");
}
Using dbCtx = New DbContext(connectionString)
    dbCtx.Reindex("Categories")
End Using
完全重新索引

有时,标准的 REINDEX 不够,需要更彻底的重新索引。对于这些特殊情况,您需要删除索引并重新创建它。此过程将比标准重新索引花费更长的时间。DotNet.Core.SQLite 库有一个扩展方法可以做到这一点:

重新索引所有表:

using (var dbCtx = new DbContext(connectionString))
{
    dbCtx.ManualReindexAll();
}
Using dbCtx = New DbContext(connectionString)
    dbCtx.ManualReindexAll()
End Using

重新索引特定表,例如 "Categories":

using (var dbCtx = new DbContext(connectionString))
{
    dbCtx.ManualReindex(SQLiteResourceType.Table, "");
}
Using dbCtx = New DbContext(connectionString)
    dbCtx.ManualReindex(SQLiteResourceType.Table, "Categories")
End Using

重建特定索引,例如 "IX_CatCategoriesegories_CategoryName":

using (var dbCtx = new DbContext(connectionString))
{
    dbCtx.ManualReindex(SQLiteResourceType.Index, "IX_Categories_CategoryName");
}
Using dbCtx = New DbContext(connectionString)
    dbCtx.ManualReindex(SQLiteResourceType.Index, "IX_Categories_CategoryName");
End Using

使用 Pragmas

PRAGMA 语句是 SQLite 特有的 SQL 扩展,用于修改 SQLite 库的操作或查询 SQLite 库的内部 (非表) 数据。

DbContextPragmaExtension 支持所有当前的 Pragmas。目前有 49 个 SQLite Pragma 命令,它们都受支持。如果您需要手动执行 Pragma 命令,可以通过 DbContext 类或标准的 SQLite SQLiteConnection 类调用两个函数:GetPragmaSetPragma

支持的 Pragma 命令包括:

命令 Get Set 返回类型
ApplicationID Y Y int
AutoVacuum Y Y SQLitePragmaAutoVacuumType
AutomaticIndex Y Y bool
BusyTimeout Y Y int
CacheSize Y Y int
CacheSpill Y Y int
CaseSensitiveLike - Y -
CellSizeCheck Y Y bool
CheckpointFullfsync Y Y bool
CollationList Y - IEnumerable<string>
CompileOptions Y - IEnumerable<string>
DatabaseList Y - IEnumerable<string>
DataVersion Y - int
DeferForeignKeys Y Y bool
编码 Y Y SQLitePragmaEncodingType
ForeignKeyCheck Y - IEnumerable<KeyValuePair<string, string>>
ForeignKeyList Y - IEnumerable<SQLitePragmaForeignKey>
ForeignKeys Y Y bool
FreelistCount Y - int
Fullfsync Y Y bool
IgnoreCheckConstraints Y Y bool
IncrementalVacuum Y - -
IndexInfo Y - IEnumerable<SQLitePragmaIndexInfo>
IndexList Y - IEnumerable<SQLitePragmaIndexListItem>
IndexXInfo Y - IEnumerable<SQLitePragmaIndexInfo>
IntegrityCheck Y - 字符串
JournalMode Y Y SQLitePragmaJournalModeType
JournalSizeLimit Y Y int
LegacyFileFormat Y Y bool
LockingMode Y Y SQLitePragmaLockingModeType
MaxPageCount Y Y int
MmapSize Y Y int
Optimize Y - 字符串
PageCount Y - int
PageSize Y Y int
QueryOnly Y Y bool
QuickCheck Y - 字符串
ReadUncommitted Y Y bool
RecursiveTriggers Y Y bool
ReverseUnorderedSelects Y Y bool
SecureDelete Y Y SQLitePragmaSecureDeleteType
ShrinkMemory - Y -
SoftHeapLimit Y Y SoftHeapLimit
Synchronous Y Y SQLitePragmaSynchronousType
TableInfo Y Y IEnumerable<SQLitePragmaTableInfo>
TempStore Y Y SQLitePragmaTempStoreType
线程 Y Y int
UserVersion Y Y int
WalAutocheckpoint Y Y bool

获取 Pragma 数据:

using (var dbCtx = new DbContext(connectionString))
{
    // Get the Application Identification number
    var appId = dbCtx.ApplicationId();

    // Get the User version
    var userVersion = dbCtx.UserVersion();

    // Get the Data revision version (read only)
    var dataVersion = dbCtx.DataVersion();
}
Using dbCtx = New DbContext(connectionString)

    ' Get the Application Identification number
    Dim appId = dbCtx.ApplicationId()

    ' Get the User version
    Dim userVersion = dbCtx.UserVersion()

    ' Get the Data revision version (read only)
    Dim dataVersion = dbCtx.DataVersion()
End Using

设置 Pragma 数据:

using (var dbCtx = new DbContext(connectionString))
{
    // Set the Application Identification number
    dbCtx.ApplicationId(1234);

    // Set the User version
    dbCtx.UserVersion(5678);
}
Using dbCtx = New DbContext(connectionString)

    ' Set the Application Identification number
    dbCtx.ApplicationId(1234)

    ' Set the User version
    dbCtx.UserVersion(5678)
End Using

单个 Pragma 命令可能返回不同的数据,返回或传递的信息在需要时会被封装。

还有高效检索 SQLite Pragma 信息的报告方法:

  • GetPragmas - 获取一个或多个 Pragma 类型的 Pragma 值。
  • GetPragmaTypes - 根据模式 (Mode)、访问范围 (Access scope) 和属性范围 (Property scope) 获取 Pragma 类型列表。

生成 DDL (数据定义语言)

DDL 用于定义数据结构和数据库模式。有许多重载的 GenerateDDL 方法可以针对 DbContextSQLiteConnection 类进行调用。

  1. 提取所有定义
    using (var dbCtx = new DbContext(connectionString))
    {
        var ddl = dbCtx.GenerateDDL();
    }
    Using dbCtx = New DbContext(connectionString)
        Dim ddl = dbCtx.GenerateDDL()
    End Using
  2. 提取表/视图/索引/触发器定义
    using (var dbCtx = new DbContext(connectionString))
    {
        var ddlTable = dbCtx.GenerateDDL("Products");
        var ddlView = dbCtx.GenerateDDL(viewName: "ProductsView");
        var ddlIndex = dbCtx.GenerateDDL(indexName: "IX_Products_CategoriesProducts");
        var ddlCombo = dbCtx.GenerateDDL
            (tableName: "Products", indexName: "IX_Products_CategoriesProducts");
    }
    Using dbCtx = New DbContext(connectionString)
        Dim ddlTable = dbCtx.GenerateDDL("Products")
        Dim ddlView = dbCtx.GenerateDDL(viewName:="ProductsView")
        Dim ddlIndex = dbCtx.GenerateDDL(indexName:="IX_Products_CategoriesProducts")
        Dim ddlCombo = dbCtx.GenerateDDL
            (tableName:="Products", indexName:="IX_Products_CategoriesProducts")
    End Using

报告

有时您需要收集客户数据库的信息,以诊断问题或在开发周期中进行分析。Report 方法将根据所需的详细程度收集和转储一系列信息:

  1. 连接参数
  2. 连接信息
  3. 连接标志
  4. 数据库 Pragma 设置
  5. 连接 Pragma 设置
  6. 表信息:配置、DDL、统计信息 (索引、触发器、记录数)
  7. 索引信息
  8. 触发器信息
  9. 视图信息

报告信息的级别可以通过传递给 DbContext 类的 Report 方法的参数来选择。

下面是一个完整的报告调用:

using (var dbCtx = new DbContext(builder))
{
    Console.WriteLine(dbCtx.Report());
}
Using dbCtx = New DbContext(connectionString)
    Console.WriteLine(dbCtx.Report())
End Using

如果只需要 Pragma 设置:

using (var dbCtx = new DbContext(builder))
{
    Console.WriteLine(dbCtx.Report(SQLiteSchemaParamaterType.Pragma));
}
Using dbCtx = New DbContext(connectionString)
    Console.WriteLine(dbCtx.Report(SQLiteSchemaParamaterType.Pragma))
End Using

这是报告的前 5 部分的摘录:

== CONNECTIONSTRINGS ===========================================================

BaseSchemaName            =
BinaryGUID                = [uninitialized]
BrowsableConnectionString = True
BusyTimeout               = 0
CacheSize                 = -2000
ConnectionString          = version=3;data source=northwind.db
Count                     = 2
DataSource                = northwind.db
DateTimeFormat            = ISO8601
DateTimeFormatString      =
DateTimeKind              = Unspecified
DefaultDbType             = -1
DefaultIsolationLevel     = [uninitialized]
DefaultTimeout            = 30
DefaultTypeName           =
Enlist                    = True
FailIfMissing             = [uninitialized]
Flags                     = Default
ForeignKeys               = False
FullUri                   =
HexPassword               =
IsFixedSize               = False
IsReadOnly                = False
JournalMode               = Default
LegacyFormat              = False
MaxPageCount              = 0
NoDefaultFlags            = [uninitialized]
NoSharedFlags             = [uninitialized]
PageSize                  = 4096
Password                  =
Pooling                   = False
PrepareRetries            = 0
ProgressOps               = 0
ReadOnly                  = False
RecursiveTriggers         = False
SetDefaults               = [uninitialized]
SyncMode                  = Normal
ToFullPath                = [uninitialized]
Uri                       =
UseUTF16Encoding          = [uninitialized]
Version                   = 3
VfsName                   =
WaitTimeout               = 0
ZipVfsVersion             =

== CONNECTIONS =================================================================

AutoCommit            = True
BusyTimeout           = 0
ConnectionPool        =
ConnectionString      = version=3;data source=northwind.db
ConnectionTimeout     = 15
data source           = northwind.db
DefaultDbType         =
DefaultFlags          = Default
DefaultTimeout        = 30
DefaultTypeName       =
DefineConstants       = INTEROP_CODEC INTEROP_EXTENSION_FUNCTIONS INTEROP_FTS5_EXTENSION 
                        INTEROP_JSON1_EXTENSION INTEROP_PERCENTILE_EXTENSION 
                        INTEROP_REGEXP_EXTENSION INTEROP_SESSION_EXTENSION 
                        INTEROP_SHA1_EXTENSION INTEROP_TOTYPE_EXTENSION 
                        INTEROP_VIRTUAL_TABLE NET_45 PRELOAD_NATIVE_LIBRARY 
                        THROW_ON_DISPOSED TRACE TRACE_PRELOAD TRACE_SHARED 
                        TRACE_WARNING USE_INTEROP_DLL USE_PREPARE_V2 WINDOWS
Flags                 = Default
InteropCompileOptions = CODEC EXTENSION_FUNCTIONS JSON1_EXTENSION PERCENTILE_EXTENSION 
                        REGEXP_EXTENSION SESSION_EXTENSION SHA1_EXTENSION TOTYPE_EXTENSION 
                        VERSION_NUMBER=3022000 VIRTUAL_TABLE
InteropSourceId       = 2814aa20f86be6e8085206338c80cbb01c3881d0 2018-02-27 17:24:44 UTC
InteropVersion        = 1.0.108.0
LastInsertRowId       = 0
MemoryHighwater       = 126751
MemoryUsed            = 126751
OwnHandle             = True
ParseViaFramework     = False
PoolCount             = 0
PrepareRetries        = 3
ProgressOps           = 0
ProviderSourceId      = 2814aa20f86be6e8085206338c80cbb01c3881d0 2018-02-27 17:24:44 UTC
ProviderVersion       = 1.0.108.0
ServerVersion         = 3.22.0
SharedFlags           = None
Site                  =
SQLiteCompileOptions  = COMPILER=msvc-1700 ENABLE_API_ARMOR ENABLE_COLUMN_METADATA 
                        ENABLE_DBSTAT_VTAB ENABLE_FTS3 ENABLE_LOAD_EXTENSION 
                        ENABLE_MEMORY_MANAGEMENT ENABLE_PREUPDATE_HOOK ENABLE_RTREE 
                        ENABLE_SESSION ENABLE_STAT4 ENABLE_STMTVTAB HAS_CODEC SOUNDEX 
                        THREADSAFE=1 USE_URI WIN32_MALLOC
SQLiteSourceId        = 2018-01-22 18:45:57 
                        0c55d179733b46d8d0ba4d88e01a25e10677046ee3da1d5b1581e86726f2171d
SQLiteVersion         = 3.22.0
version               = 3
VfsName               =
WaitTimeout           = 30000

== SQLITE CONNECTION FLAGS =====================================================

AllowNestedTransactions                = False
BindAllAsText                          = False
BindAndGetAllAsInvariantText           = False
BindAndGetAllAsText                    = False
BindDateTimeWithKind                   = False
BindDecimalAsText                      = False
BindInvariantDecimal                   = True
BindInvariantText                      = False
BindUInt32AsInt64                      = False
ConvertAndBindAndGetAllAsInvariantText = False
ConvertAndBindInvariantText            = False
ConvertInvariantText                   = False
Default                                = True
DefaultAndLogAll                       = True
DenyOnException                        = False
DetectStringType                       = False
DetectTextAffinity                     = False
GetAllAsText                           = False
GetDecimalAsText                       = False
GetInvariantDecimal                    = True
InterruptOnException                   = False
LogAll                                 = True
LogBackup                              = False
LogBind                                = False
LogCallbackException                   = True
LogDefault                             = True
LogModuleError                         = False
LogModuleException                     = True
LogPreBind                             = False
LogPrepare                             = False
MapIsolationLevels                     = False
NoBindFunctions                        = False
NoConnectionPool                       = False
NoConvertSettings                      = False
NoCreateModule                         = False
NoExtensionFunctions                   = False
NoGlobalTypes                          = False
NoLoadExtension                        = False
NoLogModule                            = False
NoVerifyTextAffinity                   = False
NoVerifyTypeAffinity                   = False
RollbackOnException                    = False
StickyHasRows                          = False
StrictEnlistment                       = False
TraceWarning                           = False
UnbindFunctionsOnClose                 = False
UseConnectionAllValueCallbacks         = False
UseConnectionBindValueCallbacks        = False
UseConnectionPool                      = False
UseConnectionReadValueCallbacks        = False
UseConnectionTypes                     = False
UseParameterAnythingForTypeName        = False
UseParameterDbTypeForTypeName          = False
UseParameterNameForTypeName            = False
WaitForEnlistmentReset                 = False

== PRAGMA: DATABASES ===========================================================

application_id            = 0
auto_vacuum               = None
cache_size                = -2000
collation_list            = [RTRIM], [NOCASE], [BINARY]
database_list             = [main]
encoding                  = UTF-8
foreign_key_check         =
foreign_keys              = Off
freelist_count            = 0
journal_mode              = wal
journal_size_limit        = -1
legacy_file_format        = Off
max_page_count            = 1073741823
mmap_size                 = 0
page_count                = 811
page_size                 = 1024
schema_version            = 28
user_version              = 0
writable_schema           = Off

== PRAGMA: CONNECTIONS =========================================================

automatic_index           = On
busy_timeout              = 0
cache_spill               = 1841
cell_size_check           = Off
checkpoint_fullfsync      = Off
compile_options           = [COMPILER=msvc-1700], [ENABLE_API_ARMOR], 
                            [ENABLE_COLUMN_METADATA], [ENABLE_DBSTAT_VTAB], 
                            [ENABLE_FTS3], [ENABLE_LOAD_EXTENSION], 
                            [ENABLE_MEMORY_MANAGEMENT], [ENABLE_PREUPDATE_HOOK], 
                            [ENABLE_RTREE], [ENABLE_SESSION], [ENABLE_STAT4], 
                            [ENABLE_STMTVTAB], [HAS_CODEC], [SOUNDEX], 
                            [THREADSAFE=1], [USE_URI], [WIN32_MALLOC]
data_version              = 3
defer_foreign_keys        = Off
fullfsync                 = Off
ignore_check_constraints  = Off
locking_mode              = normal
query_only                = Off
read_uncommitted          = Off
recursive_triggers        = Off
reverse_unordered_selects = Off
secure_delete             = Off
soft_heap_limit           = 0
synchronous               = Full
temp_store                = Default
threads                   = 0
wal_autocheckpoint        = 1000
wal_checkpoint            = [busy = 0], [log = 3], [checkpointed = 3]
[...trimmed...]

基准测试

DotNet.Core.SQLite 库经过优化,以提供最大的灵活性,同时影响最小。以下是一些基准测试比较:

我的开发/测试机器是一台 MacBook Pro。不是最快的电脑,但工作勤恳,因为我总是在移动。这是 Benchmark 对我的系统的看法:

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i7-4980HQ CPU 2.80GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
Frequency=2728070 Hz, Resolution=366.5595 ns, Timer=TSC
  [Host]     : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3110.0
  DefaultJob : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3110.0

以下所有基准测试都包含在下载中。测试是针对一个包含 77 个产品和 8 个类别的较小数据集进行的。基准测试的目的不是测试 SQLite 的性能,而是测试 DotNet.Core.SQLite 库的最小开销。我试图在每个测试中保持代码尽可能相同,以进行无偏见的比较。

计数比较

有 5 个基准测试,从所有标准的 SQLite 代码,到 DotNet.Core.SQLite 库和标准 SQLite 代码的混合,最后是仅使用 DotNet.Core.SQLite 库的代码。

public class CountBenchmark
{
    public CountBenchmark()
    {
        // Register the Table/View Context
        dbRegistry.Register(new ProductsContext(), dbRegistry.Register(dbCtx));
    }

    private static readonly string connectionString
        = "data source=northwind.db;version=3;journal mode=Wal"

    private readonly DbContext dbCtx = new DbContext(connectionString);
    private readonly DbRegistry dbRegistry = new DbRegistry();

    [Benchmark(Baseline = true)]
    public void RawSQLite()
    {
        using (var conn = new SQLiteConnection(connectionString))
        using (var cmd = new SQLiteCommand("SELECT COUNT (*) FROM Products", conn))
        {
            conn.Open();
            using (var coreReader = cmd.ExecuteReader())
            {
                coreReader.Read();
                long productCount = coreReader.HasRows ? coreReader.GetInt64(0) : 0;
            }
        }
    }

    [Benchmark]
    public void RawDbContext()
    {
        using (var conn = dbCtx.ConnectionFactory())
        using (var cmd = new SQLiteCommand("SELECT COUNT (*) FROM Products", conn))
        {
            conn.Open();
            using (var coreReader = cmd.ExecuteReader())
            {
                coreReader.Read();
                long productCount = coreReader.HasRows ? coreReader.GetInt64(0) : 0;
            }
        }
    }

    [Benchmark]
    public void ExtensionDbContext()
    {
        long productCount = dbCtx.RecordCount("Products");
    }

    [Benchmark]
    public void ExtensionSQLiteConnection()
    {
        using (var conn = dbCtx.ConnectionFactory())
        {
            long productCount = conn.RecordCount("Products");
        }
    }

    [Benchmark]
    public void ResourceContext()
    {
        long productCount = dbRegistry.Resolve<Product>().Count();
    }
}
Public Class CountBenchmark

    Public Sub New()
        dbRegistry.Register(New ProductsContext(), dbRegistry.Register(dbCtx))
    End Sub

    Private Shared ReadOnly connectionString As String
        = "data source=northwind.db;version=3;journal mode=Wal"

    Private ReadOnly dbCtx As DbContext = New DbContext(connectionString)
    Private ReadOnly dbRegistry As DbRegistry = New DbRegistry()

    <Benchmark(Baseline:=True)>
    Public Sub RawSQLite()

        Using conn = New SQLiteConnection(connectionString)
            Using cmd = New SQLiteCommand("SELECT COUNT (*) FROM Products", conn)
                conn.Open()

                Using coreReader = cmd.ExecuteReader()
                    coreReader.Read()
                    Dim productCount As Long = _
                        If(coreReader.HasRows, coreReader.GetInt64(0), 0)
                End Using
            End Using
        End Using

    End Sub

    <Benchmark>
    Public Sub RawDbContext()

        Using conn = dbCtx.ConnectionFactory()
            Using cmd = New SQLiteCommand("SELECT COUNT (*) FROM Products", conn)
                conn.Open()

                Using coreReader = cmd.ExecuteReader()
                    coreReader.Read()
                    Dim productCount As Long = _
                        If(coreReader.HasRows, coreReader.GetInt64(0), 0)
                End Using
            End Using
        End Using

    End Sub

    <Benchmark>
    Public Sub ExtensionDbContext()

        Dim productCount As Long = dbCtx.RecordCount("Products")

    End Sub

    <Benchmark>
    Public Sub ExtensionSQLiteConnection()

        Using conn = dbCtx.ConnectionFactory()
            Dim productCount As Long = conn.RecordCount("Products")
        End Using

    End Sub

    <Benchmark>
    Public Sub ResourceContext()

        Dim productCount As Long = dbRegistry.Resolve(Of Product)().Count()

    End Sub

End Class

结果是:

<small>
                    Method |    Mean |    Error |   StdDev | Scaled | ScaledSD |  Gen 0 | Allocated |
-------------------------- |--------:|---------:|---------:|-------:|---------:|-------:|----------:|
                 RawSQLite | 7.239ms | 0.1408ms | 0.1622ms |   1.00 |     0.00 | 7.8125 |  62.19 KB |
              RawDbContext | 7.336ms | 0.1453ms | 0.2730ms |   1.01 |     0.04 | 7.8125 |  62.19 KB |
        ExtensionDbContext | 7.209ms | 0.1385ms | 0.1423ms |   1.00 |     0.03 | 7.8125 |  62.31 KB |
 ExtensionSQLiteConnection | 7.384ms | 0.1441ms | 0.1972ms |   1.02 |     0.03 | 7.8125 |  62.31 KB |
           ResourceContext | 7.241ms | 0.1413ms | 0.1735ms |   1.00 |     0.03 | 7.8125 |  62.44 KB |</small>

这是一项简单的任务,开销非常小,甚至没有。

记录读取比较

与上面的计数基准测试类似,基准测试范围从原始到完整的 DotNet.Core.SQLite 库代码。此外,还有原始 SQLite 和完整 DotNet.Core.SQLite 库代码的异步版本。

[MemoryDiagnoser]
public class ReadBenchmark
{
    public ReadBenchmark()
    {
        // Register the Table/View Context
        dbRegistry.Register(new ProductsContext(), dbRegistry.Register(dbCtx));
    }

    private static readonly string connectionString
        = "data source=northwind.db;version=3;journal mode=Wal"

    private readonly DbContext dbCtx = new DbContext(connectionString);
    private readonly DbRegistry dbRegistry = new DbRegistry();

    [Benchmark(Baseline = true)]
    public void RawSQLite()
    {
        var Products = new List<Product>();

        using (var conn = new SQLiteConnection(connectionString))
        {
            conn.Open();

            using (var cmd = conn.CommandFactory("SELECT * FROM Products;"))
            using (var coreReader = cmd.ExecuteReader())
            {
                while (coreReader.Read())
                {
                    Products.Add(new Product
                    {
                        ID = coreReader.GetLong("ProductID", 0L),
                        Name = coreReader.GetString("ProductName"),
                        SupplierID = coreReader.GetLong("SupplierID", 0L),
                        CategoryID = coreReader.GetLong("SupplierID", 0L),
                        QuantityPerUnit = coreReader.GetString("QuantityPerUnit"),
                        UnitPrice = coreReader.GetDecimal("UnitPrice", 0M),
                        UnitsInStock = coreReader.GetInt("UnitsInStock", 0),
                        UnitsOnOrder = coreReader.GetInt("UnitsOnOrder", 0),
                        ReorderLevel = coreReader.GetInt("ReorderLevel", 0),
                        Discontinued = coreReader.GetBoolean("Discontinued", false),
                        DiscontinuedDate = coreReader.GetDateTime("DiscontinuedDate",
                                                                  DateTime.MinValue)
                    });
                }
            }
        }
    }

    [Benchmark]
    public async Task RawSQLiteAsync()
    {
        var Products = new List<Product>();

        using (var conn = new SQLiteConnection(connectionString))
        {
            await conn.OpenAsync().ConfigureAwait(false);

            using (var cmd = conn.CommandFactory("SELECT * FROM Products;"))
            using (var coreReader = (SQLiteDataReader)(await cmd.ExecuteReaderAsync()
                                                                .ConfigureAwait(false)))
            {
                while (await coreReader.ReadAsync().ConfigureAwait(false))
                {
                    Products.Add(new Product
                    {
                        ID = coreReader.GetLong("ProductID", 0L),
                        Name = coreReader.GetString("ProductName"),
                        SupplierID = coreReader.GetLong("SupplierID", 0L),
                        CategoryID = coreReader.GetLong("SupplierID", 0L),
                        QuantityPerUnit = coreReader.GetString("QuantityPerUnit"),
                        UnitPrice = coreReader.GetDecimal("UnitPrice", 0M),
                        UnitsInStock = coreReader.GetInt("UnitsInStock", 0),
                        UnitsOnOrder = coreReader.GetInt("UnitsOnOrder", 0),
                        ReorderLevel = coreReader.GetInt("ReorderLevel", 0),
                        Discontinued = coreReader.GetBoolean("Discontinued", false),
                        DiscontinuedDate = coreReader.GetDateTime("DiscontinuedDate",
                                                                  DateTime.MinValue)
                    });
                }
            }
        }
    }

    [Benchmark]
    public void RawDbContext()
    {
        var Products = new List<Product>();

        using (var conn = dbCtx.ConnectionFactory())
        {
            conn.Open();

            using (var cmd = conn.CommandFactory("SELECT * FROM Products;"))
            using (var coreReader = cmd.ExecuteReader())
            {
                while (coreReader.Read())
                {
                    Products.Add(new Product
                    {
                        ID = coreReader.GetLong("ProductID", 0L),
                        Name = coreReader.GetString("ProductName"),
                        SupplierID = coreReader.GetLong("SupplierID", 0L),
                        CategoryID = coreReader.GetLong("SupplierID", 0L),
                        QuantityPerUnit = coreReader.GetString("QuantityPerUnit"),
                        UnitPrice = coreReader.GetDecimal("UnitPrice", 0M),
                        UnitsInStock = coreReader.GetInt("UnitsInStock", 0),
                        UnitsOnOrder = coreReader.GetInt("UnitsOnOrder", 0),
                        ReorderLevel = coreReader.GetInt("ReorderLevel", 0),
                        Discontinued = coreReader.GetBoolean("Discontinued", false),
                        DiscontinuedDate = coreReader.GetDateTime("DiscontinuedDate",
                                                                  DateTime.MinValue)
                    });
                }
            }
        }
    }

    [Benchmark]
    public void ResourceContext()
    {
        var Products = dbRegistry.Resolve<Product>().Get().ToList();
    }

    [Benchmark]
    public async Task ResourceContextAsync()
    {
        var Products = (await dbRegistry.Resolve<Product>()
                                        .GetAsync().ConfigureAwait(false)).ToList();
    }
}
Public Class ReadBenchmark

    Protected Sub New()
        dbRegistry.Register(New ProductsContext(), dbRegistry.Register(dbCtx))
    End Sub

    Private Shared ReadOnly connectionString As String
        = "data source=northwind.db;version=3;journal mode=Wal"

    Private ReadOnly dbCtx As DbContext = New DbContext(connectionString)
    Private ReadOnly dbRegistry As DbRegistry = New DbRegistry()

    <Benchmark(Baseline:=True)>
    Public Sub RawSQLite()

        Dim Products = New List(Of Product)()

        Using conn = New SQLiteConnection(connectionString)
            conn.Open()

            Using cmd = conn.CommandFactory("SELECT * FROM Products;")
                Using coreReader = cmd.ExecuteReader()
                    While coreReader.Read()
                        Products.Add(New Product With {
                        .Id = coreReader.GetLong("ProductID", 0L),
                        .Name = coreReader.GetString("ProductName"),
                        .SupplierID = coreReader.GetLong("SupplierID", 0L),
                        .CategoryID = coreReader.GetLong("SupplierID", 0L),
                        .QuantityPerUnit = coreReader.GetString("QuantityPerUnit"),
                        .UnitPrice = coreReader.GetDecimal("UnitPrice", 0D),
                        .UnitsInStock = coreReader.GetInt("UnitsInStock", 0),
                        .UnitsOnOrder = coreReader.GetInt("UnitsOnOrder", 0),
                        .ReorderLevel = coreReader.GetInt("ReorderLevel", 0),
                        .Discontinued = coreReader.GetBoolean("Discontinued", False),
                        .DiscontinuedDate = coreReader.GetDateTime("DiscontinuedDate",
                                                                   DateTime.MinValue)
                    })
                    End While
                End Using
            End Using
        End Using

    End Sub

    <Benchmark>
    Public Async Function RawSQLiteAsync() As Task

        Dim Products = New List(Of Product)()

        Using conn = New SQLiteConnection(connectionString)
            Await conn.OpenAsync().ConfigureAwait(False)

            Using cmd = conn.CommandFactory("SELECT * FROM Products;")
                Using coreReader = CType((Await cmd.ExecuteReaderAsync().ConfigureAwait(False)), 
                                         SQLiteDataReader)
                    While Await coreReader.ReadAsync().ConfigureAwait(False)
                        Products.Add(New Product With {
                                .Id = coreReader.GetLong("ProductID", 0L),
                                .Name = coreReader.GetString("ProductName"),
                                .SupplierID = coreReader.GetLong("SupplierID", 0L),
                                .CategoryID = coreReader.GetLong("SupplierID", 0L),
                                .QuantityPerUnit = coreReader.GetString("QuantityPerUnit"),
                                .UnitPrice = coreReader.GetDecimal("UnitPrice", 0D),
                                .UnitsInStock = coreReader.GetInt("UnitsInStock", 0),
                                .UnitsOnOrder = coreReader.GetInt("UnitsOnOrder", 0),
                                .ReorderLevel = coreReader.GetInt("ReorderLevel", 0),
                                .Discontinued = coreReader.GetBoolean("Discontinued", False),
                                .DiscontinuedDate = coreReader.GetDateTime("DiscontinuedDate",
                                                                           DateTime.MinValue)
                            })
                    End While
                End Using
            End Using
        End Using

    End Function

    <Benchmark>
    Public Sub RawDbContext()

        Dim Products = New List(Of Product)()

        Using conn = dbCtx.ConnectionFactory()
            conn.Open()

            Using cmd = conn.CommandFactory("SELECT * FROM Products;")
                Using coreReader = cmd.ExecuteReader()
                    While coreReader.Read()
                        Products.Add(New Product With {
                                .Id = coreReader.GetLong("ProductID", 0L),
                                .Name = coreReader.GetString("ProductName"),
                                .SupplierID = coreReader.GetLong("SupplierID", 0L),
                                .CategoryID = coreReader.GetLong("SupplierID", 0L),
                                .QuantityPerUnit = coreReader.GetString("QuantityPerUnit"),
                                .UnitPrice = coreReader.GetDecimal("UnitPrice", 0D),
                                .UnitsInStock = coreReader.GetInt("UnitsInStock", 0),
                                .UnitsOnOrder = coreReader.GetInt("UnitsOnOrder", 0),
                                .ReorderLevel = coreReader.GetInt("ReorderLevel", 0),
                                .Discontinued = coreReader.GetBoolean("Discontinued", False),
                                .DiscontinuedDate = coreReader.GetDateTime("DiscontinuedDate",
                                                                           DateTime.MinValue)
                            })
                    End While
                End Using
            End Using
        End Using

    End Sub

    <Benchmark>
    Public Sub ResourceContext()

        Dim Products = dbRegistry.Resolve(Of Product)().[Get]().ToList()

    End Sub

    <Benchmark>
    Public Async Function ResourceContextAsync() As Task

        Dim Products = (Await dbRegistry.Resolve(Of Product)()
                                        .GetAsync().ConfigureAwait(False)).ToList()

    End Function

End Class

结果是:

               Method |     Mean |     Error |    StdDev | Scaled | ScaledSD |  Gen 0 | Allocated |
--------------------- |---------:|----------:|----------:|-------:|---------:|-------:|----------:|
            RawSQLite | 6.379 ms | 0.0676 ms | 0.0599 ms |   1.00 |     0.00 | 7.8125 |  78.13 KB |
       RawSQLiteAsync | 6.368 ms | 0.0799 ms | 0.0709 ms |   1.00 |     0.01 | 7.8125 |  78.45 KB |
         RawDbContext | 6.382 ms | 0.0750 ms | 0.0702 ms |   1.00 |     0.01 | 7.8125 |  78.13 KB |
      ResourceContext | 6.456 ms | 0.1393 ms | 0.1303 ms |   1.01 |     0.02 | 7.8125 |  78.44 KB |
 ResourceContextAsync | 6.485 ms | 0.1001 ms | 0.0937 ms |   1.02 |     0.02 | 7.8125 |  79.19 KB |

同样,我们可以看到开销非常小,甚至没有,而 DotNet.Core.SQLite 库代码只有一行。如预期,异步开销非常低。

添加/删除记录比较

此基准测试将添加和删除 10 个类别。这组基准测试还演示了如何优化 DotNet.Core.SQLite 库调用,当对同一数据库连接执行多个操作时。将多个命令分组以在单个连接上进行数据库访问始终是最佳实践。ResourceContextAsyncResourceContextAsyncOptimized 方法清楚地展示了原因。

[MemoryDiagnoser]
public class AddDeleteBenchmark
{
    public AddDeleteBenchmark()
    {
        // Register the Table/View Context
        dbRegistry.Register(new CategoriesContext(), dbRegistry.Register(dbCtx));
    }

    private static readonly string connectionString
        = "data source=northwind.db;version=3;journal mode=Wal";

    private readonly DbContext dbCtx = new DbContext(connectionString);
    private readonly DbRegistry dbRegistry = new DbRegistry();

    private const string insertSql = 
        "INSERT INTO Categories (CategoryName, Description, Picture)" +
        " VALUES (@CategoryName, @Description, @Picture);";

    private const string deleteSql
        = "DELETE FROM Categories WHERE CategoryID = @CategoryID;";

    private static readonly byte[] nullPicture = new byte[0];

    private readonly Category categoryItem = new Category
    {
        Description = "this is a test category",
        Picture = nullPicture
    };

    private readonly int records = 10;

    [Benchmark(Baseline = true)]
    public async Task RawSQLiteAsync()
    {
        using (var conn = new SQLiteConnection(connectionString))
        {
            await conn.OpenAsync().ConfigureAwait(false);

            for (int i = 0; i < records; i++)
            {
                using (var addCmd = new SQLiteCommand(insertSql, conn))
                using (var delCmd = new SQLiteCommand(deleteSql, conn))
                {
                    addCmd.Parameters.AddWithValue("@CategoryName",
                                                   ("test category " + i).ToSqLiteText());
                    addCmd.Parameters.AddWithValue("@Description",
                                                   ("this is a test category").ToSqLiteText());
                    addCmd.Parameters.AddWithValue("@Picture", nullPicture);
                    await addCmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                    delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId);
                    await delCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                }
            }
        }
    }

    [Benchmark]
    public async Task OptimisedRawSQLiteAsync()
    {
        using (var conn = new SQLiteConnection(connectionString))
        {
            await conn.OpenAsync().ConfigureAwait(false);

            using (var addCmd = new SQLiteCommand(insertSql, conn))
            using (var delCmd = new SQLiteCommand(deleteSql, conn))
            {
                for (int i = 0; i < records; i++)
                {
                    addCmd.Parameters.AddWithValue("@CategoryName",
                                                   ("test category " + i).ToSqLiteText());
                    addCmd.Parameters.AddWithValue("@Description",
                                                   ("this is a test category").ToSqLiteText());
                    addCmd.Parameters.AddWithValue("@Picture", nullPicture);
                    await addCmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                    delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId);
                    await delCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                }
            }
        }
    }

    [Benchmark]
    public async Task OptimisedRawDbContextAsync()
    {
        using (var conn = dbCtx.ConnectionFactory())
        {
            await conn.OpenAsync().ConfigureAwait(false);
            using (var addCmd = new SQLiteCommand(insertSql, conn))
            using (var delCmd = new SQLiteCommand(deleteSql, conn))
            {
                for (int i = 0; i < records; i++)
                {
                    addCmd.Parameters.AddWithValue("@CategoryName",
                                                   ("test category " + i).ToSqLiteText());
                    addCmd.Parameters.AddWithValue("@Description",
                                                   ("this is a test category").ToSqLiteText());
                    addCmd.Parameters.AddWithValue("@Picture", nullPicture);
                    await addCmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                    delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId);
                    await delCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                }
            }
        }
    }

    [Benchmark]
    public async Task ResourceContextAsync()
    {
        var repo = dbRegistry.Resolve<Category>();
        for (int i = 0; i < records; i++)
        {
            categoryItem.Name = "test category " + i;
            await repo.AddAsync(new DbQuery<Category>(categoryItem)).ConfigureAwait(false);

            categoryItem.Id = repo.LastInsertRowId;
            await repo.DeleteAsync(new DbQuery<Category>(categoryItem)).ConfigureAwait(false);
        }
    }

    [Benchmark]
    public async Task OptimizedResourceContextAsync()
    {
        var repo = dbRegistry.Resolve<Category>();
        using (var conn = repo.Context.ConnectionFactory())
        {
            for (int i = 0; i < records; i++)
            {
                categoryItem.Name = "test category " + i;
                await repo.AddAsync(conn,
                              new DbQuery<Category>(categoryItem)).ConfigureAwait(false);

                categoryItem.Id = repo.LastInsertRowId;
                await repo.DeleteAsync(conn,
                                 new DbQuery<Category>(categoryItem)).ConfigureAwait(false);
            }
        }
    }
}
Public Class AddDeleteBenchmark

    Public Sub New()
        dbRegistry.Register(New CategoriesContext(), dbRegistry.Register(dbCtx))
    End Sub

    Private Shared ReadOnly connectionString As String
        = "data source=northwind.db;version=3;journal mode=Wal"

    Private ReadOnly dbCtx As DbContext = New DbContext(connectionString)
    Private ReadOnly dbRegistry As DbRegistry = New DbRegistry()

    Private Const insertSql As String =
        "INSERT INTO Categories (CategoryName, Description, Picture)" &
        " VALUES (@CategoryName, @Description, @Picture);"

    Private Const deleteSql As String
        = "DELETE FROM Categories WHERE CategoryID = @CategoryID;"

    Private Shared nullPicture As Byte() = New Byte(-1) {}

    Private ReadOnly categoryItem As Category = New Category With {
            .Description = "this is a test category",
            .Picture = nullPicture
        }

    Private ReadOnly records As Integer = 10

    <Benchmark(Baseline:=True)>
    Public Async Function RawSQLiteAsync() As Task

        Using conn = New SQLiteConnection(connectionString)
            Await conn.OpenAsync().ConfigureAwait(False)

            For i As Integer = 0 To records - 1
                Using addCmd = New SQLiteCommand(insertSql, conn)
                    Using delCmd = New SQLiteCommand(deleteSql, conn)
                        addCmd.Parameters.AddWithValue("@CategoryName",
                                                       ("test category " & i).ToSqLiteText())
                        addCmd.Parameters.AddWithValue("@Description",
                                                 ("this is a test category").ToSqLiteText())
                        addCmd.Parameters.AddWithValue("@Picture", nullPicture)
                        Await addCmd.ExecuteNonQueryAsync().ConfigureAwait(False)

                        delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId)
                        Await delCmd.ExecuteNonQueryAsync().ConfigureAwait(False)
                    End Using
                End Using
            Next
        End Using

    End Function

    <Benchmark>
    Public Async Function OptimisedRawSQLiteAsync() As Task

        Using conn = New SQLiteConnection(connectionString)
            Await conn.OpenAsync().ConfigureAwait(False)

            Using addCmd = New SQLiteCommand(insertSql, conn)
                Using delCmd = New SQLiteCommand(deleteSql, conn)
                    For i As Integer = 0 To records - 1
                        addCmd.Parameters.AddWithValue("@CategoryName",
                                                 ("test category " & i).ToSqLiteText())
                        addCmd.Parameters.AddWithValue("@Description",
                                                 ("this is a test category").ToSqLiteText())
                        addCmd.Parameters.AddWithValue("@Picture", nullPicture)
                        Await addCmd.ExecuteNonQueryAsync().ConfigureAwait(False)

                        delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId)
                        Await delCmd.ExecuteNonQueryAsync().ConfigureAwait(False)
                    Next
                End Using
            End Using
        End Using

    End Function

    <Benchmark>
    Public Async Function OptimisedRawDbContextAsync() As Task

        Using conn = dbCtx.ConnectionFactory()
            Await conn.OpenAsync().ConfigureAwait(False)

            Using addCmd = New SQLiteCommand(insertSql, conn)
                Using delCmd = New SQLiteCommand(deleteSql, conn)
                    For i As Integer = 0 To records - 1
                        addCmd.Parameters.AddWithValue("@CategoryName",
                                              ("test category " & i).ToSqLiteText())
                        addCmd.Parameters.AddWithValue("@Description",
                                              ("this is a test category").ToSqLiteText())
                        addCmd.Parameters.AddWithValue("@Picture", nullPicture)
                        Await addCmd.ExecuteNonQueryAsync().ConfigureAwait(False)
                        delCmd.Parameters.AddWithValue("@CategoryID", conn.LastInsertRowId)
                        Await delCmd.ExecuteNonQueryAsync().ConfigureAwait(False)
                    Next
                End Using
            End Using
        End Using

    End Function

    <Benchmark>
    Public Async Function ResourceContextAsync() As Task

        Dim repo = dbRegistry.Resolve(Of Category)()

        For i As Integer = 0 To records - 1
            categoryItem.Name = "test category " & i
            Await repo.AddAsync(New DbQuery(Of Category)
                       (categoryItem)).ConfigureAwait(False)
            categoryItem.Id = repo.LastInsertRowId
            Await repo.DeleteAsync(New DbQuery(Of Category)
                       (categoryItem)).ConfigureAwait(False)
        Next

    End Function

    <Benchmark>
    Public Async Function OptimizedResourceContextAsync() As Task

        Dim repo = dbRegistry.Resolve(Of Category)()

        Using conn = repo.Context.ConnectionFactory()

            For i As Integer = 0 To records - 1
                categoryItem.Name = "test category " & i
                Await repo.AddAsync(conn,
                                    New DbQuery(Of Category)
                                   (categoryItem)).ConfigureAwait(False)

                categoryItem.Id = repo.LastInsertRowId
                Await repo.DeleteAsync(conn,
                                       New DbQuery(Of Category)
                                      (categoryItem)).ConfigureAwait(False)
            Next
        End Using

    End Function

End Class

RawSQLiteAsyncOptimisedRawSQLiteAsync 基准测试之间的主要区别在于 OptimisedRawSQLiteAsync 基准测试在循环外部创建了 SQLiteCommands 对象。

此外,ResourceContextAsyncOptimizedResourceContextAsync 基准测试之间的主要区别在于 OptimizedResourceContextAsync 共享一个 SQLiteConnection 对象。ResourceContextAsync 在循环中的每个 AddAsyncDeleteAsync 方法内部创建 SQLiteConnectionSQLiteCommands 对象。

结果是:

                       Method |    Mean |    Error |   StdDev | Scaled | ScaledSD | Gen 0 | Allocated |
----------------------------- |--------:|---------:|---------:|-------:|---------:|------:|----------:|
               RawSQLiteAsync | 143.3ms |  2.830ms |  4.321ms |   1.00 |     0.00 |     - |     80 KB |
      OptimisedRawSQLiteAsync | 138.2ms |  2.733ms |  7.056ms |   0.97 |     0.06 |     - |   64.5 KB |
   OptimisedRawDbContextAsync | 132.5ms |  3.496ms | 10.307ms |   0.93 |     0.08 |     - |   64.5 KB |
         ResourceContextAsync | 778.1ms | 23.810ms | 70.204ms |   5.43 |     0.51 | 187.5 | 1000.6 KB |
OptimizedResourceContextAsync | 135.1ms |  2.885ms |  8.506ms |   0.94 |     0.07 |     - |     86 KB |

DotNet.Core.SQLite 库中的代码在异步环境中运行效率稍高,即使在 SQLite 仅基准测试中使用的代码与 DotNet.Core.SQLite 库中使用的代码相同。

事务性添加/删除记录比较

此基准测试与前一个几乎相同,但是,由于我们现在正在处理事务,因此将记录数量从 10 增加到 1000。

[MemoryDiagnoser]
public class TransactionBenchmark
{
    public TransactionBenchmark()
    {
        // Register the Table/View Context
        dbRegistry.Register(new CategoriesContext(), dbRegistry.Register(dbCtx));
    }

    private static readonly string connectionString
        = "data source=northwind.db;version=3;journal mode=Wal";

    private readonly DbContext dbCtx = new DbContext(connectionString);
    private readonly DbRegistry dbRegistry = new DbRegistry();

    private const string insertSql = "INSERT INTO Categories 
                                      (CategoryName, Description, Picture)"
                                      + " VALUES (@CategoryName, @Description, @Picture);";

    private const string deleteSql = "DELETE FROM Categories WHERE CategoryID = @CategoryID;";

    private static readonly byte[] nullPicture = new byte[0];

    private readonly Category categoryItem = new Category
    {
        Description = "this is a test category",
        Picture = nullPicture
    };

    private readonly int records = 1000;

    [Benchmark(Baseline = true)]
    public async Task BulkRawSQLiteTransAsync()
    {
        using (var conn = new SQLiteConnection(connectionString))
        {
            await conn.OpenAsync().ConfigureAwait(false);
            using (var trans = conn.BeginTransaction())
            {
                try
                {
                    for (int i = 0; i < records; i++)
                    {
                        using (var addCmd = new SQLiteCommand(insertSql, conn))
                        using (var delCmd = new SQLiteCommand(deleteSql, conn))
                        {
                            addCmd.Parameters.AddWithValue("@CategoryName",
                                               ("test category " + i).ToSqLiteText());
                            addCmd.Parameters.AddWithValue("@Description",
                                               ("this is a test category").ToSqLiteText());
                            addCmd.Parameters.AddWithValue("@Picture", nullPicture);
                            await addCmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                            delCmd.Parameters.AddWithValue
                                               ("@CategoryID", conn.LastInsertRowId);
                            await delCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                        }
                    }

                    trans.Commit();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("!! ERROR: " + ex.Message + " ... "
                        + ex.InnerException.Message.Replace("\r\n", " >> "));
                    // fail, so log here
                    trans.Rollback();
                }
            }
        }
    }

    [Benchmark]
    public async Task OptimisedBulkRawSQLiteTransAsync()
    {
        using (var conn = new SQLiteConnection(connectionString))
        {
            await conn.OpenAsync().ConfigureAwait(false);
            using (var trans = conn.BeginTransaction())
            {
                try
                {
                    using (var addCmd = new SQLiteCommand(insertSql, conn))
                    using (var delCmd = new SQLiteCommand(deleteSql, conn))
                    {
                        for (int i = 0; i < records; i++)
                        {
                            addCmd.Parameters.AddWithValue("@CategoryName",
                                              ("test category " + i).ToSqLiteText());
                            addCmd.Parameters.AddWithValue("@Description",
                                              ("this is a test category").ToSqLiteText());
                            addCmd.Parameters.AddWithValue("@Picture", nullPicture);
                            await addCmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                            delCmd.Parameters.AddWithValue
                                              ("@CategoryID", conn.LastInsertRowId);
                            await delCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                        }
                    }

                    trans.Commit();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("!! ERROR: " + ex.Message + " ... "
                        + ex.InnerException.Message.Replace("\r\n", " >> "));
                    // fail, so log here
                    trans.Rollback();
                }
            }
        }
    }

    [Benchmark]
    public async Task RawDbContextUOWAsync()
    {
        using (var conn = dbCtx.ConnectionFactory())
        {
            await conn.OpenAsync().ConfigureAwait(false);
            using (var uow = new UnitOfWork(conn))
            {
                try
                {
                    using (var addCmd = new SQLiteCommand(insertSql, conn))
                    using (var delCmd = new SQLiteCommand(deleteSql, conn))
                    {
                        for (int i = 0; i < records; i++)
                        {
                            addCmd.Parameters.AddWithValue("@CategoryName",
                                              ("test category " + i).ToSqLiteText());
                            addCmd.Parameters.AddWithValue("@Description",
                                              ("this is a test category").ToSqLiteText());
                            addCmd.Parameters.AddWithValue("@Picture", nullPicture);
                            await addCmd.ExecuteNonQueryAsync().ConfigureAwait(false);

                            delCmd.Parameters.AddWithValue
                                              ("@CategoryID", conn.LastInsertRowId);
                            await delCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
                        }
                    }

                    uow.SaveChanges();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("!! ERROR: " + ex.Message + " ... "
                        + ex.InnerException.Message.Replace("\r\n", " >> "));
                    // fail, so log here and the transaction
                    // will auto rollback (see UnitOfWork class)
                }
            }
        }
    }

    [Benchmark]
    public async Task ResourceContextUOWAsync()
    {
        var repo = dbRegistry.Resolve<Category>();
        using (var uow = repo.UnitOfWorkFactory())
        {
            try
            {
                for (int i = 0; i < records; i++)
                {
                    categoryItem.Name = "test category " + i;
                    await repo.AddAsync(uow, new DbQuery<Category>(categoryItem))
                              .ConfigureAwait(false);

                    categoryItem.Id = repo.LastInsertRowId;
                    await repo.DeleteAsync(uow, new DbQuery<Category>(categoryItem))
                              .ConfigureAwait(false);
                }
                uow.SaveChanges();
            }
            catch (Exception ex)
            {
                Console.WriteLine("!! ERROR: " + ex.Message + " ... "
                    + ex.InnerException.Message.Replace("\r\n", " >> "));
                // fail, so log here and the transaction
                // will auto rollback (see UnitOfWork class)
            }
        }
    }
}
Public Class TransactionBenchmark

    Public Sub New()
        dbRegistry.Register(New CategoriesContext(), dbRegistry.Register(dbCtx))
    End Sub

    Private Shared ReadOnly connectionString As String =
        "data source=northwind.db;version=3;journal mode=Wal"

    Private ReadOnly dbCtx As DbContext = New DbContext(connectionString)
    Private ReadOnly dbRegistry As DbRegistry = New DbRegistry()

    Private Const insertSql As String =
        "INSERT INTO Categories (CategoryName, Description, Picture)" &
        " VALUES (@CategoryName, @Description, @Picture);"

    Private Const deleteSql As String =
        "DELETE FROM Categories WHERE CategoryID = @CategoryID;"

    Private Shared ReadOnly nullPicture As Byte() = New Byte(-1) {}

    Private ReadOnly categoryItem As Category = New Category With {
            .Description = "this is a test category",
            .Picture = nullPicture
        }

    Private ReadOnly records As Integer = 1000

    <Benchmark(Baseline:=True)>
    Public Async Function BulkRawSQLiteTransAsync() As Task

        Using conn = New SQLiteConnection(connectionString)
            Await conn.OpenAsync().ConfigureAwait(False)

            Using trans = conn.BeginTransaction()
                Try
                    For i As Integer = 0 To records - 1
                        Using addCmd = New SQLiteCommand(insertSql, conn)
                            Using delCmd = New SQLiteCommand(deleteSql, conn)
                                addCmd.Parameters.AddWithValue("@CategoryName",
                                                  ("test category " & i).ToSqLiteText())
                                addCmd.Parameters.AddWithValue("@Description",
                                                  ("this is a test category").ToSqLiteText())
                                addCmd.Parameters.AddWithValue("@Picture", nullPicture)
                                Await addCmd.ExecuteNonQueryAsync().ConfigureAwait(False)

                                delCmd.Parameters.AddWithValue
                                                  ("@CategoryID", conn.LastInsertRowId)
                                Await delCmd.ExecuteNonQueryAsync().ConfigureAwait(False)
                            End Using
                        End Using
                    Next

                    trans.Commit()

                Catch ex As Exception
                    Console.WriteLine("!! ERROR: " & ex.Message & " ... " &
                        ex.InnerException.Message.Replace(vbCrLf, " >> "))
                    // fail, so log here
                    trans.Rollback()
                End Try
            End Using
        End Using

    End Function

    <Benchmark>
    Public Async Function OptimisedBulkRawSQLiteTransAsync() As Task

        Using conn = New SQLiteConnection(connectionString)
            Await conn.OpenAsync().ConfigureAwait(False)

            Using trans = conn.BeginTransaction()
                Try
                    Using addCmd = New SQLiteCommand(insertSql, conn)
                        Using delCmd = New SQLiteCommand(deleteSql, conn)
                            For i As Integer = 0 To records - 1
                                addCmd.Parameters.AddWithValue("@CategoryName",
                                                  ("test category " & i).ToSqLiteText())
                                addCmd.Parameters.AddWithValue("@Description",
                                                  ("this is a test category").ToSqLiteText())
                                addCmd.Parameters.AddWithValue("@Picture", nullPicture)
                                Await addCmd.ExecuteNonQueryAsync().ConfigureAwait(False)

                                delCmd.Parameters.AddWithValue
                                                  ("@CategoryID", conn.LastInsertRowId)
                                Await delCmd.ExecuteNonQueryAsync().ConfigureAwait(False)
                            Next
                        End Using
                    End Using

                    trans.Commit()

                Catch ex As Exception
                    Console.WriteLine("!! ERROR: " & ex.Message & " ... " &
                        ex.InnerException.Message.Replace(vbCrLf, " >> "))
                    // fail, so log here
                    trans.Rollback()
                End Try
            End Using
        End Using

    End Function

    <Benchmark>
    Public Async Function RawDbContextUOWAsync() As Task

        Using conn = dbCtx.ConnectionFactory()
            Await conn.OpenAsync().ConfigureAwait(False)

            Using uow = New UnitOfWork(conn)
                Try
                    Using addCmd = New SQLiteCommand(insertSql, conn)
                        Using delCmd = New SQLiteCommand(deleteSql, conn)
                            For i As Integer = 0 To records - 1
                                addCmd.Parameters.AddWithValue("@CategoryName",
                                                  ("test category " & i).ToSqLiteText())
                                addCmd.Parameters.AddWithValue("@Description",
                                                  ("this is a test category").ToSqLiteText())
                                addCmd.Parameters.AddWithValue("@Picture", nullPicture)
                                Await addCmd.ExecuteNonQueryAsync().ConfigureAwait(False)

                                delCmd.Parameters.AddWithValue
                                                  ("@CategoryID", conn.LastInsertRowId)
                                Await delCmd.ExecuteNonQueryAsync().ConfigureAwait(False)
                            Next
                        End Using
                    End Using

                    uow.SaveChanges()
                Catch ex As Exception
                    Console.WriteLine("!! ERROR: " & ex.Message & " ... " &
                        ex.InnerException.Message.Replace(vbCrLf, " >> "))
                    // fail, so log here and the transaction
                    // will auto rollback (see UnitOfWork class) 
                End Try
            End Using
        End Using

    End Function

    <Benchmark>
    Public Async Function ResourceContextUOWAsync() As Task

        Dim repo = dbRegistry.Resolve(Of Category)()

        Using uow = repo.UnitOfWorkFactory()
            Try
                For i As Integer = 0 To records - 1
                    categoryItem.Name = "test category " & i
                    Await repo.AddAsync(uow, New DbQuery(Of Category)(categoryItem))
                              .ConfigureAwait(False)
                    categoryItem.Id = repo.LastInsertRowId
                    Await repo.DeleteAsync(uow, New DbQuery(Of Category)(categoryItem))
                              .ConfigureAwait(False)
                Next

                uow.SaveChanges()

            Catch ex As Exception
                Console.WriteLine("!! ERROR: " & ex.Message & " ... " &
                    ex.InnerException.Message.Replace(vbCrLf, " >> "))
            End Try
        End Using

    End Function

End Class

结果是:

<small>
                    Method |    Mean |    Error |  StdDev | Scaled | ScaledSD |    Gen 0 |Allocated |
-------------------------- |--------:|---------:|--------:|-------:|---------:|---------:|---------:|
         BulkRawTransAsync | 54.96ms | 1.2501ms | 3.646ms |   1.00 |     0.00 | 625.0000 |   3.14MB |
OptimisedBulkRawTransAsync | 48.41ms | 2.1520ms | 6.311ms |   0.88 |     0.13 | 250.0000 |   1.46MB |
      RawDbContextUOWAsync | 45.50ms | 1.6327ms | 4.737ms |   0.83 |     0.10 | 250.0000 |   1.46MB |
   ResourceContextUOWAsync | 39.26ms | 0.7823ms | 1.017ms |   0.72 |     0.05 | 687.5000 |   3.69MB |</small>

与上一组基准测试类似,DotNet.Core.SQLite 库中的代码在异步环境中运行效率似乎稍高。

工作原理

Man Behind the Curtain

引用

不要关注幕后的人……L. Frank Baum,《绿野仙踪》

看到某些事物发生而不知道其原因,有一种简单的乐趣。有时,试图理解它会让你失去乐趣。但对于那些感兴趣的人,请继续阅读。

DotNet.Core.SQLite 库的大部分功能是标准 SQLite 资源的扩展方法,只有少量具体类:

  1. DbContext - 封装数据库连接、跟踪以及报告等数据库特定功能。
  2. DbRegistry - 将表和视图与 POCOs 和 DbContext 相关联。
  3. DbResourceContext - 将 POCO 映射到表或视图。
  4. UnitOfWork - 管理事务。

这使得开发人员可以选择使用部分扩展方法来增强他们现有的代码,或者使用 DotNet.Core.SQLite 库的全部功能。

DbContext 数据库上下文

DotNet.Core.SQLite 库围绕 DbContext 构建。DbContext 代表单个数据库实例。稍后在本文中,我们将讨论 DbRegistry 类,在该类中,我们可以将任意数量的不同数据库链接到各个表和视图。

DbContext 用作针对数据库可执行的操作 (方法) 的核心。它还用于启用内部 SQLite 引擎的跟踪和日志记录。

public class DbContext : IDisposable
{
    #region Constructor / Deconstructor

    public DbContext() { }

    public DbContext(ISqliteEventTrace trace) : this()
        => SetTrace(trace);

    public DbContext(SQLiteConnectionStringBuilder builder)
        => SetConnectionString(builder);

    public DbContext(string connectionString)
        => SetConnectionString(connectionString);

    public DbContext(string file, Dictionary<string, string> paramList)
        => SetConnectionString(file, paramList);

    public DbContext(SQLiteConnectionStringBuilder builder, ISqliteEventTrace trace)
    {
        SetConnectionString(builder);
        SetTrace(trace);
    }

    public DbContext(string connectionString, ISqliteEventTrace trace)
    {
        SetConnectionString(connectionString);
        SetTrace(trace);
    }

    public DbContext(string file, Dictionary<string, string> paramList, 
                     ISqliteEventTrace trace)
    {
        SetConnectionString(file, paramList);
        SetTrace(trace);
    }

    public void Dispose()
    {
        if (Trace != null)
        {
            Trace.StateChange -= OnStateSchanged;
            Trace.DbContext = null;
            Trace = null;
        }
        if (Builder != null) Builder = null;
    }

    // just in case we forget to dispose, let the GC do it for us...
    ~DbContext() => Dispose();

    #endregion

通过实现 IDisposable 接口,我们可以将类的生命周期封装在 using 语句中。如果未封装,则必须调用 Dispose() 方法。

    #region Properties

    public SqliteEventTraceBase Trace { get; private set; }

    public SQLiteConnectionStringBuilder Builder { get; private set; }

    public string DbPath { get => Path.GetDirectoryName(DbFileName); }
    public string DbFile { get => Path.GetFileName(DbFileName); }

    public string DbFileName { get; private set; }

    private int openConnectionCount;
    public int OpenConnectionCount { get => openConnectionCount; }

    #endregion

公开了关键属性以供快速访问:

    #region ConnectionFactory

    public SQLiteConnection ConnectionFactory()
    {
        if (Builder.IsNull() || Builder.ConnectionString.IsNullOrEmpty())
            throw new Exception("ConnectionString is not set.");

        var connection = new SQLiteConnection(Builder.ConnectionString);
        Trace?.StartListeningToConnection(connection);

        return connection;
    }

    #endregion

如果需要跟踪,则必须使用 ConnectionFactory() 方法来创建数据库连接。它将返回一个标准的 SQLite SQLiteConnection 对象。

    #region ConnectionString

    public bool SetConnectionString(SQLiteConnectionStringBuilder builder)
    {
        SetConnection(new SQLiteConnectionStringBuilder 
                     { ConnectionString = builder.ConnectionString });
        return File.Exists(Builder.DataSource);
    }

    public bool SetConnectionString(string connectionString)
    {
        SetConnection(new SQLiteConnectionStringBuilder 
                     { ConnectionString = connectionString });
        return File.Exists(Builder.DataSource);
    }

    public bool SetConnectionString(string file, Dictionary<string, string> paramList)
    {
        var @params = paramList.Count == 0
            ? ""
            : string.Concat(paramList.Select(x => $";{x.Key}={x.Value}"));

        SetConnection(new SQLiteConnectionStringBuilder($"Data Source={file}{@params}"));
        return File.Exists(Builder.DataSource);
    }

    private void SetConnection(SQLiteConnectionStringBuilder builder)
    {
        Builder = builder;
        DbFileName = builder.DataSource;
    }

    #endregion

ConnectionString 可以在初始化 DbContext 类时设置,也可以在之后手动设置。

    #region Tracing

    public void SetTrace(ISqliteEventTrace trace)
    {
        if (Trace != null)
        {
            Trace.StateChange -= OnStateSchanged;
            Trace.DbContext = null;
        }

        if (trace != null)
        {
            Trace = trace as SqliteEventTraceBase;
            Trace.DbContext = this;
            Trace.StateChange += OnStateSchanged; 
        }
    }

    internal void OnStateSchanged(object sender, StateChangeEventArgs e)
    {
        switch (e.CurrentState)
        {
            case ConnectionState.Closed:
                Interlocked.Decrement(ref openConnectionCount);
                break;
            case ConnectionState.Open:
                Interlocked.Increment(ref openConnectionCount);
                break;
        }
    }

    #endregion
}

ConnectionString 类似,跟踪也可以在初始化 DbContext 类时设置,或者在之后手动设置。

跟踪和日志记录

理解和优化代码可能很棘手。但是,SQLite 支持监听正在执行的操作。下面是再次打开连接并执行查询的代码以及跟踪日志:

var connectionString = "data source=northwind.db;version=3";

var writer = new SimpleConsoleTextWriter();
var trace = new SqliteCsvEventTrace(writer, SqliteConnectionTraceType.Full);

using (var dbCtx = new DbContext(connectionString, trace))
{
    var result = dbCtx.RecordCount("Products");
    Console.WriteLine($"\n** Total Products: {result}\n");
}

输出

_TRACE: "northwind.db","Listening","74DB74"
_TRACE: "74DB74","Event","Opening",""
_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Event","ConnectionString","version=3;data source=northwind.db"
_TRACE: "northwind.db","Event","NewCriticalHandle",
        "C:\SQLiteWrapper\Samples\Tracing\bin\Debug\northwind.db"
_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","DisposingCommand",""
_TRACE: "74DB74","State","Open"
_TRACE: "74DB74","Event","Opened",""
_TRACE: "74DB74","Event","NewCommand",""
_TRACE: "74DB74","Event","NewDataReader",""
_TRACE: "74DB74","Auth","","Select","","","Ok"
_TRACE: "74DB74","Auth","","Function","","count","Ok"
_TRACE: "74DB74","Auth","","Read","Products","","Ok"
_TRACE: "northwind.db","Event","NewCriticalHandle","SELECT Count(*) FROM Products"
_TRACE: "74DB74","Event","DisposingDataReader",""
_TRACE: "74DB74","Event","ClosingDataReader",""
_TRACE: "74DB74","Event","DisposingCommand",""
_TRACE: "74DB74","Event","Closing",""
_TRACE: "74DB74","State","Closed"
_TRACE: "74DB74","Event","Closed",""
_TRACE: "74DB74","Disposing"

** Total Products: 77

SqliteCsvEventTrace 类继承自 SqliteEventTraceBase,监听选定的事件,并将信息以 CSV 格式输出到 TextWriter。在上面的示例中,我使用了 SimpleConsoleTextWriter 类将输出转储到控制台。

SqliteEventTraceBase 类

SqliteEventTraceBase 基类封装了监听事件的代码,并公开了 TextWriter 以便应用程序编写自定义消息。

public interface ISqliteEventTrace : IDisposable
{
    TextWriter TextWriter { get; }
}
public abstract class SqliteEventTraceBase : ISqliteEventTrace 
{
    protected SqliteEventTraceBase(TextWriter writer,
                                   SqliteConnectionTraceType traceTypes =
                                   SqliteConnectionTraceType.Compact)
    {
        TextWriter = writer;
        TraceTypes = traceTypes;
    }

    internal DbContext DbContext { get; set; }
    public TextWriter TextWriter { get; internal set; }

    internal SqliteConnectionTraceType TraceTypes { get; set; }

    internal void StartListeningToConnection(SQLiteConnection connection)
    {
        // we should never encounter this! But just in case...
        if (connection.IsNull()) return;

        Write(SqliteEventTraceWriterType.StartListening, connection, null);

        connection.Disposed += OnConnectionDisposed;
        connection.StateChange += OnStateChange;

        if (IsTrace(SqliteConnectionTraceType.Authorize))
            connection.Authorize += OnAuthorize;

        // ... trimmed - see downloaded code ...
    }

    internal void StopListeningToConnection(SQLiteConnection connection)
    {
        // we should never encounter this! But just in case...
        if (connection.IsNull()) return;

        connection.Disposed -= OnConnectionDisposed;
        connection.StateChange -= OnStateChange;

        if (IsTrace(SqliteConnectionTraceType.Authorize))
            connection.Authorize -= OnAuthorize;

        // ... trimmed - see downloaded code ...
    }

    internal bool IsTrace(SqliteConnectionTraceType type) => (TraceTypes & type) != 0;

    internal event EventHandler<StateChangeEventArgs> StateChange;

    internal void RaiseStateChange(object sender, StateChangeEventArgs e)
        => StateChange?.Invoke(sender, e);

    // just in case we forget to dispose, let the GC do it for us...
    ~SqliteEventTraceBase() => Dispose();

    public virtual void Dispose()
    {
        if (TextWriter != null) TextWriter = null;
        if (DbContext != null) DbContext = null;
    }

    #region Events

    #region Event Messages

    protected internal abstract void Write(SqliteEventTraceWriterType eventType,
                                           object sender,
                                           EventArgs e);

    #endregion

    private void OnConnectionDisposed(object sender, EventArgs e)
    {
        var connection = (SQLiteConnection)sender;
        Write(SqliteEventTraceWriterType.ConnectionDisposed, sender, e);
        StopListeningToConnection(connection);
    }

        // ... trimmed - see downloaded code ...

    #endregion
}

以下是 SqliteEventTraceBase 基类的 CSV 实现。您可以创建自己的实现以满足您的特定需求。

public sealed class SqliteCsvEventTrace : SqliteEventTraceBase
{
    public SqliteCsvEventTrace(TextWriter writer,
                               SqliteConnectionTraceType traceTypes =
                               SqliteConnectionTraceType.Compact) : base(writer, traceTypes)
    {
    }

    protected internal override void Write(SqliteEventTraceWriterType eventType,
                                           object sender,
                                           EventArgs e)
    {
        var id = sender?.GetHashCode().ToString("X") ?? DbContext.DbFile;

        switch (eventType)
        {
            case SqliteEventTraceWriterType.StartListening:
                TextWriter?.WriteLine($"\"{DbContext.DbFile}\",\"Listening\",\"{id}\"");
                break;
            // ... trimmed ...
        }
    }
}

DbContextConnectionFactory 方法将调用 SqliteEventTraceBaseStartListeningToConnection 来连接事件处理程序。当事件发生时,会调用抽象的 Write 事件,然后已实现的类 (在本例中为 SqliteCsvEventTrace) 将输出格式化到 TextWriter

当连接被释放时,SqliteEventTraceBase 基类会自动取消挂钩事件处理程序,避免任何内存泄漏。

写入器示例

TextWriter 实现将简单地输出来自 SqliteEventTraceBase 基类实现的 string 文本。下面是两个包含的控制台 TextWriter 示例。您可以实现自己的,例如:写入本地或远程日志服务,写入数据库等。

public sealed class SimpleConsoleTextWriter : TextWriter
{
    public override void WriteLine(string value)
        => Console.WriteLine($"_TRACE: {value}");

    public override void Write(char value)
        => throw new NotImplementedException();

    public override Encoding Encoding
        => Encoding.UTF8;
}

下面是另一个例子:

public sealed class ConsoleTextWriter : TextWriter
{
    public override void WriteLine(string value)
        => Console.WriteLine($"_TRACE {DateTime.Now} : {value}");

    public override void Write(char value)
        => throw new NotImplementedException();

    public override Encoding Encoding
        => Encoding.UTF8;
}

DbRegistry 服务类

DbRegistry 服务只是一个跟踪数据库以及与哪个数据库关联的表和视图的类。DbTableContext<T>DbViewContext<T> 基类要求 DbRegistry 服务设置 DbContext。它还有一个方便的内置查找功能,可以将 POCO 与其 DbResourceContext<T> 关联起来。

public class DbRegistry
{
    private Dictionary<string, DbContext> DataBases { get; }
        = new Dictionary<string, DbContext>();

    private Dictionary<string, IDbResourceContext> Contexts { get; }
        = new Dictionary<string, IDbResourceContext>();

    #region Register Data Contexts

    public string Register(DbContext db, string key = "",
                           bool autoCreate = true)
    {
        if (key.IsNullOrEmpty()) key = db.GetHashCode().ToString();
        if (!DataBases.ContainsKey(key)) DataBases.Add(key, db);

        if (autoCreate && !db.Exists()) db.CreateDB();

        return key;
    }

    public void Register<T>(DbResourceContext<T> resource, string dbKey,
                            bool autoCreate = true)
    {
        if (!DataBases.ContainsKey(dbKey))
            throw new Exception($"Database not found for key {dbKey}");

        if (Contexts.ContainsKey(resource.Identifier))
            throw new Exception($"Resource {resource.Identifier} is already registered");

        ((IDbResourceContextSet)resource).SetDb(DataBases[dbKey]);
        Contexts.Add(resource.Identifier, resource);

        if (autoCreate) resource.CheckOrCreateTable();
    }

    #endregion

    #region Retrieve Data Context

    public IDbResourceContext this[string key]
        => Contexts[key];

    public IDbResourceContext this[Type type]
        => Resolve(type);

    public IDbResourceContext<T> Resolve<T>()
        => Resolve(typeof(T)) as IDbResourceContext<T>;

    private IDbResourceContext Resolve(Type type)
        => Contexts.Where(x => ((IOwnership)x.Value).IsOfType(type))
                   .Select(x => x.Value)
                   .FirstOrDefault();

    #endregion
}

DbResourceContext<t> 基类

DbResourceContext<T> 基类是 DbTableContext<T>DbViewContext<T> 基类的功能核心。DbResourceContext<T> 要求 DbRegistry 服务类来设置 DbContext

前面有两个关于如何将记录从 DB 拉取到 POCOs 的示例。回顾一下:

使用 CategoriesContext 检索所有记录:

// Get all categories
var categories = dbRegistry.Resolve<Category>().Get();

检索部分数据:

// Get the data
var query = "SELECT * FROM Categories WHERE CategoryName Like 'c%'";
var categories = dbRegistry.Resolve<Category>().Get(query);

CategoriesContext 将自动处理所有数据库操作。CategoriesContext 继承自 DbTableContext<T> 基类。对于视图,您将使用 DbViewContext<T> 基类。这两个 DbXxxxContext<T> 抽象类之间的区别在于 DbViewContext<T> 是只读的。

以下是 CategoriesContext 类。CategoriesContext 类继承自 DbTableContext<T>,并且只实现表特定的代码。所有关键方法都封装在基类中。

public class CategoriesContext : DbTableContext<Category>
{
    // trimmed...

    public override Category FromSQLiteDataReader(SQLiteDataReader coreReader)
        => new Category
    {
        Id = coreReader.GetLong("CategoryID", 0L),
        Name = coreReader.GetString("CategoryName"),
        Description = coreReader.GetString("Description"),
        Picture = coreReader.GetBinary("Picture")
    };

    // trimmed...
}

以下是将在 CategoriesContext > DbTableContext<T> > DbResourceContext<T> 基类中执行的代码:

public IEnumerable<T> Get(SQLiteConnection conn, SqlQuery query)
    => conn.Get(query, FromSQLiteDataReader);

这会调用 SQLiteConnectionGet 扩展方法:

public static IEnumerable<T> Get<T>(this SQLiteConnection @this, string query,
                                    Func<SQLiteDataReader, T> FromSQLiteDataReader)
    => @this.Get(new SqlQuery(query), FromSQLiteDataReader);

public static IEnumerable<T> Get<T>(this SQLiteConnection @this, SqlQuery query,
                                        Func<SQLiteDataReader, T> FromSQLiteDataReader)
{
    @this.OpenIfRequired();
    using (var cmd = @this.CommandFactory(query))
    using (var coreReader = cmd.ExecuteReader())
        while (coreReader.Read())
            yield return (T)Convert.ChangeType(FromSQLiteDataReader(coreReader), typeof(T));
}

SQLiteConnectionGet 扩展方法可以直接使用,无需 DbResourceContext<T> 基类。

var connectionString = "data source=northwind.db;version=3";

List<Category> categories;

using (var dbCtx = new DbContext(connectionString))
using (var conn = dbCtx.ConnectionFactory())
{
    categories = new List<Category>(
        conn.Get("SELECT * FROM Categories",
                 new Func<SQLiteDataReader, Category>((coreReader)
                 => new Category
                 {
                     Id = coreReader.GetLong("CategoryID", 0L),
                     Name = coreReader.GetString("CategoryName"),
                     Description = coreReader.GetString("Description"),
                     Picture = coreReader.GetBinary("Picture")
                 })));
}

我们也可以直接调用 DbContextGet 扩展方法:

var connectionString = "data source=northwind.db;version=3";

List<Category> categories;

using (var dbCtx = new DbContext(connectionString))
    categories = new List<Category>(
        dbCtx.Get("SELECT * FROM Categories",
                  new Func<SQLiteDataReader, Category>((coreReader)
                  => new Category
                  {
                      Id = coreReader.GetLong("CategoryID", 0L),
                      Name = coreReader.GetString("CategoryName"),
                      Description = coreReader.GetString("Description"),
                      Picture = coreReader.GetBinary("Picture")
                  })));

这是 DbContextCoreExtensionGet 扩展方法:

public static IEnumerable<T> Get<T>(this DbContext @this, string query,
                            Func<SQLiteDataReader, T> FromSQLiteDataReader)
    => @this.Get(new SqlQuery(query), FromSQLiteDataReader);

public static IEnumerable<T> Get<T>(this DbContext @this, SqlQuery query,
                                Func<SQLiteDataReader, T> FromSQLiteDataReader)
{
    using (var conn = @this.ConnectionFactory())
        return conn.Get(query, FromSQLiteDataReader);
}

DbContextCoreExtensionGet 扩展方法调用 SQLiteConnectionGet 扩展方法来检索数据。

UnitOfWork 类

这是 SQLite SQLiteTransaction 类的 BeginTransactionCommitRollback 方法的包装器,它还持有一个共享的 SQLiteConnection

public interface IUnitOfWork : IDisposable
{
    SQLiteConnection Connection { get; }
    void SaveChanges();
}
public class UnitOfWork : IUnitOfWork
{
    private SQLiteTransaction trans;

    public SQLiteConnection Connection { get; }

    public UnitOfWork(SQLiteConnection conn)
    {
        Connection = conn;

        Connection.OpenIfRequired();
        trans = Connection.BeginTransaction();
    }

    public void SaveChanges()
    {
        if (trans == null)
            throw new InvalidOperationException("UnitOfWork has already been saved.");

        trans.Commit();
        trans = null;
    }

    ~UnitOfWork() => Dispose();

    public void Dispose()
    {
        if (Connection.IsOpen()) trans?.Rollback();
    }
}

SQLiteConnection 对象传递给 UnitOfWork 构造函数时,如果需要,连接将被打开,并且事务将被启动。调用 SaveChanges 方法将尝试提交所有挂起的更改。如果 Commit 失败,则会抛出一个普通的 SQLite 异常,由开发者管理。当 UnitOfWork 类被释放时,它会检查 Commit 是否不成功,如果不成功则执行 RollBack

包含的项目 (示例代码)

注意:下载链接位于本文顶部...

下载中的核心库 (仅 C#) 包括:

  1. DotNet.Core.SQLite - 这是本文使用的库。
  2. DotNet.Core - 一些辅助函数和便捷的扩展方法。

本文中的一些代码示例 (C# 和 VB) 在下载中包含示例项目:

  1. 连接
    • 01_Connecting_manual - 仅 SQLite 代码,用于打开连接并执行查询。
    • 02_Connecting_hybrid - 演示如何使用 'DbContext' 类和 ConnectionFactory 方法来包装现有代码库。如果需要使用跟踪来检查现有代码,则非常有用。
    • 03_Connecting_fullv1 - 与上一个示例相同的代码,但现有代码已转换为使用 SQLiteConnection 类的 DotNet.Core.SQLite 库扩展方法。
    • 04_Connecting_fullv2 - 与上一个示例相同的代码,但使用 DbContext 类的扩展方法而不是 SQLiteConnection
    • 05_Connecting_fullv3 - 上面代码的修订版本,使用 DbTableContextDbRegistry 类以获得更清晰、更高效的编码。
  2. 跟踪
    • 06_Trace_OptimizationExample - 用于从 2 个表中检索计数并启用跟踪输出的代码。
    • 07_Trace_OptimizationConn - 优化代码的首次尝试,并启用跟踪输出。
    • 08_Trace_OptimizationConnSql - 优化代码的最终示例,并启用跟踪输出。
  3. 事务
    • 09_Transactions_manual - 仅 SQLite 代码,使用事务添加和删除 5,000 条记录。
    • 10_Transactions_hybrid - 使用 DotNet.Core.SQLite 库的 UnitOfWork 类替换事务代码,以实现更清晰的编码和失败事务的自动回滚。
    • 11_Transactions_full - 与上一个相同,但使用 DbTableContextDbRegistry 类以实现更清晰、更高效的编码。

下载中包含一个名为 DotNet.Core.SQLite.Benchmarking 的基准测试项目 (C# 和 VB),它使用了第三方库 BenchmarkDotNet

  1. CountBenchmark - 比较:原始 SQLite、混合模式、DbContext 类扩展方法、SQLiteConnection' 类扩展方法、DbTableContextDbRegistry 类。
  2. ReadBenchmark - 表读取比较:原始 SQLite 同步、原始 SQLite 异步、混合同步、同步 DbTableContextDbRegistry 类、异步 DbTableContextDbRegistry 类。
  3. TransactionBenchmark - 异步事务性添加和删除 5,000 条记录比较:原始 SQLite、优化的原始 SQLite、使用 UnitOfWork 类的混合优化、使用 UnitOfWork 类的 DbTableContextDbRegistry 类。

摘要

SQLite 是一个很棒的单用户应用程序系统。DotNet.Core.SQLite 库对我所需的项目来说是一个很好的封装,大大减少了所需的编码量,跟踪功能可以快速帮助识别需要调整以优化性能的地方,最后,报告功能可以检索配置文件快照以改进支持和调试。我希望其他人也能从 DotNet.Core.SQLite 库中受益。

历史

  • v1.0 - 2018 年 7 月 1 日:初始发布
© . All rights reserved.