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





5.00/5 (2投票s)
如何初始化数据库,然后自动保持其与版本发布的同步。
引言
写这篇文章是因为我在创建一个小型博客风格的 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 框架以播种 Role
和 User
对象。
示例项目是为 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 日:初始版本