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

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.

Related

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

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!)

How to get min/max Values of Realms LinkingObjects

What is the most efficient way to get the overall max speed over all Customers/Cars.
// 157087 Entries (already filtered)
class Customer: Object {
//...
let cars = LinkingObjects(fromType: Car.self, property: "customer")
}
// 2537950 Entries for the 157087 filtered Customers
class Car: Object {
//...
#objc dynamic var customer: Customer?
}
// results here are already filtered (For simplicity i kept the additional filters away)
var results : Results<Customer> = realm.filter("age > 18")
// takes several seconds
let maxSpeed = results.map { $0.cars.max(ofProperty: "speed") as Double? }.max() ?? 0
is there a better way to do so? Just for benchmarking I tried the other way around. It takes just as long
// need to add a `IN` clause because of the pre filtered results (see above)
let cars : Results<Car> = realm.objects(Car.self).filter("customer IN %#", results)
let maxSpeed = cars.max(ofProperty: "speed") as Double?
Super question; clear, concise and property formatted.
I don't have a 2 million row dataset but a few things that may/may not be a complete answer:
1)
This could be bad
results.map { $0.cars.max(ofProperty: "speed") as Double? }.max()
Realm objects are lazily loaded so as long as you work with them in a Realm-ish way, it won't have a significant memory impact. However, as soon as Swift filters, maps, reduce etc. are used ALL of the objects are loaded into memory and could overwhelm the device. Considering the size of your dataset, I would avoid this option.
2)
This is a good strategy as shown in your question as it it treats it as Realm objects, memory won't be affected and is probably your best solution
let max: Double = realm.objects(Car.self).max(ofProperty: "speed") ?? 0.0 //safely handle optional
print(max)
3)
One option we've used in some use cases is ordering the object and then grabbing the last one. Again, it's safer and we generally do this on a background thread to not affect the UI
if let lastOne = realm.objects(Car.self).sorted(byKeyPath: "speed").last {
print(lastOne.speed)
}
4)
One other thought: add a speed_range property to the object and when it's initially written update that as well.
class Car: Object {
#objc dynamic var speed = 0.0
#objc dynamic var speed_range = 0
}
where speed_range is:
0: 0-49
1: 50-99
2: 100-149
3: 150-199
4: 200-249
etc
That would enable you to quickly narrow the results which would dramatically improve the filter speed. Here we want the fastest car in the speed_range of 4 (200-249)
let max: Double = realm.objects(Car.self).filter("speed_range == 4").max(ofProperty: "speed") ?? 0.0
print(max)
You could add a calculated property backed Realm property to the Car object that automatically set's it to the correct speed_range when the speed is initally set.

Swift - Detecting whether item was inserted into NSMutableSet

This is more for interest rather than a problem, but I have an NSMutableSet, retrieved from UserDefaults and my objective is to append an item to it and then write it back. I am using an NSMutableSet because I only want unique items to be inserted.
The type of object to be inserted is a custom class, I have overrode hashCode and isEqual.
var stopSet: NSMutableSet = []
if let ud = UserDefaults.standard.object(forKey: "favStops") as? Data {
stopSet = NSKeyedUnarchiver.unarchiveObject(with: ud) as! NSMutableSet
}
stopSet.add(self.theStop!)
let outData = NSKeyedArchiver.archivedData(withRootObject: stopSet)
UserDefaults.standard.set(outData, forKey: "favStops")
NSLog("Saved to UserDefaults")
I get the set, call mySet.add(obj) and then write the set back to UserDefaults. Everything seems to work fine and (as far as I can see) there don't appear to be duplicates.
However is it possible to tell whether a call to mySet.add(obj) actually caused an item to be written to the set. mySet.add(obj) doesn't have a return value and if you use Playgrounds (rather than a project) you get in the output on the right hand side an indication of whether the set was actually changed based on the method call.
I know sets are not meant to store duplicate objects so in theory I should just trust that, but I was just wondering if the set did return a response that you could access - as opposed to just getting the length before the insert and after if I really wanted to know!
Swift has its own native type, Set, so you should use it instead of NSMutableSet.
Set's insert method actually returns a Bool indicating whether the insertion succeeded or not, which you can see in the function signature:
mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element)
The following test code showcases this behaviour:
var set = Set<Int>()
let (inserted, element) = set.insert(0)
let (again, newElement) = set.insert(0)
print(inserted,element) //true, 0
print(again,oldElement) //false,0
The second value of the tuple returns the newly inserted element in case the insertion succeeded and the oldElement otherwise. oldElement is not necessarily equal in every aspect to the element you tried to insert. (since for custom types you might define the isEqual method in a way that doesn't compare each property of the type).
You don't need to handle the return value of the insert function, there is no compiler warning if you just write insert like this:
set.insert(1)

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
}

Best way to display swiftyjson parsed data?

After already having parsed my data through swiftyjson I am looking to display the parsed information in a uitableview. Let's say I have a list of names that I parsed out of the json file. I would like those names to display each in their own respective cells to allow the user to view them all.
I would also assume that if they selected the selected cells which are being displayed strings now after being parsed that I can actually have those stored into a core data object storing them as returnable strings? The thing is I used the .append function onto an array that I created we'll call names = [String]() to actually allow the values to actually come in order for the cells.
But when doing this I noticed a huge spike in cpu usage doing this as it keeps appending them in order lets say first name is matt second name is john and so forth then it creates a new situation of the array every time. so the first time it reads just matt then the next println would print out matt, john then the next one prints out matt, john, tom and so forth until the list of json names is completed.
If I have 500 names or more it takes the cpu awhile to process all those names into the array. I know there has to be another way but all references I see all use the .append method. So my code would be names.append(Name). Name is a parsed string from swiftyjson. From there I am actually able to populate the tableview which is fine it displays correctly in the end but it takes the device awhile to populate as it first needs to process adding all 500 names each time the parse is called. I am hoping to allow the 500 names be rechecked every time in case new ones have been added. This all stores currently as a local json file already.
The code I am using is below I can figure out the UItableview part but it's more a question of whether appending an array is the best and most efficient way of displaying the parsed data from swiftyjson.
let path = NSBundle.mainBundle().pathForResource("List", ofType: "json")
let jsonData = NSData(contentsOfFile: path!, options: NSDataReadingOptions.DataReadingMappedIfSafe, error: nil)
let json = JSON(data: jsonData!)
for (key, subJson) in json["array"] {
if let Name = subJson["Name"].string {
NameList.append(Name)
println(NameList)
}
}
maybe the Swift map function is more effective
…
let json = JSON(data: jsonData!)
var nameList = [String]()
if let subJson = json["array"] as [Dictionary<String,NSObject>]? {
nameList = subJson.map { $0["name"] as! String }
}