'CoreData: Is there a way to cleanly delete the backuped DB, after database backup via migratePersistentStore?
Currently, our app provides a backup functionality to user, where she can perform snapshot backup of app database.
We are using migratePersistentStore to achieve such functionality.
After running migratePersistentStore, we will execute other I/O operations. Some of the I/O operations might fail. In such case, we need to completely delete the backup-ed database.
However, we notice it is very difficult to delete the backup-ed database!
We notice at certain point, CoreData will re-create the SQLite file, even though we have perform destroyPersistentStore and FileManager.default.removeItem.
Here's our code snippet.
public static func cloneXXXDatabase(dstUrl: URL, directory: Directory, cloneTrash: Bool) -> Bool {
// Current already opened app database.
let coreDataStack = CoreDataStack.INSTANCE
guard let srcUrl = coreDataStack.persistentContainer.persistentStoreDescriptions.first?.url else { return false }
// New destination to backup current app database.
let coreDataNamedStack = CoreDataNamedStack(srcUrl)
// Have a new NSPersistentStoreCoordinator solely for migration purpose.
let psc = coreDataNamedStack.persistentContainer.persistentStoreCoordinator
// Open the SQLite. Is it fine for 2 different NSPersistentStoreCoordinator to access 1 same SQLite?
guard let srcStore = psc.persistentStore(for: srcUrl) else { return false }
do {
// Reference: https://www.avanderlee.com/swift/write-ahead-logging-wal/
// This is to ensure only 1 SQLite file is produced, without WAL & SHM.
let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]]
try psc.migratePersistentStore(srcStore, to: dstUrl, options: options, withType: NSSQLiteStoreType)
} catch {
error_log(error)
return false
}
// ...
// ... (some other I/O operations)
// ...
var somethingWentWrong = true
if somethingWentWrong {
do {
try psc.destroyPersistentStore(at: dstUrl, ofType: NSSQLiteStoreType)
if dstUrl.exists {
try FileManager.default.removeItem(at: dstUrl)
}
print(">>>> DELETE \(dstUrl)")
//
// WARNING: Such SQLite removal code is not working, and I am not sure why?!
// It seems that after returning from this function, CoreData will still re-create the backup destination
// DB, by logging
//
// CoreData: annotation: Connecting to sqlite database file at ".../xxx.sqlite".
//
} catch {
error_log(error)
}
}
return true
}
Do you have idea, how can we cleanly delete the SQLite file, which is generated via migratePersistentStore? destroyPersistentStore and FileManager.default.removeItem do not seem to work.
Thank you.
Here's the code of CoreDataStack (Core data stack for main app), and CoreDataNamedStack (Core data stack which points to the backup destination)
CoreDataStack (Core data stack for main app)
class CoreDataStack {
static let INSTANCE = CoreDataStack()
private init() {
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.wenote)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
}
CoreDataNamedStack (Core data stack which points to the backup destination)
class CoreDataNamedStack: CoreDataStackable {
let url: URL
init(_ url: URL) {
self.url = url
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "xxx", managedObjectModel: NSManagedObjectModel.wenote)
let storeDescription = NSPersistentStoreDescription(url: url)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
// So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
// persistent store.
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
}
Finding
Instead of executing the following deletion code immediately
var somethingWentWrong = true
if somethingWentWrong {
do {
try psc.destroyPersistentStore(at: dstUrl, ofType: NSSQLiteStoreType)
if dstUrl.exists {
try FileManager.default.removeItem(at: dstUrl)
}
print(">>>> DELETE \(dstUrl)")
} catch {
error_log(error)
}
}
If we delay a few seconds before executing deletion code.
var somethingWentWrong = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if somethingWentWrong {
do {
try psc.destroyPersistentStore(at: dstUrl, ofType: NSSQLiteStoreType)
if dstUrl.exists {
try FileManager.default.removeItem(at: dstUrl)
}
print(">>>> DELETE \(dstUrl)")
} catch {
error_log(error)
}
}
}
Then such removal will success.
My guess is, CoreData has a background thread commit changes to disk. If the deletion happens before the disk changes committed, database file re-creation will happen again.
May I know what is a proper way to solve this, without having an arbitrary/ random delayed code block?
Solution 1:[1]
Observations that I have noted from your code snippets:
- You used the deprecated method on
NSPersistentStoreCoordinator. Apple Documentation - To migrate from srcStore URL to dstStore it will take time for migration using
migratePersistentStore()so, you have to check if the whole migration is completed before deleting the backup-ed database.
To do it you can check the status of migration and destroyPersistentStore ple:
let migrationResult = try psc.migratePersistentStore(srcStore, to: dstUrl, options: options, withType: NSSQLiteStoreType)
if migrationResult {
do {
try psc.destroyPersistentStore(at: dstUrl, ofType: NSSQLiteStoreType)
if dstUrl.exists {
try FileManager.default.removeItem(at: dstUrl)
}
print(">>>> DELETE \(dstUrl)")
//
// WARNING: Such SQLite removal code is not working, and I am not sure why?!
// It seems that after returning from this function, CoreData will still re-create the backup destination
// DB, by logging
//
// CoreData: annotation: Connecting to sqlite database file at ".../xxx.sqlite".
//
} catch {
error_log(error)
}
}
Solution 2:[2]
I am unable to test this on my end, but the destroyPersistentStore method or the removeItem method might want to be called from the main thread. That could be why it works after a delay, as the asyncAfter(deadline: .now() + 2) method runs the code after a delay in the main thread.
You could try this:
var somethingWentWrong = true
DispatchQueue.main.async {
if somethingWentWrong {
do {
try psc.destroyPersistentStore(at: dstUrl, ofType: NSSQLiteStoreType)
if dstUrl.exists {
try FileManager.default.removeItem(at: dstUrl)
}
print(">>>> DELETE \(dstUrl)")
} catch {
error_log(error)
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | msusare |
| Solution 2 |
