Updating Coredata using threads - swift

I am trying to update an entity in Coredata using the background/thread. I am passing two variables in the below function, one is date and the other one is Account that has 'to one' relationship with the transaction attribute. I am getting an error when I execute the below code.
The goal is to perform updates to coredata without freezing the tableview and other controls/views.
ERROR - *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'account' between objects in different contexts
public class Transaction: NSManagedObject {
#NSManaged public var transDate: Date?
#NSManaged public var account: Account?
class func addTransaction(transDate : Date, transAccount : Account){
let appDelegate = NSApplication.shared.delegate as! AppDelegate
appDelegate.persistentContainer.performBackgroundTask({ (context) in
let entity = NSEntityDescription.entity(forEntityName: "Transaction", in: context)
let CD = Transaction(entity: entity!, insertInto: context)
CD.transDate = transDate //updated successfully
CD.account = transAccount//Gets an error while performing this line of code.
do {
try context.save()
}
catch {
print("error in saving Transaction data")
}
})
}
}

You can not use objects from different context together, the transaction object is created in your background context but the account object is from your main context.
The solution is to get the corresponding account object from the background context using the objectID which is always the same between contexts.
let account = context.existingObject(with: transAccount.objectID)) as? Account
CD.account = account
Note that account is optional here so you need to add some error handling in case it is nil, this should never happen but still it needs to be handled.

Related

await withTaskGroup with Core Data sometimes fails

I have the following taskGroup, using the async/await concurrency in Swift 5.5, where I iterate over meters, fetching onboardingMeter from Core Data. This works ok in around 4 out of 5 times, but then crashes the iOS app. I am testing this with deleting the app from the iOS device, and run the onboarding from the start. Sometimes I can run this with no problems 4 or 5 or 6 times in a row, then it suddenly crashes with the following error:
2021-09-05 09:11:01.095537+0200 Curro[12328:1169419] [error] error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[__NSCFSet addObject:]: attempt to insert nil with userInfo (null)
CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[__NSCFSet addObject:]: attempt to insert nil with userInfo (null)
2021-09-05 09:11:01.095954+0200 Curro[12328:1169419] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFSet addObject:]: attempt to insert nil'
*** First throw call stack:
(0x1857b105c 0x19dc63f64 0x1858ba538 0x1858c5df8 0x1857c68bc 0x18ce313d8 0x18cd13314 0x18cd140cc 0x105616700 0x105626a90 0x185769ce4 0x185723ebc 0x1857373c8 0x1a0edd38c 0x1880dae9c 0x187e58864 0x18d36da2c 0x18d29ab70 0x18d27bf2c 0x1048bba04 0x1048bbab0 0x10551da24)
libc++abi: terminating with uncaught exception of type NSException
This is the code where it fails, the let onboardingMeter = await OnboardingMeter.fromMeterDict is never returned, I don't see the second print statement when it fails.
Is this an error in the Swift beta, or have I made any mistake here? I will try again when a new iOS 15 beta arrives, and when the official release of iOS 15 comes out.
await withTaskGroup(of: OnboardingMeter.self) { group in
for (number, codableAddress) in meters {
group.addTask {
print("The following statement sometimes fails to return an onboarding meter, resulting in a crash")
let onboardingMeter = await OnboardingMeter.fromMeterDict(number, address: codableAddress, in: moc)
print("onboardingMeter:", onboardingMeter)
return onboardingMeter
}
}
for await meter in group {
onboardingMeters.append(meter)
}
}
The OnboardingMeter.fromMeterDict function:
extension OnboardingMeter {
static func fromMeterDict(_ number: String, address: CodableAddress, in moc: NSManagedObjectContext) async -> OnboardingMeter {
let me: OnboardingMeter = await moc.findOrCreateInstance(of: self, predicate: NSPredicate(format: "number == \(number)"))
let address = await Address.createOrUpdate(from: address, in: moc)
await moc.perform {
me.address = address
me.number = number
}
return me
}
}
and findOrCreateInstance:
func findOrCreateInstance<T: NSManagedObject>(of type: T.Type, predicate: NSPredicate?) async -> T {
if let found = await self.findFirstInstance(of: type, predicate: predicate) {
return found
} else {
return T(context: self)
}
}
Although I'd need to see more code to confirm this, I believe that you're returning a managed object that is already registered to a managed object context. It is only valid to refer to such registered objects within the closure of a perform call. If you need to refer to a managed object between different execution contexts, either make use of the object ID and refetch as needed, or make use of the dictionary representation option of the fetch request:
let request: NSFetchRequest = ...
request.resultType = NSManagedObjectIDResultType // or NSDictionaryResultType
return request.execute()
I'd strongly encourage you to watch this video from WWDC. At the minute 10:39 they're talking exactly about this.

Inserting child records is slow in coredata

I have close to 7K items stored in a relation called Verse. I have another relation called Translation that needs to load 7K related items with a single call from a JSON file.
Here is my code:
let container = getContainer()
container.performBackgroundTask() { (context) in
autoreleasepool {
for row in translations{
let t = Translation(context: context)
t.text = (row["text"]! as? String)!
t.lang = (row["lang"]! as? String)!
t.contentType = "Verse"
t.verse = VerseDao.findById(row["verse_id"] as! Int16, context: context)
// this needs to make a call to the database to retrieve the approparite Verse instance.
}
}
do {
try context.save()
} catch {
fatalError("Failure to save context: \(error)")
}
context.reset()
}
Code for the findById method.
static func findById(_ id: Int16, context: NSManagedObjectContext) -> Verse{
let fetchRequest: NSFetchRequest<Verse>
fetchRequest = Verse.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "verseId == %#", id)
fetchRequest.includesPropertyValues = false
fetchRequest.fetchLimit = 1
do {
let results =
try context.fetch(fetchRequest)
return results[0]
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
return Verse()
}
}
This works fine until I add the VerseDao.findById, which makes the whole process really slow because it has to make a request for each object to the Coredata database.
I did everything I could by limiting the number of fetched properties and using NSFetchedResultsController for data fetching but no luck.
I wonder if there's any way to insert child records in a more efficient way? Thanks.
Assuming your store type is persistent store type is sqlite (NSSQLiteStoreType):
The first thing you should check is whether you have an Core Data fetch index on the Verse objects verseId property. See this stack overflow answer for some introductory links on fetch indexes.
Without that, the fetch in your VerseDao.findById function may be scanning the whole database table every time.
To see if your index is working properly you may inspect the SQL queries generated by adding -com.apple.CoreData.SQLDebug 1 to the launch arguments in your Xcode scheme.
Other improvements:
Use NSManagedObjectContext.fetch or NSFetchRequest.execute (equivalent) instead of NSFetchedResultsController. The NSFetchedResultsController is typically used to bind results to a UI. In this case using it just adds overhead.
Don't set fetchRequest.propertiesToFetch, instead set fetchRequest.includesPropertyValues = false. This will avoid fetching the Verse object property values which you don't need to establish the relation to the Translation object.
Don't specify a sortDescriptor on the fetch request, this just complicates the query

How to deal with concurrency on core data

I've been wandering around google, stackoverflow and internet trying to understand how to work with core data and deal with the concurrency.
Consider that we have 2 tables, Events and Rooms.
An Event can have 1+ Rooms.
FunctionA - AddEvent
FunctionB - AddRoom
FunctionC - SearchRoom -> returns RoomEntity or nil
My problem, I keep getting these errors
Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x10a507160) for NSManagedObject (0x1092f00c0) with objectID '0xd000000000040000 <x-coredata://A34C65BD-F9F0-4CCC-A9FB-1B1F5E48C70E/Rooms/p1>' with oldVersion = 116 and newVersion = 124 and old object snapshot = {\n location = Lisboa;\n name = \"\\U00cdndico LX\";\n} and new cached row = {\n location = Lisboa;\n name = \"\\U00cdndico LX\";\n}"
Notice the information of the Rooms is equal
my approach is the following.
1- I call the webservice once ( it brings a json with data of 3 types of Events ) These 3 all have the same json structure and share the same managedObjectContext passed by parameter
2- I create a managedObject
var managedObjectContext: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext = persistentContainer.viewContext
managedObjectContext.parent?.mergePolicy = NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType
3-
managedObjectContext.perform(
{
do
{
try self.deleteAllEventsFromDb()
FunctionA(eventList, managedObjectContext) -> save
FunctionA(eventList2, managedObjectContext) -> save
FunctionA(eventList3, managedObjectContext) -> save
self.DatabaseDispatchGroup.enter()
try managedObjectContext.save()
self.DatabaseDispatchGroup.leave()
completion(Result.Success(true))
}
catch let error as NSError
{
print("Could not save. \(error), \(error.userInfo)")
completion(Result.Success(false))
}
})
4- For each Event I execute the same FunctionA to create and save the data in database (managedObjectContext.insert(eventEntity)) . This will work over several tables but lets only consider Events and Rooms(FunctionB).
5- FunctionA contains functionB. Function B search for an existing Room(FunctionC->returns entity?) if it doesn't exists(nil), it creates the entity ( should I save here? )
6- If a Room exists, gets the entity and tries to update the data
Not sure if its making any difference but when I save I do these saves I do it between a dispatchGroup
DatabaseDispatchGroup.enter()
try managedObjectContext.save()
DatabaseDispatchGroup.leave()
I was using a static managedObjectContext which was used for all the database requests but now I decided to create a objectContext per function which accesses the database.
I do keep the same persistentContainer and the same DispatchGroup for all requests
private override init() {
persistentContainer = NSPersistentContainer(name: "DataModel")
persistentContainer.loadPersistentStores() { (description, error) in
if let error = error {
fatalError("Failed to load Core Data stack: \(error)")
}
}
}
It seems to me that my problem is that I am Storing the changes in memory, and always doing updates over the initial data meaning that when I execute save() the context the data is not updated for the next operation?
How/when am I suppose to execute the save function?
Thank you
Once context is being saved, global notification is being posted: ContextDidSave notification.
When using multiple contexts (and not using parent-child approach) you should use this notification and either:
Re-fetch/refresh data in case you need to update view or perform some operation on new data set (using either fetch request or refreshObjects: API).
Merge changes to other contexts (remember about thread confinement! do that only on proper context queues). (merge doc)
There are many articles about it, check for instance this tutorial
and documentation

Cannot save strings into CoreData

I'm trying to save strings I have in my string array into the Core Data. My .xcdatamodel looks like this:
My saving function (a method of a class called "Memory"):
func save(from: [String])
{
for i in 0..<from.count
{
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let saved = NSEntityDescription.insertNewObject(forEntityName: "Person", into: context)
saved.setValue(from[i], forKey: "password")
do
{
try context.save()
print("SAVED")
}
catch
{
print("ERROR - COULDN'T SAVE ", to)
}
}
print("NEW ", to, ": ")
print(save)
}
Lastly, inside my ViewController:
Memory().save(from: codes)
However, what I get is this:
Thread 1: Fatal error: Unresolved error Error
Domain=NSCocoaErrorDomain Code=134140 "Persistent store migration
failed, missing mapping model."
UserInfo={sourceModel=() isEditable
1, entities {
Person = "() name Person,
managedObjectClassName NSManagedObject, renamingIdentifier Person,
isAbstract 0, superentity name (null), properties {\n password =
\"(), name password, isOptional
1, isTransient 0, entity
You have made changes to your data model but you failed/forgot to migrate the model. If you don't have anything valuable in your current persistence store (SQLite database?) then I suggest you throw it away and let Core Data create a new one using the new model.
Otherwise you might want to fetch the previous version of your model from your source repository if you have one and do a proper migration. Here is a SO question of interest and Apple's documentation on migration

Primary key property has duplicate values after migration - Realm migration

I have an existing Realm project with two models
Model1 -> id, updateDate, details
Model2 -> id, updateDate, title, model1
(yes I used a class object instead of the id - aargh. Both id are primary keys)
With an updated version of my app, I am adding a new property to Model1 (title) and changing Model2.model1 from type Model1 to type string (=Model1.id)
I wrote a migration block for this as per the samples provided
let migrationBlock: (RLMMigration, UInt64) -> Void = { (migration, oldSchemeVersion) in
if oldSchemeVersion < 1 {
migration.enumerateObjects(Model1.className()) { oldObject, newObject in
//Nothing needed, the title can be a blank
}
migration.enumerateObjects(Model2.className()) { oldObject, newObject in
if let oldModel1 = oldObject!["model1"] as? RLMDynamicObject {
newObject!["model1"] = oldModel1["id"]
}
}
}
}
let config = RLMRealmConfiguration.defaultConfiguration()
config.schemaVersion = newSchemaVersion
config.migrationBlock = migrationBlock
RLMRealmConfiguration.setDefaultConfiguration(config)
But at the end of the migration, when I try to access the default realm (Realm.defaultRealm), it fails with this error
Terminating app due to uncaught exception 'RLMException', reason: 'Primary key property 'id' has duplicate values after migration.'
I cannot figure out why this is going wrong and what I am supposed to do to make this work. Any help would be appreciated.
NOTE - my code uses the Realm objective-c code but in a Swift app