Skip to content

Instantly share code, notes, and snippets.

@makadev
Last active March 7, 2021 16:39
Show Gist options
  • Save makadev/8b9ba12231cfabcef776499fe0c342a5 to your computer and use it in GitHub Desktop.
Save makadev/8b9ba12231cfabcef776499fe0c342a5 to your computer and use it in GitHub Desktop.
Swift 3 Realm (realm.io) Database Helper Singleton with exclusive queue and lightweight configuration
import Foundation
import RealmSwift
class RealmDB {
//MARK:- Migration Information
static let currentSchema: UInt64 = 1;
/**
Migration Handler
- parameter migration: Migration Instance
- parameter oldSchemaVersion: old Schema Version
- Note: Yes, this is for your migration code...
*/
class func migration(_ migration: Migration, _ oldSchemaVersion: UInt64) -> Void {
// Implement Me
}
//MARK:- Singleton Instance Holder and Data
static let sharedInstance: RealmDB = RealmDB();
private var currentConfiguration: Realm.Configuration? = nil;
private var currentRealm: Realm? = nil;
private var database_main_queue = DispatchQueue.init(label: "realmdb.sequential.queue")
private var configuration_lock = DispatchSemaphore.init(value: 1);
//MARK:- Additional Types
enum DatabaseError : Error {
case NotConfigured
}
//MARK:- Realm Queue Operations
/**
Enqueue a synchronous executed block. The block will get it's own Realm Instance for the current Thread/Block
with the current Configuration and will be executed in an autoreleasepool.
- parameter task: block to be executed
- Throws: `DatabaseError.NotConfigured` if database was not opened with `connectDBFor` or closed before that call,
additionally passes any exception raised in the block `task`.
- important: Don Not Microtask. This is not meant to be called frequently as it creates a Realm Instance for the Block
and an autorelease pool to reduce memory consumption slightly. Try to read/write as much at once as possible instead of
using multiple updates.
- remark: As long as RealmDB is *open*, it holds a strong reference to the initial Realm Instance which should keep
realm instance creation penalty low.
*/
func realmQueueSync( task:(_ realm: Realm) throws -> Void ) throws {
try database_main_queue.sync() {
RealmDB.sharedInstance.configuration_lock.wait();
if let conf = RealmDB.sharedInstance.currentConfiguration {
RealmDB.sharedInstance.configuration_lock.signal();
try autoreleasepool {
let realm = try! Realm.init(configuration: conf);
try task(realm);
}
} else {
RealmDB.sharedInstance.configuration_lock.signal();
throw DatabaseError.NotConfigured
}
}
}
/**
Enqueue an asynchronous executed block. The block may get it's own Realm Instance for the current Thread/Block
with the current Configuration as long as Database access does not fail. Realm Instance creation and
the task will be executed in an autoreleasepool.
- parameter task: block to be executed, this block will get `realm=nil` if either no configuration is given or
realm instance creation failed.
- important: Don Not Microtask. This is not meant to be called frequently as it creates a Realm Instance for the Block
and an autorelease pool to reduce memory consumption slightly. Try to read/write as much at once as possible instead of
using multiple updates.
- remark: As long as RealmDB is *open*, it holds a strong reference to the initial Realm Instance which should keep
realm instance creation penalty low.
*/
func realmQueueAsync( task: @escaping (_ realm: Realm?)->Void ) {
database_main_queue.async() {
autoreleasepool {
RealmDB.sharedInstance.configuration_lock.wait();
guard let conf = RealmDB.sharedInstance.currentConfiguration else {
RealmDB.sharedInstance.configuration_lock.signal();
task(nil);
return;
}
RealmDB.sharedInstance.configuration_lock.signal();
if let realm = try? Realm.init(configuration: conf) {
task(realm);
} else {
task(nil);
}
}
}
}
/**
Wait for currently queued Blocks on RealmDB Queue to finish.
*/
func realmQueueWait() {
database_main_queue.sync() {
// pass
}
}
//MARK:- DB Connect/Disconnect
/**
Open/Connect to Realm
- parameter fn: File URL for the Realm File
- parameter cryptoKey512bit: 512bit/64byte Key for Encryption
- parameter recreateOnKeyMissmatch: If true, try to delete and recreate the Database on error (assuming errors are produced by Key missmatch).
- Todo: Needs a check wether an Error is caused by wrong key (database missmatch) or not.
- Precondition: optional cryptoKey512bit needs to be either nil or a 64byte block as needed for Realm.Configuration.encryptionKey
- Warning: Do not call this synchronously from within the RealmDB Queue - like `realmQueueAsync`/`realmQueueSync`
scheduled task or `realmQueueWait` - or it will Deadlock.
*/
func connectDBFor(fn: URL?, cryptoKey512bit: Data?, recreateOnKeyMissmatch: Bool = true) {
if let key = cryptoKey512bit {
if (key.count != (512/8)) {
fatalError("Crypto Key Length Missmatch");
}
}
// "disconnect"
disconnectDB();
// "lock" for configuration modifiations
RealmDB.sharedInstance.configuration_lock.wait();
defer{
RealmDB.sharedInstance.configuration_lock.signal();
}
// setup
currentConfiguration = Realm.Configuration.init(fileURL: fn, encryptionKey: cryptoKey512bit, schemaVersion: type(of: self).currentSchema, migrationBlock: { (m, v) in
type(of: self).migration(m, v);
});
// first: try to open Database with given key assuming Database is crypted with that key
do {
currentRealm = try Realm.init(configuration: currentConfiguration!);
} catch let e as NSError {
// print("Error opening DB: \(e)");
if(recreateOnKeyMissmatch) {
// delete database
if let dbfn = currentConfiguration?.fileURL {
try? FileManager.default.removeItem(at: dbfn);
} else {
RealmDB.sharedInstance.currentConfiguration = nil;
}
}
}
if currentRealm == nil && currentConfiguration != nil {
if recreateOnKeyMissmatch {
// second try: recreate Database with new key
do {
currentRealm = try Realm.init(configuration: currentConfiguration!);
return
} catch let e as NSError {
// print("Error opening DB (second try): \(e)");
}
}
fatalError("Database Initialization Failed");
}
}
/**
Try to disconnect the Database after current tasks in Queue.
- parameter force: If true, the configuration and realm instance will be reset immediatly which may result in errors on the
current scheduled blocks. If false, it will append a block (synchronous) to the Queue which removes the references after the
current queued blocks are executed.
- important: This only removes the references handled by this class, not any other so it won't actually close
the Database as long as other references keep the Realm alive.
- Warning: Do not call this synchronously with `force=false` from within the RealmDB Queue - like `realmQueueAsync`/`realmQueueSync`
scheduled task or `realmQueueWait` - or it will Deadlock.
*/
func disconnectDB(Now force: Bool = false) {
if (force) {
RealmDB.sharedInstance.configuration_lock.wait();
RealmDB.sharedInstance.currentConfiguration = nil;
RealmDB.sharedInstance.currentRealm = nil;
RealmDB.sharedInstance.configuration_lock.signal();
} else {
database_main_queue.sync() {
RealmDB.sharedInstance.configuration_lock.wait();
RealmDB.sharedInstance.currentConfiguration = nil;
RealmDB.sharedInstance.currentRealm = nil;
RealmDB.sharedInstance.configuration_lock.signal();
}
}
}
}
@makadev
Copy link
Author

makadev commented Dec 18, 2018

Database Helper which provides easy DB Setup and a serial dispatch queue for Database Operations regardless of the executing thread. Be aware that Operations might be quite slow due to Mutual Exclusion and recreation of the the Realm Instance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment