Realm best practices: How to handle asynchronous HTTP object update? - ios

I have a model which is a swift object.
I retrieve data from the web and then I need to update my object but there are different cases to handle:
I create an object, fetch the data, update the properties, save it in realm
I create an object, save it in realm, fetch the data, update the properties, save it again
I create an object, save it in realm, start to fetch the data, delete it from realm, receive the data, do nothing.
And this is how I handle it:
If self.invalidated == false & self.realm == nil -> update the properties on self
If self.invalidated == false & self.realm != nil -> Fetch the object from Realm in a background thread, set the properties, Refresh Realm on main thread before completion
If self.invalidated == true -> Stop (object has been deleted so it's not needed anymore)
One solution to simplify this code is to save the object in realm, but I don't want to save an object that could be dirtier than a potential one in realm. Or I could fetch whatever I have in realm before fetching the data online, so that I'm sure I save something at least as dirty as one in realm (but performance is not as optimal as it could be)
Could you give me some insight about what is the cleanest way to handle such a case?
Here is my code at the moment:
func fetchDataOnline(completion:(success : Bool)->()){
let params = ["tmdb_id":self.tmdbId,"lang":kLang]
let tmdbId = self.tmdbId
let invoker = AWSLambdaInvoker.defaultLambdaInvoker()
invoker.invokeFunction("getMovie", JSONObject: params).continueWithBlock { (task) -> AnyObject? in
guard self.invalidated == false else{
DDLogWarn("Movie has been invalidated while fecthing data")
completion(success: false)
return nil
}
if let dic = task.result as? NSDictionary{
var objectToUpdate = self
if self.realm != nil{ //Use new realm instance
guard let newRealmInstance = try! Realm().objectForPrimaryKey(MovieNew.self, key: tmdbId) else{
DDLogError("self.realm not nil but can't find movie in realm")
completion(success: false)
return nil
}
objectToUpdate = newRealmInstance
}
try! Realm().write{
objectToUpdate.setProperties(dic: dic)
objectToUpdate.lastUpdate = NSDate()
}
}
else{ //No dictionary found from result
if let error = task.error{
DDLogError(error.description)
}
DDLogError("Error getting movie")
}
Async.main{
try! Realm().refresh()
completion(success : task.error == nil)
}
return nil
}
}

Given your specific use-case scenarios, I think this looks like the best way to go about doing it. While you could do state tracking within the Realm objects themselves, it's much better to simply track their status against their parent Realm objects and respond to that accordingly.
The main best practice for Realm is mainly to try and minimize the number of write transactions as much as possible., so the only thing I could potentially question here is if it's absolutely necessary to add an object to a Realm before performing the request, only to potentially delete it again before the download is complete. If that's necessary because you're using those objects as placeholders in your UI, then that's perfectly fine.
Either way, all of this is really a matter of opinion. This solution you've put forward is competent and fills all of your requirements, so I'm not sure if it's worth trying to find something better. :)

Related

How can I make multiple calls of NSBatchUpdateRequest within DB transaction so that either all rows is updated or none is updated?

Is there a way, to make multiple NSBatchUpdateRequest calls executed within a DB transaction, so that either all DB rows is updated or none is updated (When exception thrown)?
The following code illustrate the problem.
func debug() {
let coreDataStack = CoreDataStack.INSTANCE
let backgroundContext = coreDataStack.backgroundContext
backgroundContext.perform {
let fetchRequest = NSTabInfo.fetchSortedRequest()
do {
var objectIDs: [NSManagedObjectID] = []
let nsTabInfos = try fetchRequest.execute()
//
// QUESTION: We are updating multiple rows of data directly in a persistent store.
// How can we ensure either all rows is updated, or none row is updated is exception
// happens in between?
//
for nsTabInfo in nsTabInfos {
let batchUpdateRequest = NSBatchUpdateRequest(entityName: "NSTabInfo")
batchUpdateRequest.predicate = NSPredicate(format: "self == %#", nsTabInfo.objectID)
batchUpdateRequest.propertiesToUpdate = ["name": nsTabInfo.name! + "XXX"]
batchUpdateRequest.resultType = .updatedObjectIDsResultType
let batchUpdateResult = try backgroundContext.execute(batchUpdateRequest) as? NSBatchUpdateResult
guard let batchUpdateResultX = batchUpdateResult else { return }
guard let managedObjectIDs = batchUpdateResultX.result else { return }
if let nsManagedObjectIDs = managedObjectIDs as? [NSManagedObjectID] {
objectIDs.append(contentsOf: nsManagedObjectIDs)
}
//
// Simulate some exception
// We notice the first row is updated & rest of the rows are unchanged.
// This leaves our data in inconsistent state.
//
throw "Custom error!!!"
}
if !objectIDs.isEmpty {
let changes = [NSUpdatedObjectsKey : objectIDs]
coreDataStack.mergeChanges(changes)
}
} catch {
backgroundContext.rollback()
error_log(error)
}
}
}
class CoreDataStack {
static let INSTANCE = CoreDataStack()
private init() {
}
private(set) lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "wenote")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
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
}()
private(set) lazy var backgroundContext: NSManagedObjectContext = {
let backgroundContext = persistentContainer.newBackgroundContext()
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return backgroundContext
}()
func mergeChanges(_ changes: [AnyHashable : Any]) {
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: changes,
into: [persistentContainer.viewContext, backgroundContext]
)
}
}
We write a demo code to illustrate the following
Performing NSBatchUpdateRequest multiple times within a loop.
An exception happens in between.
We wishes none of the row in persistent store is updated. However, a row is already updated before the exception thrown.
May I know what technique I can use, which is similar to SQLite transaction feature, so that either all rows is updated, or none of the row is updated when exception happens?
CoreData.framework doesn't open up SQLite level controls to the user, it provides you NSManagedObjectContext.
How does it work in a similar manner?
You pull as many objects in many as you need and do your changes on them.
When you are done with your changes, you do context.save().
In that way, you save all of your changes in one shot.
In all cases, pulling all objects in memory might not be possible or a good idea, so then you need to implement your own solution around how to send all of these changes to disk.
From the NSBatchUpdateRequest docs -
A request to Core Data to do a batch update of data in a persistent store without loading any data into memory.
When you execute this, you are doing the changes in store that you can't roll back. For a large data-set, you can do following -
Say you have to perform a series of updates (5 different steps) on 100k records as an operation.
Start in a background thread, pull objects in memory in batches of 1k at a time.
You can load 1k objects easily in memory, mutate them - go through all of your changes/steps one by one and save these changes on this batch. If this is successful, you move on to the next batch.
In case one intermediate step fails on a batch, you can then use either NSManagedObjectContext.rollback() or NSManagedObjectContext.reset() depending on your implementation.
Here's a popular SO post on the differences between the two in case official docs don't provide enough clarity.

How to get ObjectID and search for specific ObjectID in CoreData in Swift 5?

I am currently working on a project with a multi user system. The user is able to create new profiles which are saved persistently using CoreData.
My problem is: Only one profile can be the active one at a single time, so I would like to get the ObjectID of the created profile and save it to UserDefaults.
Further I was thinking that as soon as I need the data of the active profile, I can simply get the ObjectID from UserDefaults and execute a READ - Request which only gives me back the result with that specific ObjectID.
My code so far for SAVING THE DATA:
// 1. Create new profile entry to the context.
let newProfile = Profiles(context: context)
newProfile.idProfileImage = idProfileImage
newProfile.timeCreated = Date()
newProfile.gender = gender
newProfile.name = name
newProfile.age = age
newProfile.weight = weight
// 2. Save the Object ID to User Defaults for "activeUser".
// ???????????????????
// ???????????????????
// 3. Try to save the new profile by saving the context to the persistent container.
do {
try context.save()
} catch {
print("Error saving context \(error)")
}
My code so far for READING THE DATA
// 1. Creates an request that is just pulling all the data.
let request: NSFetchRequest<Profiles> = Profiles.fetchRequest()
// 2. Try to fetch the request, can throw an error.
do {
let result = try context.fetch(request)
} catch {
print("Error reading data \(error)")
}
As you can see, I haven't been able to implement Part 2 of the first code block. The new profile gets saved but the ObjectID isn't saved to UserDefaults.
Also Party 1 of the second code block is not the final goal. The request just gives you back all the data of that entity, not only the one with the ObjectID I stored in User Defaults.
I hope you guys have an idea on how to solve this problem.
Thanks for your help in advance guys!
Since NSManagedObjectID does not conform to one of the types handled by UserDefaults, you'll have to use another way to represent the object id. Luckily, NSManagedObjectID has a uriRepresentation() that returns a URL, which can be stored in UserDefaults.
Assuming you are using a NSPersistentContainer, here's an extension that will handle the storage and retrieval of a active user Profile:
extension NSPersistentContainer {
private var managedObjectIDKey: String {
return "ActiveUserObjectID"
}
var activeUser: Profile? {
get {
guard let url = UserDefaults.standard.url(forKey: managedObjectIDKey) else {
return nil
}
guard let managedObjectID = persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else {
return nil
}
return viewContext.object(with: managedObjectID) as? Profile
}
set {
guard let newValue = newValue else {
UserDefaults.standard.removeObject(forKey: managedObjectIDKey)
return
}
UserDefaults.standard.set(newValue.objectID.uriRepresentation(), forKey: managedObjectIDKey)
}
}
}
This uses a method on NSPersistentStoreCoordinator to construct a NSManagedObjectID from a URI representation.

Core Data and Concurrency

I am using the performBackgroundTask function to pull data from firebase, compare it with data already stored in Core Data, save new data to Core Data, and call a completion handler when done.
I understand that Core Data is not thread safe but I am trying to do this concurrently.
static func cache(completion: #escaping (Void) -> Void) {
CoreDataHelper.persistentContainer.performBackgroundTask { (context) in
let dispatchGroup = DispatchGroup()
// fetch previously saved Core Data from main thread (1) and filter them (2)
let newsSourceIDs = NewsSourceService.getSaved().filter{$0.isEnabled}.map{$0.id!}
let oldArticleURLs = ArticleService.getSaved().map{$0.url!}
// create firebase database reference
let ref = Database.database().reference()
Constants.Settings.timeOptions.forEach { time in
let timeRef = ref.child("time\(time)minutes")
newsSourceIDs.forEach { newsSourceID in
dispatchGroup.enter()
// pull from Firebase Database
timeRef.child(newsSourceID).observeSingleEvent(of: .value, with: { (snapshot) in
guard let newsSourceDict = snapshot.value as? [String: [String:String]] else {
return
}
newsSourceDict.values.forEach { articleDict in
dispatchGroup.enter()
if oldArticleURLs.contains(articleDict["url"]!) {
dispatchGroup.leave()
return
}
// create article entity with firebase data
let article = Article(context: context)
article.date = articleDict["date"]
article.source = newsSourceID
article.time = Int16(time)
article.title = articleDict["title"]
article.url = articleDict["url"]
article.urlToImage = articleDict["urlToImage"]
dispatchGroup.leave()
}
dispatchGroup.leave()
})
}
}
// when done, save and call completion handler (3)
dispatchGroup.notify(queue: .main) {
do {
try context.save()
completion()
} catch {
fatalError("Failure to save context: \(error)")
}
}
}
}
Fetch from Core Data function:
static func getSaved() -> [Article] {
let fetchRequest: NSFetchRequest<Article> = Article.fetchRequest()
do {
let results = try CoreDataHelper.managedContext.fetch(fetchRequest)
return results
} catch let error as NSError {
print("Could not fetch \(error)")
}
return []
}
Can I fetch Core Data from the main thread during performBackgroundTask?
Should I filter with the high level filter function or using a special batch request (can I do that concurrently?)
How can I use dispatchGroup.notify(queue:) to determine when the creation and saving of Core Data is complete?
Can I fetch Core Data from the main thread during performBackgroundTask?
You can fetch from any thread if you use that method. You can't use the results on the main thread though. NSPersistentContainer provides the viewContext property for use on the main thread.
Should I filter with the high level filter function or using a special batch request (can I do that concurrently?)
I'd do it with a predicate on a normal non-batch request. Either of the ways you mention are possible. It depends on what kind of fetching and filtering you need. A batch request might be good if the fetch and filter takes a long time to run. Filtering the results after the fetch might be good if your filtering rules can't be expressed in a predicate.
How can I use dispatchGroup.notify(queue:) to determine when the creation and saving of Core Data is complete?
Add the notify call after your forEach closure. If you never enter, it'll execute immediately. If you do enter, it will execute when you match each enter with a leave.
One other detail: Your getSaved method should take a managed object context as an argument, and fetch with that context. Otherwise you're mixing contexts here. The performBackgroundTask creates one context, but you're using a different one in getSaved.
Another way to handle concurrency with Core Data(one of the easiest, but not optimized) would be using a "child" managedObjectContext with a concurrencyType of private, setting that new MOC's parent to be the MOC on your main thread.
let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateMOC.parent = persistentManager.managedObjectContext
privateMOC.perform {
do {
try privateMOC.save()
} catch let error as NSError {
}
}
You would perform all of your needed core data actions inside of the .perform closure. When you run privateMOC.save() the changes are pushed up to the parent managedObjectContext on the main thread.

aws dynamodb how to use object mapper with batch get in ios

I need to get ~50 items with their primary keys from dynamodb using ios sdk. i am able to get the items by AWSDynamoDB.defaultDynamoDB().batchGetItem but couldn't figure out if it is possible to use object mapper with the response. Unfortunately objectmapper class in ios doesn't have batchGet function. As far as i know i cant use query in this situation.
Is it possible to use object mapper? If not which one makes more sense: parsing the response to get the desired class instance or calling objectMapper.load on each item?
Currently, AWSDynamoDBObjectMapper does not support the batch get item. You need to load one item at a time if you want to use the object mapper.
I solved it by doing this,
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.defaultDynamoDBObjectMapper()
let task1 = dynamoDBObjectMapper.load(User.self, hashKey: "rtP1oQ5DJG", rangeKey: nil)
let task2 = dynamoDBObjectMapper.load(User.self, hashKey: "dbqb1zyUq1", rangeKey: nil)
AWSTask.init(forCompletionOfAllTasksWithResults: [task1, task2]).continueWithBlock { (task) -> AnyObject? in
if let users = task.result as? [User] {
print(users.count)
print(users[0].firstName)
print(users[1].firstName)
}
else if let error = task.error {
print(error.localizedDescription)
}
return nil
}

Delete duplicated object in core data (swift)

I'm saving objects to core data from a JSON, which I get using a for loop (let's say I called this setup function.
Because the user might stop this loop, the objects saved in core data will be partial. The user can restart this setup function, restarting the parsing and the procedure to save object to core data.
Now, I'm getting duplicated objects in core data if I restart the setup().
The object has an attribute which is id.
I've thought I could fetch first objects that could eventually already exist in core data, save them to an array (a custom type one), and test for each new object to add to core data if already exist one with the same id.
The code used is the following:
if !existingCards.isEmpty {
for existingCard in existingCards {
if id == existingCard.id {
moc.deleteObject(existingCard)
println("DELETED \(existingCard.name)")
}
}
}
...
// "existingCards is the array of object fetched previously.
// Code to save the object to core data.
Actually, the app return
EXC_BAD_ACCESS(code=1, address Ox0)
Is there an easier way to achieve my purpose or what should I fix to make my code work? I'm quite new to swift and I can't figure other solution.
The main purpose is to delete duplicated core data, BTW.
Swift 4 code to delete duplicate object:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Card")
var resultsArr:[Card] = []
do {
resultsArr = try (mainManagedObjectContext!.fetch(fetchRequest) as! [Card])
} catch {
let fetchError = error as NSError
print(fetchError)
}
if resultsArr.count > 0 {
for x in resultsArr {
if x.id == id {
print("already exist")
mainManagedObjectContext.deleteObject(x)
}
}
}
At the end, I managed to make it work.
I had to rewrite my code, because I realized moc.deleteObject() works with a fetch before, which in my previous code wasn't in the same function, but it was in viewDidLoad().
// DO: - Fetch existing cards
var error: NSError?
var fetchRequest = NSFetchRequest(entityName: "Card")
if let results = moc.executeFetchRequest(fetchRequest, error: &error) as? [Card] {
if !results.isEmpty {
for x in results {
if x.id == id {
println("already exist")
moc.deleteObject(x)
}
}
}
} else {
println(error)
}
No more existingCards, the result of the the fetch is now processed as soon as possible. Something isn't clear to me yet, but now my code works. If you have any improvements/better ways, they're welcome.
P.S.: I actually found Apple reference useful but hard to understand because I don't know Obj-C. Often I can figure what the code do, but in swift functions and properties are a bit different.

Resources