使用 GRDB 禁用或推迟外键强制执行以进行数据库迁移
Disabling or Deferring Foreign Key Enforcement with GRDB for Database Migration
我已经在我的移动应用程序中使用了 GRDB 库。所有当前功能都运行良好,我已经开始实施从早期数据库版本的迁移。为了与我在其他平台上的实现保持一致,我决定使用 SQLite 的 user_version
而不是 GRDB 自己的迁移框架,后者使用自己的 table.
随着 table 的更改、复制、创建和删除,一个迁移步骤(数据库版本)中的更改会相互依赖。由于更改是在事务结束时提交的,这会导致外键违规,并且升级失败。
解决此问题的一种方法是通过推迟(defer_foreign_keys
pragma)它或通过为事务设置 foreign_keys
pragma 暂时禁用它来防止外键强制执行。不幸的是,我对这两种选择都不太满意。经过一些测试后注意到,例如,试图关闭外键检查
config.prepareDatabase { db in
try db.execute(sql: "PRAGMA foreign_keys = OFF")
}
并使用
读取编译指示
try dbQueue.write { db in
print(try Bool.fetchOne(db, sql: "PRAGMA foreign_keys")! as Bool)
}
或通过检查数据库表明 foreign_keys
设置保持打开状态。
我的迁移步骤看起来,稍微简化了,像这样:
if try userVersion() < 2 {
try dbQueue.write { db in
try db.execute(sql: ...)
try db.execute(sql: ...)
...
try db.execute(sql: "PRAGMA user_version = 2")
}
}
if try userVersion() < 3 {
try dbQueue.write { db in
try db.execute(sql: ...)
try db.execute(sql: ...)
...
try db.execute(sql: "PRAGMA user_version = 3")
}
}
我的初始 GRDB 设置如下:
var config = Configuration()
config.foreignKeysEnabled = true
let appSupportDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first!
dbPath = (appSupportDirectory as NSString).appendingPathComponent(dbName)
let fileManager = FileManager.default
if fileManager.fileExists(atPath: dbPath) {
// Just connect to database.
dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
} else {
// Create new database.
dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
// Create tables
...
}
// Database migration steps:
...
在数据库迁移期间防止这些外键失败的最佳方法是什么?为什么我的 pragma 语句不起作用?
引用 https://www.sqlite.org/pragma.html#pragma_foreign_keys:
This pragma [foreign_keys
] is a no-op within a transaction; foreign key constraint enforcement may only be enabled or disabled when there is no pending BEGIN or SAVEPOINT.
GRDB DatabaseQueue.write
方法打开一个事务。所以当你想禁用外键时,你必须执行 manual transaction handling:
// Add the `writeWithDeferredForeignKeys` method to
// DatabaseQueue and DatabasePool.
extension DatabaseWriter {
func writeWithDeferredForeignKeys(_ updates: (Database) throws -> Void) throws {
try writeWithoutTransaction { db in
// Disable foreign keys
try db.execute(sql: "PRAGMA foreign_keys = OFF");
do {
// Perform updates in a transaction
try db.inTransaction {
try updates(db)
// Check foreign keys before commit
if try Row.fetchOne(db, sql: "PRAGMA foreign_key_check") != nil {
throw DatabaseError(resultCode: .SQLITE_CONSTRAINT_FOREIGNKEY)
}
return .commit
}
// Re-enable foreign keys
try db.execute(sql: "PRAGMA foreign_keys = ON");
} catch {
// Re-enable foreign keys and rethrow
try db.execute(sql: "PRAGMA foreign_keys = ON");
throw error
}
}
}
}
try dbQueue.writeWithDeferredForeignKeys { db in
try db.execute(sql: ...)
try db.execute(sql: ...)
}
我对 defer_foreign_keys
还不够熟悉,但上面的示例代码应该可以帮助您找到您想要的解决方案。
我已经在我的移动应用程序中使用了 GRDB 库。所有当前功能都运行良好,我已经开始实施从早期数据库版本的迁移。为了与我在其他平台上的实现保持一致,我决定使用 SQLite 的 user_version
而不是 GRDB 自己的迁移框架,后者使用自己的 table.
随着 table 的更改、复制、创建和删除,一个迁移步骤(数据库版本)中的更改会相互依赖。由于更改是在事务结束时提交的,这会导致外键违规,并且升级失败。
解决此问题的一种方法是通过推迟(defer_foreign_keys
pragma)它或通过为事务设置 foreign_keys
pragma 暂时禁用它来防止外键强制执行。不幸的是,我对这两种选择都不太满意。经过一些测试后注意到,例如,试图关闭外键检查
config.prepareDatabase { db in
try db.execute(sql: "PRAGMA foreign_keys = OFF")
}
并使用
读取编译指示 try dbQueue.write { db in
print(try Bool.fetchOne(db, sql: "PRAGMA foreign_keys")! as Bool)
}
或通过检查数据库表明 foreign_keys
设置保持打开状态。
我的迁移步骤看起来,稍微简化了,像这样:
if try userVersion() < 2 {
try dbQueue.write { db in
try db.execute(sql: ...)
try db.execute(sql: ...)
...
try db.execute(sql: "PRAGMA user_version = 2")
}
}
if try userVersion() < 3 {
try dbQueue.write { db in
try db.execute(sql: ...)
try db.execute(sql: ...)
...
try db.execute(sql: "PRAGMA user_version = 3")
}
}
我的初始 GRDB 设置如下:
var config = Configuration()
config.foreignKeysEnabled = true
let appSupportDirectory = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).first!
dbPath = (appSupportDirectory as NSString).appendingPathComponent(dbName)
let fileManager = FileManager.default
if fileManager.fileExists(atPath: dbPath) {
// Just connect to database.
dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
} else {
// Create new database.
dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
// Create tables
...
}
// Database migration steps:
...
在数据库迁移期间防止这些外键失败的最佳方法是什么?为什么我的 pragma 语句不起作用?
引用 https://www.sqlite.org/pragma.html#pragma_foreign_keys:
This pragma [
foreign_keys
] is a no-op within a transaction; foreign key constraint enforcement may only be enabled or disabled when there is no pending BEGIN or SAVEPOINT.
GRDB DatabaseQueue.write
方法打开一个事务。所以当你想禁用外键时,你必须执行 manual transaction handling:
// Add the `writeWithDeferredForeignKeys` method to
// DatabaseQueue and DatabasePool.
extension DatabaseWriter {
func writeWithDeferredForeignKeys(_ updates: (Database) throws -> Void) throws {
try writeWithoutTransaction { db in
// Disable foreign keys
try db.execute(sql: "PRAGMA foreign_keys = OFF");
do {
// Perform updates in a transaction
try db.inTransaction {
try updates(db)
// Check foreign keys before commit
if try Row.fetchOne(db, sql: "PRAGMA foreign_key_check") != nil {
throw DatabaseError(resultCode: .SQLITE_CONSTRAINT_FOREIGNKEY)
}
return .commit
}
// Re-enable foreign keys
try db.execute(sql: "PRAGMA foreign_keys = ON");
} catch {
// Re-enable foreign keys and rethrow
try db.execute(sql: "PRAGMA foreign_keys = ON");
throw error
}
}
}
}
try dbQueue.writeWithDeferredForeignKeys { db in
try db.execute(sql: ...)
try db.execute(sql: ...)
}
我对 defer_foreign_keys
还不够熟悉,但上面的示例代码应该可以帮助您找到您想要的解决方案。