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

SQLite 和 NPoco 的数据库初始化程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2017年6月22日

CPOL

4分钟阅读

viewsIcon

18295

downloadIcon

387

如何初始化数据库,然后自动保持其与版本发布的同步。

引言

写这篇文章是因为我在创建一个小型博客风格的 Web 应用程序时,发现 EF 和 SQL Server 的开销太大了。我需要一个更小、更轻、进程内的数据库,可以与 Microsoft AspNet Identity 框架一起使用。之前在使用 Umbraco 时,我使用过 PetaPoco 作为一个轻量级的 ORM,后来发现了 NPoco,它添加了一些很棒的功能,包括许多方法的异步版本。SQLite 是我进程内数据库的首选。

但是,这个解决方案没有提供任何自动更新数据库模式更改的方法,因此我创建了一个简单的初始化框架,这就是本文的主题。

恕我直言,SQLite 是一个很棒的产品,甚至支持全文搜索。但是,一个小限制是,在某些情况下,SQLite 只支持标准 SQL 命令的一个子集。例如,ALTER TABLE 不能用于删除列,而且与 CREATE TABLE IF NOT EXISTS 不同,没有类似的 ALTER TABLE ADD COLUMN IF NOT EXISTS。因此,任何初始化器都必须能够处理比简单地执行脚本更复杂的场景。当然,初始化器还必须有一个有效的版本控制机制,以便知道何时以及从何处开始应用更改。

最终的初始化器绝非完美,我确信读者会发现很多改进它的方法,但它可以完成工作,它灵活、自动且易于实现。它包含 2 个接口

IDbInitialiser

/// <summary>
/// IDbInitialiser is used to manage database initialisation and upgrades.
/// You would normally call this once at application startup
/// </summary>
public interface IDbInitialiser : IDisposable
{
    /// <summary>
    /// Perform database initialisation. 
    /// This should move the database from it's current
    /// version up to the latest version. 
    /// The IDbInitialiser will resolve how to locate and sort the 
    /// required IDatabaseConfigurators
    /// </summary>
    void InitialiseDatabase();
    /// <summary>
    /// Perform database initialisation. 
    /// This should move the database from it's current
    /// version up to the latest version. 
    /// The correct sequence of the IDatabaseConfigurators is the 
    /// responsibility of the caller.
    /// </summary>
    /// <param name="configurators">An array of 
    /// IDatabaseConfigurators which will be execute in order</param>
    /// <param name="dispose">Set to true to cause 
    /// each IDatabaseConfigurator to be disposed after use</param>
    void InitialiseDatabase(IDatabaseConfigurator[] configurators, bool dispose = false);
    /// <summary>
    /// After completion this should show the initial version of the database
    /// </summary>
    long InitialVersion { get; }
    /// <summary>
    /// After completion this should show the final version of the database
    /// </summary>
    long FinalVersion { get; }
    /// <summary>
    /// After completion this should show 
    /// the number of IDatabaseConfigurators which were executed
    /// </summary>
    long ConfiguratorsRun { get; }
}

这个接口提供了维护数据库模式的主要功能。每次应用程序启动时都会调用 InitialiseDatabase 方法,并且应该执行所有必要的更新,使数据库达到当前版本。它提供的属性不是必需的,但对于调试或记录数据库更改活动很有用。

IDatabaseConfigurator

/// <summary>
/// Configurators are the components which actually make database changes. 
/// To use automatic configuration the configurator classes 
/// must use a consistent naming convention 
/// which ends in 3 numeric digits starting 
/// with 001 e.g. Config001, Config002, Config003 etc. 
/// The 3 digit number is the database version number 
/// and is stored in Sqlite using a PRAGMA.
/// The convention must use a consistent name 
/// e.g. Configxxx so that the configurators can be sorted
/// into the correct operational sequence.
/// For manual configuration then class naming is 
/// irrelevant since you must provide an array of
/// configurators to IDbInitialiser which are already 
/// in the correct order and which provide their
/// own version numbers.
/// </summary>
public interface IDatabaseConfigurator : IDisposable
{
    /// <summary>
    /// Provides the version number of this set of database changes
    /// </summary>
    int Version { get; }
    /// <summary>
    /// Perform any actions on the database before changing the schema. This might 
    /// involve copying data to temp tables etc to avoid data loss
    /// </summary>
    /// <param name="db">An Npoco Database object</param>
    void PreMigrate(IDatabase db);
    /// <summary>
    /// Update the database schema
    /// The final task must be to update the version number in Sqlite
    /// </summary>
    /// <param name="db">An Npoco Database object</param>
    void Migrate(IDatabase db);
    /// <summary>
    /// Perform any actions on the database after changing the schema. This might
    /// include copying data back from temporary tables and then cleaning up.
    /// </summary>
    /// <param name="db">An Npoco Database object</param>
    void PostMigrate(IDatabase db);
    /// <summary>
    /// Perform any seeding needed by the database. This might include setting
    /// new column values to a default as well as genuine data seeding
    /// </summary>
    /// <param name="db">An Npoco Database object</param>
    void Seed(IDatabase db);
}

这个 interface 提供了执行特定数据库版本更改的核心功能。每次发布需要进行数据库更改时,都会创建一个新的 IDatabaseConfigurator,它将执行所有需要的更改。

示例实现

SQLite 的一个很好的特性是,当你第一次以任何方式访问数据库时,如果数据库不存在,SQLite 会创建一个空数据库。我使用 SQLite 本身来存储当前的数据库版本号。这是使用 PRAGMA user_version 命令完成的。

当按照约定使用更新过程时,每个 IDatabaseConfigurator 必须使用类名的最后三个字符来提供其版本号(例如,Config000, Config001 等)。初始化器使用反射来定位和实例化配置器,并在完成后释放它们。第二个版本的 InitialiseDatabase 允许你提供自己的预初始化配置器对象列表,这些对象必须按照正确的顺序排列,并且你可以选择负责释放它们。

MyDbInitialiser 将整个更新序列包装在一个事务中,这样数据库要么完全升级,要么在发生错误时根本不升级。

假设需要进行一些数据库更改,应用程序的每个版本都有自己的 IDatabaseConfigurator 。这些允许你除了播种之外,还可以执行迁移前和迁移后任务。提供的示例说明了如何将其连接到 Microsoft Identity 框架以播种 RoleUser 对象。

示例项目是为 VS2017 提供的,初始化器的执行是通过单元测试而不是虚拟应用程序来演示的(请注意,这些不是一组真正的测试,而仅仅是演示初始化器执行的一种方式)。

当使用初始化器时,应该在每次应用程序启动时执行它。对于 MVC 应用程序,一个好的点可能是 Application_Start() 方法

using (var db = new MyDb())
{
    using (var initialiser = new MyDbInitialiser(db))
    {
        initialiser.InitialiseDatabase();
    }
}

注意事项

当将 SQLite 与 System.Data.SQLite 一起使用时,你需要在你的 *config* 文件中添加所需的 DbProviderFactory 条目。

<system.data>
    <DbProviderFactories>
        <add name="SQLite Data Provider" 
        invariant="System.Data.SQLite" 
        description=".NET Framework Data Provider for SQLite" 
        type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
    </DbProviderFactories>
</system.data>

SQLite 有一个习惯,即保持 SQLite.Interop.dll 打开,从而导致构建失败。发生这种情况是因为测试运行器在测试之间保留在内存中。通过使用测试设置来停止将执行引擎保存在内存中来解决此问题。

这个例子不是生产代码,它只是一个如何使用 SQLite 和 NPoco 实现 interface 的例子。

历史

  • 2017 年 6 月 22 日:初始版本
© . All rights reserved.