SQLite 数据库的升级框架






4.73/5 (16投票s)
我为升级 SQLite 数据库而开发的简易数据库升级框架……
引言
如果你和我一样,你肯定讨厌编写将现有客户数据库安装升级到最新数据库模式所需的数据库代码。
在网上搜索了一圈后,我得出结论,没有免费且好的解决方案(实际上,我也没有找到任何商业解决方案)可以用来支持这些升级。所以我不得不自己编写一个 :-)
在深入探讨解决方案的架构之前,我必须提及,**我将展示的升级框架不包括对触发器、视图或任何高级约束的支持**(例如 SQLite 中并不真正支持的外键约束)。因此,如果你真的想利用它,我建议你将其从你的模式中删除,或者扩展框架以支持它们。
该框架对简单的表和索引支持得很好,我认为这足以让大多数开发人员觉得这个框架很有用。
那么,话不多说,让我们从一些定义开始。
定义
- 数据库模式 - 描述给定数据库结构的元数据,**不**考虑其包含的实际数据。例如,数据库模式描述表的字段或索引的列。
- 数据库升级模块 - 一个知道如何升级具有 X 数据库模式的数据库并将其转换为具有 Y 数据库模式的数据库的模块。
- 升级路径 - 一系列模块,当它们依次应用时,将数据库从源模式升级到目标模式。例如,如果有一个升级模块知道如何从模式 X 升级到模式 Y,而另一个升级模块知道如何从模式 Y 升级到模式 Z,那么我们可以依次应用它们以从模式 X 升级到模式 Z。
- 最优升级路径 - 系统中的所有升级模块构成一种升级图,其中每个节点都是一个升级模块,如果一个升级模块的目标模式是另一个升级模块的源模式,则弧将一个升级模块连接到另一个升级模块。为了缩短升级时间,我们总是希望在升级图中找到最短路径。在我的框架中,我为此目的使用了一个简单的 BFS 算法。
下图说明了上述定义。
在这张图中,我们看到最初,数据库经历了三个独立的模式升级(T、R 和 W),然后才更改为最终的 Z 模式。此时,开发人员希望为现有客户加快从模式 X 到模式 Z 的转换速度,因此他们选择编写一个单独的升级模块,该模块知道如何直接从模式 X 升级到模式 Z。
使用 x->z 升级模块(橙色标记)从 X 到 Z 的升级路径是最优升级路径,因为它涉及最少数量的升级模块。
框架架构
为了使其有用,升级框架支持两个关键操作:
- 将所有升级模块加载到内存中并创建内存中的升级图。在此阶段,每个升级模块都会查询其“FROM”模式和“TO”模式。每个模式都分配一个单独的标识符,并使用这些标识符作为其节点构建一个图。
- 执行数据库升级。这通过执行以下操作完成:
- 加载并解析我们要升级的数据库文件的数据库模式。
- 找到从数据库文件的数据库模式升级到指定为目标模式的模式所需的最优升级路径(升级模块列表)。
- 将原始数据库文件克隆到临时文件(这将在升级失败时防止对原始数据库的更改)。
- 运行升级路径中的每个升级模块。对于每个升级模块,验证数据库模式在运行结束后是否确实更改为升级模块中定义的“TO”模式。
- 所有升级模块运行完成后,对数据库进行真空操作以对其进行碎片整理。
- 将临时数据库文件重命名为原始数据库文件的名称,并删除原始数据库文件。
以下图表说明了框架架构中的关键参与者:
升级过程的基本构建块是 IDbUpgrader
接口,它代表一个知道如何将具有“From”模式的数据库文件升级到具有“To”模式的数据库文件的单个升级模块。每个升级模块还有一个 Name
属性,用于在此过程中进行进度通知。
/// <summary>
/// This interface should be implemented by all upgrade modules. It provides the
/// information and actions that are required by the upgrade manager module in order
/// to perform DB upgrades.
/// </summary>
public interface IDbUpgrader
{
#region Events
/// <summary>
/// Fired whenever some progress is made in the upgrade module (0-100)
/// </summary>
event DbUpgradeProgressEventHandler DbUpgradeProgress;
#endregion
#region Properties
/// <summary>
/// The DB schema from which the upgrade module performs the upgrade.
/// </summary>
DbSchema FromSchema
{
get;
}
/// <summary>
/// The DB schema to which the upgrade module performs the upgrade.
/// </summary>
DbSchema ToSchema
{
get;
}
/// <summary>
/// The name of the upgrade module module (used to display progress information).
/// </summary>
string Name
{
get;
}
#endregion
#region Methods
/// <summary>
/// Upgrade the specified DB file to my ToSchema schema.
/// </summary>
/// <param name="dbPath">The DB file to upgrade</param>
void Upgrade(string dbPath);
/// <summary>
/// Cancels the upgrade
/// </summary>
void Cancel();
#endregion
}
每个升级模块都支持从升级管理器模块调用的 Upgrade
方法和 Cancel
方法。它还通过 DbUpgradeProgress
事件支持进度通知。
所有自动升级代码都位于 BasicDbUpgrader
类中,该类实现了 IDbUpgrader
类,并提供了各种钩子,派生类可以使用这些钩子来覆盖基本升级模块代码所做的一些(或所有)决策。
基本数据库升级模块类的默认行为是允许将数据库文件模式转换为其“To”模式所需的所有操作。为了支持必要的微调,基本升级模块提供了以下钩子:
AllowIndexAddition
– 返回false
将指示升级模块不添加在调用中指定名称的索引。这允许具体类在调用其DoSpecificUpgrades
方法时稍后添加索引。AllowIndexDeletion
– 返回false
将指示升级模块不删除在调用中指定名称的索引。这允许具体类在调用其DoSpecificUpgrades
方法时稍后删除索引。AllowTableAutoUpgrade
– 返回false
将指示升级模块不对表进行自动升级。这允许具体类在调用其DoSpecificUpgrades
方法时稍后处理升级问题。基本升级模块将尝试选择成本最低的方式升级表——如果可能,使用ALTER TABLE
,否则完全重写现有表。AllowTableCreation
– 返回false
将指示升级模块不创建在调用中指定名称的表。请注意,新表是在没有任何数据的情况下创建的。如果你需要向此类表添加行,可以使用DoSpecificUpgrades
钩子。请注意,当创建表时,其所有索引也会创建,因此你不会收到单独的AllowIndexAddition
钩子调用。AllowTableDeletion
– 返回false
将指示升级模块不删除在调用中指定名称的表。请注意,当删除表时,其所有索引也会删除,因此你不会收到单独的AllowIndexDeletion
钩子调用。AllowTableReplacement
– 如果表的所有原始列都被其他列替换,升级模块则认为该表应该被替换。替换表与删除它然后(根据新模式)重新创建它相同。表中的所有先前信息都将被删除。DoSpecificUpgrades
– 在所有自动升级操作完成后,基本升级模块将调用此方法,以便为具体的升级模块类提供一个钩子,以便在必要时执行更精细的升级操作。例如,有时我们不希望升级模块删除表,因为我们希望使用其数据来填充其他表。在这种情况下,我们将通过在AllowTableDeletion
钩子中返回false
来阻止该表的删除,并且我们将在DoSpecificUpgrades
方法中放置代码,该代码将复制表的内容并最终删除它。
除了支持这些钩子之外,基本升级模块还为从中派生的具体升级模块类提供了一些辅助方法:
CreateIndex
– 请求创建方法参数中指定的索引。CreateTable
– 请求创建方法参数中指定的表。DropIndex
– 请求删除方法参数中指定的索引。DropTable
– 请求删除方法参数中指定的表。ReplaceIndex
– 请求用指定的索引替换现有索引。
在解析 SQL 模式文件或嵌入式数据库模式(使用 *SQLITE_MASTER* 表)时,升级框架除了基本升级模块类之外,还使用以下四个类提供的功能:
DbSchema
类 – 封装数据库的结构。目前,它不支持视图或触发器,只支持表和索引。DbTable
类 – 封装数据库表的结构。它包含列列表、主键列表和表的名称。DbColumn
类 – 封装单个数据库表列的结构。它包含列的名称、其类型和大小描述符以及列的各种约束。如前所述,并非所有约束都受支持,因此如果你想使用升级框架,你需要删除那些不兼容的约束(CHECK
约束不受支持,REFERENCE
约束不受支持,ON CONFLICT
子句也不受支持)。
升级管理器类是负责协调所有升级操作的中心类。它支持以下方法:
SetUpgraders
– 在这里,你提供你想要支持的升级模块列表。该方法将在内部构建一个升级图。该图将用于稍后构建升级路径。UpgradeDB
– 在这里,你提供要升级的 DB 文件的路径以及要升级到的目标模式。请注意,目标模式必须由你的一个升级模块支持(否则,升级管理器将无法构建升级路径)。Cancel
– 在用户想要在升级过程中取消时调用。它将调用所有相关升级模块的Cancel
方法并停止升级过程。
在升级过程中,升级管理器类将通过 DbUpgradeProgress
事件发出进度事件。你的 GUI 可以处理这些事件,以向用户显示升级进度。
使用代码
为了方便使用代码,我附上了一个包含升级引擎库的示例应用程序。随附的解决方案具有以下结构:
- *DB* - 包含数据库的最新 SQL 模式定义文件。
- *Libs* - 包含 SQLite .NET 提供程序 DLL。
- *DbUpgraders* - 一个类库项目,包含示例数据库升级类。
- *SchemaExtractor* - 一个用于提取 SQLite 数据库 SQL 模式定义的实用程序应用程序。然后将 SQL 模式文件用作升级实用程序中的嵌入式资源(请参阅 *DbUpgraders* 库中的代码)。
- *TestApp* - 一个骨架升级实用程序应用程序,经过你的一些修改后,可用于执行必要的数据库升级。
- *UpgradeEngine* - 数据库升级框架(一个类库)。
- *DbVersions* - 一个文件夹,包含每个受支持数据库版本的历史版本。此文件夹在调试数据库升级时非常有用。
最终注释
我已尽力解释 SQLite 数据库升级框架的内部结构和使用方法,但我并非完美无缺。如果你认为我的解释有不足之处,请给我发邮件提出你的问题,我会尽力尽快回答。
历史
- 2009 年 2 月 2 日 - 版本 1.0。