Error - objects in different contexts when setting relationship during a batch insert - swift

I have a function used to create an object graph in my app for testing purposes. The data structure is very simple at present with a one-to-many relationship between Patient and ParameterMeasurement entities.
As setup of the test state involves around 800 entries it makes sense to do this as a batch insert which works...until you try and establish the relationship between ParameterMeasurement and Patient (which, in the reciprocal, is a one-to-one) at which point the app crashes with the dreaded "Illegal attempt to establish a relationship 'cdPatient' between objects in different contexts"
I'm struggling to understand why this is happening as both Patient and ParameterMeasurement entities are created using the same managed object context which is passed to the function by the caller.
I've already tried to store the objectID of the Patient (created before instantiating ParameterMeasurement instances) and then creating a local copy of the Patient instance inside the batch insert closure (code in place below but commented out) but this does not resolve the issue. I've also checked my model (all OK, relationships are good), deleted the app and reset the sim but still no joy.
Finally, I've stuck in print statements to check the MOCs associated with both entities at the point of instantiation and the MOC passed to the function. As expected, the memory addresses match which makes it look like the error message is a red herring.
Can anyone point me in the right direction? This seems to have been a common issue in the past (lots of posts 5y+ ago with ObjC but little in Swift) but the examples on don't deal with this specific scenario.
func addSampleData(to context: NSManagedObjectContext) throws {
try addParameterDefinitions(to: context, resetToDefaults: true)
let fetchRequest = ParameterProfile.fetchAll
let profiles = try context.fetch(fetchRequest)
for _ in 1...10 {
let patient = Patient(context: context)
patient.cdName = "Patient \(UUID().uuidString.split(separator: "-")[0])"
patient.cdCreationDate = Date()
// let patientID = patient.objectID
for profile in profiles {
let data: [(Date, Double)] = DataGenerator.placeholderDataForParameter(with: profile)
var idx = 0
let total = data.count
let batchInsert = NSBatchInsertRequest(entity: ParameterMeasurement.entity()) { (managedObject: NSManagedObject) -> Bool in
guard idx < total else { return true }
// let patientInContext = context.object(with: patientID) as! Patient
if let measurement = managedObject as? ParameterMeasurement {
// measurement.cdPatient = patientInContext
measurement.cdPatient = patient
measurement.cdName = profile.cdName
measurement.cdTimestamp = data[idx].0
measurement.cdValue = data[idx].1
}
idx += 1
return false
}
do {
try context.execute(batchInsert)
try context.save()
} catch {
fatalError("Import failed with error: \(error.localizedDescription)")
}
}
}
}
Core Data model definitions:

Having done some more digging on this, it appears that batch inserts cannot be used to add relationships to the persistent store as noted here. I'm guessing its because of the difficulties associated with correctly associating entities during the process - frustrating but not a deal breaker.
For now, I'll revert to individual insertion of entities although I could do the process in 2 passes, i.e. a batch insert of the "basic" properties and a second pass setting the relationships on the inserted entities. It seems like a bit too much effort at this level though and any time saving is likely to be minimal for the extra code complexity (and risk of bugs!)

Related

How can I bulk set the value of some results, while limiting how many I update?

In the code below, I like the simplicity of calling
results.setValue, rather than iterating over an array and calling it a bunch of times.
It seems likely that there is some way to do this without iterating over an array, but I guess really my concern is that it appears to be significantly slower to iterate. In my testing, for 500 results, it took just under 3 milliseconds to update them in bulk, vs. 537 milliseconds to iterate.
Seems like there has got to be a built in way to set the value for a subset of the results. Limiting the results by a count doesn't appear to be supported, due to the lazy nature, but I don't see any simple way to update them in bulk. I could order them by a unique field, and get the 500th and then filter I suppose to get a new result set, but seems like there should be a better way to do it.
var results = realm.objects(CloudUpdate.self).filter("status = %#", "queued")
let limitedResults = updates[0..<500]
try! realm.write {
// this works, except it sets all the results to posted
// results.setValue("posted", forKey: "status")
// I'd like to be able to do
// limitedResults.setValue("posted", forKey: "status")
// or something rather than iterate as below
// -- note that limitedResults gets smaller as we set them
// because of the filter on the results.
while limitedResults.count > 0 {
limitedResults[0].setValue("posted", forKey: "status")
}
}
Realm can do that update using key paths of the object, and the performance over large datasets is very good.
The use case is not clear but if you have a dataset and want to update the first X number of objects, this will do it
Given a person class with a name property
class PersonClass: Object {
#Persisted var name = ""
}
and you want to update the first three names to... "Jay"
let peopleResults = realm.objects(PersonClass.self)
let peopleList = RealmSwift.List<PersonClass>()
peopleList.append(objectsIn: peopleResults[0...2]) //see note
try! realm.write {
peopleList.setValue("Jay", forKey: "name")
}
Keep in mind though, as soon as realm objects are manipulated by high level Swift functions, the performance will degrade and more importantly, those objects are no longer lazily loaded - they are all loaded into memory and could potentially overwhelm the device.
One other thing to note is that Realm has no pre-defined ordering so ensure the results have a .sorted(byKeyPath: if you want to update objects by an order.

Retrieving NSOrderedSet from Core Data and casting it to entity managedObjectSubclasss

Im making a Fitness app to learn Core data, and I have found that I need to let the user store every performed workout as a WorkoutLog item, and in there, there should be a series of ExerciseLogs which represents performances of that exercise (it contains each lift and also a reference to the actual exercise design).
Problem is that after a while i realize that i need to have these ordered, so that the next time i want to show the user their workout, the order that the exercisese were performed should be the same.
So I checked "ordered" in the top right of the image there, and now my code is in dire need of an update. I have tried to read as much as I could about working with NSOrderedSet and how to fetch them from core data and then manipulate them, but I havent really found much of use to me. (I have no experice in objective-c)
For example my code that used to be:
static func deleteWorkoutLog(_ workoutLogToDelete: WorkoutLog) {
guard let exerciseLogsToDelete = workoutLogToDelete.loggedExercises as? Set<ExerciseLog> else {
print("error unwrapping logged exercises in deleteWorkoutLog")
return
}
I get the error: .../DatabaseFacade.swift:84:77: Cast from 'NSOrderedSet?' to unrelated type 'Set' always fails
So what ive learned about sets and core data no longer seems applicable.
Im far from an expert in programming, but im very eager to learn how to get access to the loggedExercises instances.
TLDR; Is there a way to cast NSOrderedSet to something I can work with? How do we usually work with NSManagedSets from core data? Do we cast them to Arrays or MutableSets? I would very much appreciate an example or two on how to get started with retrieving and using these ordered sets!
Thanks
For anyone else wondering how to get started with orderedSets in core data:
After setting my the WorkoutLog.loggedExercises "to-many" relationship to be ordered, I managed to access them through the mutableOrderedSetValue function like this:
static func deleteWorkoutLog(_ workoutLogToDelete: WorkoutLog) {
let orderedExerciseLogs: NSMutableOrderedSet = workoutLogToDelete.mutableOrderedSetValue(forKey: "loggedExercises")
let exerciseLogsToDelete = orderedExerciseLogs.array
for exerciseLog in exerciseLogsToDelete {
guard let exerciseLog = exerciseLog as? ExerciseLog else {
return
}
Works great so far.
And to rearrange the NSOrderedSet I ended up doing something like this:
// Swap the order of the orderedSet
if let orderedExerciseLogs: NSOrderedSet = dataSourceWorkoutLog.loggedExercises {
var exerciseLogsAsArray = orderedExerciseLogs.array as! [ExerciseLog]
let temp = exerciseLogsAsArray[indexA]
exerciseLogsAsArray[indexA] = exerciseLogsAsArray[indexB]
exerciseLogsAsArray[indexB] = temp
let exerciseLogsAsOrderedeSet = NSOrderedSet(array: exerciseLogsAsArray)
dataSourceWorkoutLog.loggedExercises = exerciseLogsAsOrderedeSet
}

.prepare vs. .select

I have a working connection to a database in an iOS10 app, using SQLite.swift.
I want to select details for a specific university where I have an IHE_ID passed in from another view controller.
I would like to just select the row for that specific university, but I can't get a query to work. I can however loop through all the data with a prepare and then choose the one I need from that, which of course is more resource intensive than I need since I already have the specific IHE_ID passed in as anIHE Int from the sending view controller.
connection is working so omitting that code...
do {
let db = try Connection(destinationDBPath, readonly: true)
let IHEs = Table("IHE")
let IHE_ID = Expression<Int>("IHE_ID")
let IHE_Name = Expression<String>("IHE_Name")
let IHE_COE_Url = Expression<String>("IHE_COE_URL")
let IHE_Sector = Expression<Int>("IHE_Sector")
let IHE_COE_Name = Expression<String>("IHE_COE_Name")
for ihe in try db.prepare(IHEs){
if (ihe[IHE_ID] == anIHE){
// build this string, otherwise ignore rest of dataset (doing this because query isn't working)
buildIHE = "Name: \(ihe[IHE_Name])\n"
buildIHE.append("URL: \(ihe[IHE_COE_Url])\n")
// buildIHE.append("Sector: \(ihe[IHE_Sector])\n")
if (ihe[IHE_Sector] == 0) {
buildIHE.append("Sector: Public\n")
} else {
buildIHE.append("Sector: Private\n")
}
buildIHE.append("College of Education Name: \(ihe[IHE_COE_Name])\n")
}
}
print ("Got through building IHE portion of view")
What I'd like to do is use this instead of the for loop.
if let query = IHEs.select(IHE_ID,IHE_Name,IHE_COE_Url,IHE_Sector,IHE_COE_Name).filter(IHE_ID == anIHE){
print("Query successful for \(anIHE) with name \(query[IHE_Name])")
// more actions to build the string I need would then occur
} else {
print("Query has failed or returned nil")
}
Finally, I'll use the selected elements if I can get the query to work.
I think I probably just have something wrong with my syntax on the query, but any help is appreciated.
The line with the "if let query" has this error in Xcode:
Initializer for conditional binding must have Optional type, not 'Table'.
This leads me to think it's something with my use of the .select statement and just new to using SQLite.swift and swift in general.
Last thing is that anIHE comes into this function as an Int, and IHE_ID is Expression as shown in this code. I'm thinking this may be part of the problem.
The Initializer for conditional binding must have Optional type error means that the expression on the right of the if let v = expr statement is not an Optional: there is no point using if let, and the Swift compiler says that you should just write let v = expr.
And indeed, IHEs.select(...).filter(...) returns a non-optional value of type Table.
It is not the database row you expect, because the query has been defined, but not executed yet. After all, you weren't using db: where would the rows be loaded from?
The solution is to bring back the database connection, and load a single row. This is done with the pluck method.
if let ihe = try db.pluck(IHEs.select(...).filter(...)) {
print("Name \(ihe[IHE_Name])")
}

Object has been deleted or invalidated. (Realm)

Please refer Error: Object has been deleted or invalidated. (Realm)
I encounter this error with both 2 cases also.
I try to find the DBProduct before delete, but it also got Error: Object has been deleted or invalidated. Is this wrong? Please help me. I call this method in block of Alert view as Case 2.
let realm = try! Realm()
try! realm.write {
let dbProduct = realm.objectForPrimaryKey(DBProduct.self, key: product.id)
if dbProduct != nil {
realm.delete(dbProduct!)
}
}
Update: This issue happens on iOS8 only, and it is OK on iOS 9.
Normally, that error should only be thrown if you try and access a property of a Realm object that has been deleted, or if you explicitly told its parent Realm object to invalidate.
Like James said, it's quite likely that your product variable there has already been invalidated, in which case trying to call product.id would likely cause that crash.
If that's the case, then the easiest thing to do to fix this would be to avoid using the product variable and instead, simply making a copy of the value of id directly. This way, if the object is deleted/invalidated, you still have its primary key in which you can test to see if it still exists.
On a side note, this code could certainly be made a bit more efficient as well. It's not necessary to perform queries inside write transactions and you should only open a write transaction if there actually was an object to delete (Write transactions are pretty heavy things, so they should be avoided as much as possible).
let productID = product.id //save a copy of the ID in case 'product' gets deleted.
let realm = try! Realm()
let dbProduct = realm.objectForPrimaryKey(DBProduct.self, key: productID)
if dbProduct != nil {
try! realm.write {
realm.delete(dbProduct!)
}
}
I hope that helped!

How to make a serial queue with GCD

I tried to make a serial queue for network operations with GCD like this:
let mySerialQueue = dispatch_queue_create("com.myApp.mySerialQueue", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0))
func myFunc() {
dispatch_async(mySerialQueue) {
do {
// Get object from the database if it exists
let query = PFQuery(className: aClass)
query.whereKey(user, equalTo: currentUser)
let result = try? query.getFirstObject()
// Use existing object or create a new one
let object = result ?? PFObject(className: aClass)
object.setObject(currentUser, forKey: user)
try object.save()
} catch {
print(error)
}
}
}
The code first looks for an existing object in the database.
If it finds one, it updates it. If it doesn't find one, it creates a new one. This is using the Parse SDK and only synchronous network functions (.getFirstObject, .save).
For some reason it seems that this is not executed serially, because a new object is sometimes written into the database, although one existed already that should have been updated only.
Am I missing something about the GCD?
From the documentation on dispatch_queue_attr_make_with_qos_class:
relative_priority: A negative offset from the maximum supported scheduler priority for the given quality-of-service class. This value must be less than 0 and greater than MIN_QOS_CLASS_PRIORITY
Therefore you should be passing in a value less than 0 for this.
However, if you have no need for a priority, you can simply pass DISPATCH_QUEUE_SERIAL into the attr argument when you create your queue. For example:
let mySerialQueue = dispatch_queue_create("com.myApp.mySerialQueue", DISPATCH_QUEUE_SERIAL)