目录
这篇文章是在我创建一个小型博客风格的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个接口:
/// <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方法,并且应该执行将数据库带到当前版本所需的所有更新。它提供的属性不是必需的,但对于调试或记录数据库更改活动很有用。
/// <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一起使用时,您需要在配置文件中添加所需的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 的示例。
https://www.codeproject.com/Articles/1193110/A-Database-Initialiser-for-SQLite-and-NPoco