Last active
January 11, 2021 22:21
-
-
Save SergLam/0935882e12e77d12ef1f10403124952a to your computer and use it in GitHub Desktop.
Realm DataManager - generic operations + migration setup + thread-safe(!!!) read-write async operations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@UIApplicationMain | |
final class AppDelegate: UIResponder { | |
override init() { | |
RealmDAO.shared.configureMigration() | |
super.init() | |
} | |
} | |
// MARK: - UIApplicationDelegate | |
extension AppDelegate: UIApplicationDelegate { | |
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { | |
return true | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
protocol DatabaseCommandRepo { | |
var config: Realm.Configuration { get set } | |
var commandQueue: DispatchQueue { get set } | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum DatabaseError: Error, LocalizedError { | |
case databaseAlreadyInWriteTransaction | |
var description: String { | |
switch self { | |
case .databaseAlreadyInWriteTransaction: | |
return "Database already in a write transaction." | |
} | |
} | |
var errorDescription: String? { | |
return self.description | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
protocol DatabaseQueryRepo { | |
var config: Realm.Configuration { get set } | |
var queryQueue: DispatchQueue { get set } | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
final class MyUserDatabaseCommandRepo: DatabaseCommandRepo { | |
var config: Realm.Configuration | |
var commandQueue: DispatchQueue | |
init(config: Realm.Configuration, queue: DispatchQueue) { | |
self.config = config | |
self.commandQueue = queue | |
} | |
func updateCurrentUser(_ newUser: MyUserJSON, completion: @escaping VoidClosure) { | |
commandQueue.async(flags: .barrier) { | |
autoreleasepool{ | |
do { | |
try RealmDAO.shared.realm.safeWrite { | |
RealmDAO.shared.myUserQueryRepo.getCurrentUser { userObj in | |
switch userObj { | |
case .none: | |
ErrorLoggerService.logWithTrace("Current user is nil") | |
completion() | |
case .some(let user): | |
user.update(user: newUser) | |
RealmDAO.shared.writeUnsafe(value: [user], policy: .all, onCompletion: { | |
completion() | |
}) | |
} | |
} | |
} | |
} catch { | |
ErrorLoggerService.logWithTrace(error: error) | |
completion() | |
} | |
} | |
} | |
} | |
func updateCurrentUserShippingAddress(_ address: ShippingAddressJSON, completion: @escaping VoidClosure) { | |
commandQueue.async(flags: .barrier) { [weak self] in | |
guard let `self` = self else { return } | |
do { | |
let realm = try Realm(configuration: self.config) | |
let userObj = realm.objects(MyUser.self).first | |
switch userObj { | |
case .none: | |
ErrorLoggerService.logWithTrace("Current user is nil") | |
completion() | |
case .some(let user): | |
RealmDAO.shared.writeAsync(obj: user) { realm, userRef in | |
guard let user = userRef else { return } | |
do { | |
try realm.safeWrite { | |
if let addressDao = user.shippingAddreses.first { | |
addressDao.update(address: address) | |
completion() | |
} else { | |
let new = ShippingAddressDAO(address: address) | |
user.shippingAddreses.append(new) | |
completion() | |
} | |
} | |
} catch { | |
let message: String = error.localizedDescription | |
ErrorLoggerService.logWithTrace(message) | |
completion() | |
} | |
} | |
} | |
} catch { | |
let message: String = "Unable to create realm instance \(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
preconditionFailure(message) | |
} | |
} | |
} | |
func updateCurrentUserEmailAddress(_ email: String, | |
completion: @escaping VoidClosure) { | |
commandQueue.async(flags: .barrier) { [weak self] in | |
autoreleasepool{ | |
guard let `self` = self else { return } | |
do { | |
let realm = try Realm(configuration: self.config) | |
let userObj = realm.objects(MyUser.self).first | |
switch userObj { | |
case .none: | |
ErrorLoggerService.logWithTrace("Current user is nil") | |
completion() | |
case .some(let user): | |
RealmDAO.shared.writeAsync(obj: user) { realm, userRef in | |
guard let user = userRef else { return } | |
do { | |
try realm.safeWrite { | |
user.email = email | |
completion() | |
} | |
} catch { | |
let message: String = error.localizedDescription | |
ErrorLoggerService.logWithTrace(message) | |
completion() | |
} | |
} | |
} | |
} catch { | |
let message: String = "Unable to create realm instance \(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
preconditionFailure(message) | |
} | |
} | |
} | |
} | |
func updateCurrentUserDateOfBirth(_ date: Date, completion: @escaping VoidClosure) { | |
commandQueue.async(flags: .barrier) { [weak self] in | |
autoreleasepool{ | |
guard let `self` = self else { return } | |
do { | |
let realm = try Realm(configuration: self.config) | |
let userObj = realm.objects(MyUser.self).first | |
switch userObj { | |
case .none: | |
ErrorLoggerService.logWithTrace("Current user is nil") | |
completion() | |
case .some(let user): | |
RealmDAO.shared.writeAsync(obj: user) { realm, userRef in | |
guard let user = userRef else { return } | |
do { | |
try realm.safeWrite { | |
user.dateOfBirth = date | |
completion() | |
} | |
} catch { | |
let message: String = error.localizedDescription | |
ErrorLoggerService.logWithTrace(message) | |
completion() | |
} | |
} | |
} | |
} catch { | |
let message: String = "Unable to create realm instance \(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
preconditionFailure(message) | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
final class MyUserDatabaseQueryRepo: DatabaseQueryRepo { | |
var config: Realm.Configuration | |
var queryQueue: DispatchQueue | |
init(config: Realm.Configuration, queue: DispatchQueue) { | |
self.queryQueue = queue | |
self.config = config | |
} | |
func getCurrentUser(completion: @escaping TypeClosure<MyUser?>, | |
targetQueue: DispatchQueue = .main) { | |
queryQueue.async { [weak self] in | |
guard let `self` = self else { return } | |
do { | |
let realm = try Realm(configuration: self.config) | |
guard let user = realm.objects(MyUser.self).first else { | |
completion(nil) | |
return | |
} | |
let wrappedObj = ThreadSafeReference(to: user) | |
targetQueue.async { | |
autoreleasepool { | |
do { | |
let realm = try Realm(configuration: RealmDAO.shared.realm.configuration) | |
guard let obj = realm.resolve(wrappedObj) else { | |
completion(nil) | |
return // obj was deleted | |
} | |
completion(obj) | |
} catch { | |
ErrorLoggerService.logWithTrace(error.localizedDescription) | |
completion(nil) | |
} | |
} | |
} | |
} catch { | |
let message: String = "Unable to create realm instance \(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
preconditionFailure(message) | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
final class PredicateBuilder { | |
static let shippingAddressDefaultFieldName = "isDefault" | |
// MARK: - Predicate creation methods | |
static func equalityPredicate(_ fieldName: String, _ value: Any) -> NSPredicate { | |
return NSPredicate(format: "\(fieldName) == %@", argumentArray: [value]) | |
} | |
static func multipleEqualityPredicate(_ fieldsNames: [String], _ values: [Any]) -> NSPredicate { | |
assert(fieldsNames.count == values.count, "Field names and values count should be the same") | |
var predicateFormat = "" | |
for (index, name) in fieldsNames.enumerated() { | |
if index != fieldsNames.count - 1 { | |
predicateFormat.append("\(name) == %@ AND ") | |
} else { | |
predicateFormat.append("\(name) == %@") | |
} | |
} | |
return NSPredicate(format: predicateFormat, argumentArray: [values]) | |
} | |
static func notEqualPredicate(_ fieldName: String, _ value: Any) -> NSPredicate { | |
return NSPredicate(format: "\(fieldName) != %@", argumentArray: [value]) | |
} | |
static func buildPredicate(_ format: String, _ value: [Any]) -> NSPredicate { | |
return NSPredicate(format: format, argumentArray: value) | |
} | |
static func nilPredicate(_ fieldName: String) -> NSPredicate { | |
return NSPredicate(format: "\(fieldName) == nil") | |
} | |
static func notNilPredicate(_ fieldName: String) -> NSPredicate { | |
return NSPredicate(format: "\(fieldName) != nil") | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Realm | |
import RealmSwift | |
extension Realm { | |
/** | |
// https://github.com/realm/realm-cocoa/issues/4511 | |
Terminating app due to uncaught exception 'RLMException', reason: 'The Realm is already in a write transaction' | |
*/ | |
func safeWrite(_ block: (() throws -> Void)) throws { | |
if isInWriteTransaction { | |
try block() | |
} else { | |
try write(block) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Realm | |
import RealmSwift | |
extension Migration { | |
func hadProperty(onType typeName: String, property propertyName: String) -> Bool { | |
var hasPropery = false | |
self.enumerateObjects(ofType: typeName) { oldObject, _ in | |
hasPropery = oldObject?.objectSchema.properties.contains(where: { $0.name == propertyName }) ?? false | |
return | |
} | |
return hasPropery | |
} | |
func renamePropertyIfExists(onType typeName: String, from oldName: String, to newName: String) { | |
if hadProperty(onType: typeName, property: oldName) { | |
renameProperty(onType: typeName, from: oldName, to: newName) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
final class RealmDAO { | |
static let shared = RealmDAO() | |
lazy var myUserQueryRepo: MyUserDatabaseQueryRepo = MyUserDatabaseQueryRepo(config: RealmDAO.shared.configuration, queue: RealmDAO.databaseOperationsQueue) | |
lazy var myUserCommandRepo: MyUserDatabaseCommandRepo = MyUserDatabaseCommandRepo(config: RealmDAO.shared.configuration, queue: RealmDAO.databaseOperationsQueue) | |
lazy var shippingAddressQueryRepo: ShippingAddressDatabaseQueryRepo = ShippingAddressDatabaseQueryRepo(config: RealmDAO.shared.configuration, queue: RealmDAO.databaseOperationsQueue) | |
lazy var shippingAddressCommandRepo: ShippingAddressDatabaseCommandRepo = ShippingAddressDatabaseCommandRepo(config: RealmDAO.shared.configuration, queue: RealmDAO.databaseOperationsQueue) | |
/** | |
Queue to read + write object synchroniously(reader-writer problem) | |
Always use it! | |
*/ | |
private static let databaseOperationsQueue: DispatchQueue = DispatchQueue(label: "\(Environment.bundleId).realm-queue", qos: .userInteractive, attributes: [.concurrent], autoreleaseFrequency: .workItem, target: nil) | |
lazy var migrationExecutor: RealmDatabaseMigrationExecutor = RealmDatabaseMigrationExecutor() | |
lazy var realm: Realm = { | |
do { | |
return try Realm() | |
} catch { | |
let message: String = "Unable to create realm instance \(error)" | |
ErrorLoggerService.logWithTrace(message) | |
preconditionFailure(message) | |
} | |
}() | |
/** | |
Get this configuration each time when you need to perform CRUD operation | |
*/ | |
private var configuration: Realm.Configuration { | |
return realm.configuration | |
} | |
var databaseFileURL: URL? { | |
return realm.configuration.fileURL | |
} | |
private init() { | |
} | |
/** | |
Domain specific operations should be here | |
*/ | |
func performCommand(command: @escaping VoidClosure) { | |
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { | |
command() | |
} | |
} | |
// MARK: - Base CRUD operations implemented with generics | |
/** | |
Use to write NEW UNMANAGED objects to database | |
*/ | |
func writeUnsafe<T: Object>(value: [T], | |
policy: Realm.UpdatePolicy = .all, | |
onCompletion: @escaping VoidClosure) { | |
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { [weak self] in | |
guard let `self` = self else { return } | |
do { | |
let newRealm = try Realm(configuration: self.configuration) | |
let value = value.filter{ return $0.isInvalidated == false } | |
try newRealm.write { | |
newRealm.add(value, update: policy) | |
try newRealm.commitWrite() | |
onCompletion() | |
} | |
} catch { | |
let message: String = "\(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
onCompletion() | |
} | |
} | |
} | |
/** | |
Use to write in background for already existing objects in database | |
*/ | |
func write<T: Object>(value: [T], completion: @escaping VoidClosure) { | |
let isManaged: Bool = value.first?.realm != nil | |
for obj in value { | |
if isManaged { | |
RealmDAO.shared.writeAsync(obj: obj, block: { realm, obj in | |
guard let objReference = obj else { return } | |
realm.add(objReference, update: .all) | |
completion() | |
}) | |
} else { | |
RealmDAO.shared.writeUnsafe(value: value, onCompletion: { | |
completion() | |
}) | |
} | |
} | |
} | |
func readAll<T: Object>(object: T.Type, | |
completion: @escaping ResultClosure<[T]>) { | |
RealmDAO.databaseOperationsQueue.async { [weak self] in | |
guard let `self` = self else { return } | |
do { | |
let realm = try Realm(configuration: self.configuration) | |
let readResults = realm.objects(T.self) | |
completion(.success(Array(readResults))) | |
} catch { | |
let message: String = "Unable to create realm instance \(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
completion(.failure(error)) | |
preconditionFailure(message) | |
} | |
} | |
} | |
func readAllResult<T: Object>(object: T.Type, | |
completion: @escaping ResultClosure<RealmSwift.Results<T>>) { | |
RealmDAO.databaseOperationsQueue.async { [weak self] in | |
guard let `self` = self else { return } | |
do { | |
let realm = try Realm(configuration: self.configuration) | |
completion(.success(realm.objects(T.self))) | |
} catch { | |
let message: String = "Unable to create realm instance \(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
preconditionFailure(message) | |
} | |
} | |
} | |
func deleteAll<T: Object>(_ class: T.Type, completion: @escaping VoidClosure) { | |
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { [weak self] in | |
guard let `self` = self else { return } | |
do { | |
let realm = try Realm(configuration: self.configuration) | |
let allObjects = realm.objects(T.self) | |
try realm.write { | |
realm.delete(allObjects) | |
completion() | |
} | |
} catch { | |
let message: String = error.localizedDescription | |
ErrorLoggerService.logWithTrace(message) | |
completion() | |
} | |
} | |
} | |
/** | |
Always pass predicate, not the actual objects | |
Exception - Can only delete an object from the Realm it belongs to. | |
Because of creating another Realm instance on the writing queue | |
*/ | |
func delete<T: Object>(predicates: [NSPredicate], | |
objectType: T.Type, | |
completion: @escaping VoidResultClosure) { | |
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { [weak self] in | |
guard let `self` = self else { return } | |
do { | |
let realm = try Realm(configuration: self.configuration) | |
self.readAllResult(object: T.self, completion: { result in | |
do { | |
switch result { | |
case .success(let objects): | |
var objToDelete = objects | |
for predicate in predicates { | |
objToDelete = objects.filter(predicate) | |
} | |
try realm.write { | |
realm.delete(objToDelete) | |
} | |
case .failure(let error): | |
completion(.failure(error)) | |
} | |
} catch { | |
ErrorLoggerService.logWithTrace(error.localizedDescription) | |
} | |
}) | |
} catch { | |
let message: String = "Unable to create realm instance \(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
} | |
} | |
} | |
func update<T: Object>(value: [T], completion: @escaping VoidClosure) { | |
for obj in value { | |
RealmDAO.shared.writeAsync(obj: obj) { realm, objRef in | |
guard let safeObj = objRef else { return } | |
do { | |
try realm.write { | |
realm.add(safeObj, update: .modified) | |
completion() | |
} | |
} catch { | |
let message: String = error.localizedDescription | |
ErrorLoggerService.logWithTrace(message) | |
completion() | |
} | |
} | |
} | |
} | |
func deleteAllRecords(completion: @escaping VoidClosure) { | |
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { [weak self] in | |
guard let `self` = self else { return } | |
do { | |
let realm = try Realm(configuration: self.configuration) | |
try realm.write { | |
realm.deleteAll() | |
completion() | |
} | |
} catch { | |
let message: String = "Unable to create realm instance \(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
completion() | |
} | |
} | |
} | |
func isObjectExists<T: Object>(type: T.Type, primaryKey: String) -> Bool { | |
do { | |
let realm = try Realm(configuration: self.configuration) | |
return realm.object(ofType: type, forPrimaryKey: primaryKey) != nil | |
} catch { | |
let message: String = "Unable to create realm instance \(error.localizedDescription)" | |
ErrorLoggerService.logWithTrace(message) | |
preconditionFailure(message) | |
} | |
} | |
// MARK: - Private functions | |
// NOTE: Implementation from original documentation | |
// https://realm.io/docs/swift/latest/#passing-instances-across-threads | |
/** | |
Thread-safe writing to the Realm | |
*/ | |
func writeAsync<T: ThreadConfined>(obj: T, errorHandler: @escaping ((_ error: Swift.Error) -> Void) = { _ in return }, block: @escaping ((Realm, T?) -> Void)) { | |
let wrappedObj = ThreadSafeReference(to: obj) | |
RealmDAO.databaseOperationsQueue.async(flags: .barrier) { | |
autoreleasepool { | |
do { | |
let realm = try Realm(configuration: RealmDAO.shared.realm.configuration) | |
guard let obj = realm.resolve(wrappedObj) else { | |
return // obj was deleted | |
} | |
try realm.safeWrite { | |
block(realm, obj) | |
} | |
} catch { | |
errorHandler(error) | |
} | |
} | |
} | |
} | |
} | |
// MARK: - Database migration functionality | |
extension RealmDAO { | |
func configureMigration() { | |
migrationExecutor.performMigrationIfNeeded() | |
} | |
private func getCurrentDatabaseVersion() -> UInt64 { | |
let configCheck = RealmDAO.shared.realm.configuration | |
guard let fileURL = configCheck.fileURL else { | |
let message: String = "Unable to get file url" | |
ErrorLoggerService.logWithTrace(message) | |
return UInt64(0) | |
} | |
do { | |
let lastSchemaVersion = try schemaVersionAtURL(fileURL) | |
return lastSchemaVersion | |
} catch { | |
// Realm file doesn't exist - realm initial setup | |
return UInt64(0) | |
} | |
} | |
private func deleteDatabase() { | |
let configuration = Realm.Configuration(deleteRealmIfMigrationNeeded: true) | |
Realm.Configuration.defaultConfiguration = configuration | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import RealmSwift | |
final class RealmDatabaseMigrationExecutor: NSObject { | |
private let firstReleaseSchemaVersion: UInt64 = 1000 | |
private let secondReleaseSchemaVersion: UInt64 = 1001 | |
private var currentSchemaVersion: UInt64 { | |
guard let fileURL = Realm.Configuration.defaultConfiguration.fileURL else { | |
return UInt64(0) | |
} | |
do { | |
let lastSchemaVersion = try schemaVersionAtURL(fileURL) | |
return lastSchemaVersion | |
} catch { | |
// Realm file doesn't exist - realm initial setup | |
return UInt64(0) | |
} | |
} | |
deinit { | |
} | |
override init() { | |
super.init() | |
} | |
func performMigrationIfNeeded() { | |
let config = Realm.Configuration(schemaVersion: secondReleaseSchemaVersion, migrationBlock: { migration, oldSchemaVersion in | |
if oldSchemaVersion < self.secondReleaseSchemaVersion && self.currentSchemaVersion != 0 { | |
self.migrateToSecondSchemaVersion(migration: migration, oldSchema: oldSchemaVersion) | |
} | |
}) | |
Realm.Configuration.defaultConfiguration = config | |
} | |
private func migrateToSecondSchemaVersion(migration: RealmSwift.Migration, oldSchema: UInt64) { | |
migration.enumerateObjects(ofType: MyUser.className()) { _, newObject in | |
newObject?["bio"] = "" | |
newObject?["coverImageURL"] = "" | |
newObject?["coverImageThumbURL"] = "" | |
newObject?["eventsCount"] = 0 | |
newObject?["friendsCount"] = 0 | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment