swift 2 core data predicate inconsistent - ios

I am having an issue with core data's predicate method, in that it seems to be somewhat inconsistent in the results it is returning. I have a "Users" entity that is populated from a web API, rather than deleting all the entries then downloading and adding the users, I prefer to set a flag for all users "isModified" to false, then download the users over the API and update or add users depending on whether they are in the store or not, setting the "isModified" flag when they are added/updated. Finally the code deletes any users that did not have their "isModified" flag set. I do it this way because in the event that the web API is not available I don't lose all my users and the app can continue to work.
The problem I am having is that the method that deletes users that haven't had their "isModified" flag set is deleting users that HAVE been updated!
Here's the method:
func deleteUsersNotUpdated() -> Bool {
// default result
var result = false
// setup the fetch request
let fetchRequest = NSFetchRequest(entityName: "Users")
// set a predicate that filters only records with updated set to false
fetchRequest.predicate = NSPredicate(format: "isModified == %#", false)
do {
let fetchResults = try self.managedObjectContext.executeFetchRequest(fetchRequest) as! [NSManagedObject]
for user in fetchResults {
print("Deleting \(user)")
self.managedObjectContext.deleteObject(user)
try self.managedObjectContext.save()
}
result = true
} catch let error as NSError {
print("\(error)")
}
return result
}
The method mostly works, but intermittently will delete a user for no good reason, i.e. even though it has the "isModified" flag set, e.g. here is the output of the line: print("Deleting (user)")
Deleting <NSManagedObject: 0x7b6d6ec0> (entity: Users; id: 0x7b64abd0 <x-coredata://1A7606EB-3539-4E85-BE1C-15C722AD7125/Users/p14> ; data: {
created = "2016-01-17 16:54:21 +0000";
familyName = Doe;
givenName = John;
isModified = 1;
pin = 3932;
type = STAFF;
})
As you can see, the "isModified" flag is very definitely set, yet the predicate is supposed to select only records that have the flag reset (false).
The method above is part of a class I have created, it's basically a CRUD class for the Users entity.
Scratching my head here!
I can supply the rest of the code if required.

I think your code looks perfectly fine (although you should save outside the loop). Swift booleans are automatically converted to NSNumber and back in most situations. There are many valid ways to write this predicate.
The only possible explanation that comes to mind is that another class is using the same managed object context to modify the fetched objects. If you have a single context app this a conceivable scenario.
The standard way to avoid this is to make the changes on a background context.

Use an boolean literal rather than a object placeholder %# for a boolean value
NSPredicate(format: "isModified == FALSE")
and don't call context.save() in each iteration of the repeat loop, call it once after the repeat loop.

Related

Core Data: How best to check uniqueness repeatedly? Save context? Fetch count?

Update: I looked up core data validation(validateForInsert() on managedObhects) upon suggestion but somehow it is not catching the uniqueness constraint violations. I don’t know why because it looks like it should be pretty straightforward. I tried it on both the superclass managedObject type and particular entity types, on both parent and child contexts(on corresponding objects in each context) but nothing works. Although, I only tried it on UIManagedDocument so far. Validation sounds like a much better option but I’m getting frustrated I can’t make it work and haven’t got a clue why.
——
A while back I wrote a func to create default name for core data entities. Little did I know how much trouble it was getting me into… The first iteration of the func can be found in another (solved)question if you are interested(you don’t have to read that question to understand this one): < Generics method with predicated core data search won’t return correct result >
The first iteration was working sort of ok. However, it didn’t consider if a gap exists between the numbers of existing default names. For example, if name, name1 and name3 exist simultaneously in database, a repetition of name3 would be created, and it won’t satisfy where names has to be unique.
So to solve that, I thought I should add a while-loop to check the uniqueness of the newly created names and increase count by 1 in each loop until it was indeed unique. This won’t always fill in the gap, but I just want the names to be unique. I’ll provide the code below but first, the problem I encountered was that it acts “funky.” Sometimes the name is nil when it shouldn’t(no error was caught) or an incorrect name that already exists. So my question is what is the problem with my code?Or can I do to find out what is the problem? Or maybe is there a better way to check for uniqueness other than checking uniqueness error by saving contexts? (Maybe should just fetch and count result instead?) Am I approaching this default name function wrong? I would appreciate any input and discussion.
Here are my codes. The setup is a UIManagedDocument, which automatically provides two contexts, a child and a parent. The parent is a background one. I’m only using the child for edit right now, but I did setup and check both for throwing uniqueness errors. I also set up uniqueness constraints for the attributes.(for simplicity, I omitted the error type I defined and the error type casts related to that.)
func checkUniqueness()throws {
var mergeError: Error? = nil
do{
try childContext.save()
childContext.parent!.performAndWait{
do{
childContext.parent!.save()
}catch{
mergeError = error
print(“parent error”)
}
}catch{
mergeError = error
print(“child error”)
}
if mergeError != nil{
if (mergeError! as NSError).code == 133021 && (mergeError! as NSError).domain == NSCocoaErrorDomain{
throw someUniquenessErrorIDefined
}else{
throw someDataErrorIDefined
}
}
}
//can throw an error indicating func failed and name is set to nil
func setDefaultName<T>(entity:T, defaultString: String, attribute: String) throws {
try? context.save()
//initial values
var name = defaultString
var count = 0
var someError: Error? = nil
//fetch the count
let type = T.self
let fetchRequest = NSFetchRequest<NSNumber>(entityName: “\(type)”)
let pred = NSPredicate(format: “%K CONTAINS[cd] %#", attribute, defaultString)
fetchRequest.predicate = pred
fetchRequest.resultType = .countResultType
do {
let result = try context.fetch(fetchRequest)
count = result.first!.intValue
}catch{
throw someDataErrorIDefined
}
//
//loop to find unique value
repeat{
do{
if count != 0{
name = defaultString + String(count)
}
(entity as! NSManagedObject).setValue(name, forKey: attribute)
count += 1
try checkUniqueness()
someError = nil
}catch{
someError = error
}
} while someError == someUniquenessErrorIDefined
if someError == someDataErrorIDefined{
(entity as! NSManagedObject).setValue(nil, forKey: attribute)
throw someError
}
}
I should add that the funky behaviors disappeared after I relaunched Xcode and I have not been able to reproduce them, but I’m just worried they will come up again. Maybe I shouldn’t check uniqueness this way at all? I just read that uniqueness constraints are the more performant way to insure uniqueness, but I’m not sure they are supposed to be used like this.

Core Data One to Many Relationship Fetching

When retrieving all reviews for a movie (one-to-many) relationship, which snippet of code would you use and why?
static func getReviewsByMovieId(movieId: NSManagedObjectID) -> [Review] {
// OPTION 1
// SQL call to get movie
guard let movie = CoreDataManager.shared.getMovieById(id: movieId),
// SQL call to get reviews for the movie
let reviews = movie.reviews
else {
return []
}
return (reviews.allObjects as? [Review]) ?? []
// OPTION 2
// SQL call to get reviews for a particular movie
let request: NSFetchRequest<Review> = Review.fetchRequest()
request.predicate = NSPredicate(format: "movie = %#", movieId)
do {
return try CoreDataManager.shared.viewContext.fetch(request)
} catch {
return []
}
}
Personally I'd do something more like the first, but it's mostly a matter of style. It's very unlikely to matter to performance unless you're doing this a lot. In that case I'd want to try both and profile the results.
I don't know how your getMovieById(id:) works, but if it's using a fetch request I'd suggest replacing it with either object(with:) or existingObject(with:). Both simply look up an object by ID without needing a predicate. The first is probably slightly faster, and the second is safer, so the choice depends on how certain you can be that the object ID is valid.

Core data - Set many-to-one relationship with NSBatchInsertRequest

I have two entities, User and Post. A User contains many Post, and each Post has only one User.
I am trying to batch insert more than 1000 posts.
private func newBatchInsertRequest(with posts: [PostData]) -> NSBatchInsertRequest {
var index = 0
let total = posts.count
let batchInsert = NSBatchInsertRequest(entity: Post.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < total else { return true }
if let post = managedObject as? Post {
let data = posts[index]
post.createdData = data.createdDate
post.identifier = data.identifier
post.text = data.text
}
index += 1
return false
}
PersistenceController.shared.container.performBackgroundTask { context in
try? context.execute(batchInsert)
try? context.save()
}
}
It inserts all the posts that I want to insert. However, I can not configure how to set their User.
I tried to use the following code, but it did not work.
let updateRequest = NSBatchUpdateRequest(entity: Post.entity())
updateRequest.resultType = .updatedObjectIDsResultType
updateRequest.propertiesToUpdate = ["user": user]
try? context.execute(updateRequest)
I get the following error.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid relationship ((<NSRelationshipDescription: 0x2801045a0>), name item, isOptional 1, isTransient 0, entity Post, renamingIdentifier item
I can set their user one by one, but it is inefficient due to long processing time.
How to update the user property of Posts in a more efficient way?
Update
All of these posts belongs to one User which does not exist yet. I need to create it before or after executing the NSBatchInsertRequest.
User has three properties
1. createdDate: Date
2. identifier: UUID
3. name: String
My goal is to insert Post that belongs to one Use either using NSBatchInsertRequest or in a private context so that it does not block the main thread.
Here's what I would try:
Create the User before your batch operation. Yep, it's going to be on a different context — just make sure you grab its NSManagedObjectID (it's a property on NSManagedObject) and save it on a private property.
Inside your PersistenceController.shared.container.performBackgroundTask you have a reference to your private context. Use the managed object ID from before to fetch/register the same User in your private context:
let user = context.object(with: userManagedObjectID)
Then pass this user into the method that creates your batch insert request to set the user property on your Post objects.
I believe if you set up your inverse relationship up in the data model editor you don't need to populate User.posts. That should happen automatically for you.
I solved this problem in different way.
In my entity which needs to have relationship added placeholder attributed tmpID.
When creating batch inserts setting tmpID to particular string. As example parent objectID parent.objectID.
Executing batch insert.
Then fetching all object from CoreData with tmpID.
Then going throw everyone and setting:
Relationship
tmpID to nil, the do not waist a memory.
Saving managed object context.
To merge I am using function:
/// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
///
/// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
/// - Throws: An error if anything went wrong executing the batch deletion.
public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
}
// Use case:
do {
try container.viewContext.executeAndMergeChanges(using: deleteReqest)
try container.viewContext.save()
} catch {
Logger.app.e("Can not destroy/save clean table: \(name) -> \(error)")
}

Core Data - Fetching, Sorting and Editing/Updating Attributes

So I've dipped my toes into the Core Data pool, and a dark and mysterious pool it is indeed...
I have the following:
An entity named RAS.
The following attributes exist in RAS:
rasIdentifier of type String (not optional)
rasAutoRetire of type Bool (optional)
rasAutoRetireDate of type NSDate (optional)
rasReassessmentDate of type NSDate (optional)
rasReassesmentDueNow of type Bool (optional)
rasAutoRetireDone of type Bool (optional)
rasReassessmentDone of type Bool (optional)
When I first save a "record" the following happens:
If the user selected for the item to auto-retire on a specific future date, the values are set as follows:
rasIdentifier gets a unique value.
rasAutoRetire is set to true
rasAutoRetireDate is set to a specific date (say a week in the future).
rasReassessmentDate is left blank
rasReassessmentDueNow is set to false
rasAutoRetireDone is set to false
rasReassessmentDone is set to false
If the user selected for the item to be reassessed on a specific future date, the values are set as follows:
rasIdentifier gets a unique value.
rasAutoRetire is set to false
rasAutoRetireDate is left blank.
rasReassessmentDate is set to a specific date.
rasReassessmentDueNow is set to false
rasAutoRetireDone is left blank
rasReassessmentDone is set to false
From the above it should be clear that an item has two options in the future: to either auto-retire or to be reassessed.
To check if an item has reached and passed its reassessment date I have started writing the following function, but the code is clunky and I am getting nowhere with it, and I would REALLY appreciate some genius help here... Here goes:
func HandleItemsThatNeedReassessment() {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedObjContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "RAS")
var error: NSError?
//Step 1: Check if database exists
let count = managedObjContext.countForFetchRequest(fetchRequest, error: &error)
if count == NSNotFound {
print("Database does not exist")
} else {
print("Database exists. Check now if there are any records.")
//Step 2: Check if there are any records in the database
do {
let results = try managedObjContext.executeFetchRequest(fetchRequest)
let varRecordsExist = results as! [NSManagedObject]
if varrecordsExist.count == 0 {
print("No records in the database")
//do nothing further!
} else {
print("Yes, there are \(varRecordsExist.count) records in the database.")
//Step 3: Select all the existing records that are due for reassessment (and leave those that are due for auto-retirement - for now)
//NO IDEA WHAT TO DO HERE!
//Step 4: Check which items are due for reassessment, but exclude those that have already been reassessed (rasReassessmentDone = true)
//NO IDEA WHAT TO DO HERE EITHER!
//Step 5: Because the reassessment date is in the past, change the valueForKey("rasReassessmentDueNow") to TRUE
if varIsAssessmentOverdue == true {
//NO IDEA WHAT TO DO HERE - AGAIN!
??SomethingHere??.setValue(true, forKey: "rasReassessmentDueNow")
//Do this for all items that needs reassessment
}
//Step 6: If any changes were made/values were changed in Step 5, SAVE those changes to Core Data
if managedObjContext.hasChanges {
do {
try managedObjContext.save()
//How do you save these changes???
} catch let error as NSError {
NSLog("Could not save \(error), \(error.userInfo)")
}
}
}
}
}
catch
let error as NSError {
print("Print error stuff here \(error)")
}
}
}
Yeah, the attempted code sucks. I don't even know if my steps are logical. Is it even possible to do this all at once? A lot of the above does not make sense to me - despite hours of googling and reading - and a kind, comprehensive explanation would be highly appreciated.
By the way, this function is called in the viewDidLoad of the first view controller of the app, and that's why I do all those checks in Step 1 and Step 2, otherwise things just crash to a grinding halt if there are no records.
I wouldn't check if there is anything in database at the beginning, I would jump straight to step 3.
You just need to specify predicates which match your requirements, for example:
//Step 3: Select all the existing records that are due for reassessment (and leave those that are due for auto-retirement - for now)
//You could also combine it with Step 4 in the same predicate:
let predicate1 = NSPredicate(format: "%K == true AND %K == false AND %K = false", "rasReassessmentDueNow", "rasAutoRetire", "rasReassessmentDone")
Now create fetch request as you did above, add predicate to the request and run it.
fetchRequest.predicate = predicate1
do {
let results = try managedObjContext.executeFetchRequest(fetchRequest)
let records = results as! [NSManagedObject]
...
Now you have an array with object and here you can check count property to see have you got any data.
In step 5 just enumerate the array and check if varIsAssessmentOverdue is true.
But to be honest I would add another predictate to to the predicate above which check the varIsAssessmentOverdue is true (..AND %K == true",.."varIsAssessmentOverdue") in that case you have only the object you want and you can only enumerate the array and set value to required one, every object in the array will be the one you looking for
for ras in results {
// I don't understand what you are trying to do here, all of the items here will be true and you want to change it to true again?
ras.setValue(value, forKey: attribute)
}
Save changes as you have in the code.
Consider this code mostly as pseudo code some bits will require small amendments but you should be able to pick it up.
The idea here is to hit the database as rarely as possible. You should create NSPredicate which brings you back the result with only the data you want, after then just make changes to the data and save it.

Strange behavior from Core Data in iOS swift app

I am assigning non-nil values to entities, saving them, and then immediately retrieving them. But the values come back as nil. This is iOS 8.1 and Swift on Xcode (latest GA release) with identical behavior on both simulator and a device.
I have three entities: Match, Score and Team. Each Match has two teams, team1 and team2. Score also has team1 and team2. Match has a 1x1 relationship with Score, the fields being bet and betFor respectively. Match is identified by its field matchNumber.
So I have the following code:
var match:CBMatch?
var betCard:CBScore?
for matchNum in 1...42 {
match = CBMatch.lookup(self.managedObjectContext!, matchNumber: matchNum)
betCard = CBScore.addForBet(self.managedObjectContext!, match: match!)
betCard!.team1 = match!.team1
betCard!.team2 = match!.team2
match!.bet = betCard!
println("For match \(matchNum), \(betCard!.team1.shortname) vs \(betCard!.team2.shortname)")
// This is fine for all matchNum values
}
// Verify
if true {
var err:NSError? = nil
self.managedObjectContext!.save(&err)
if err != nil {
abort() // Hasn't happened yet
}
match = CBMatch.lookup(self.managedObjectContext!, matchNumber: 1)
betCard = match!.bet
println("For match 1, \(betCard!.team1.shortname) vs \(betCard!.team2.shortname)")
// Crashes with NPE
// It complains that betCard!.team2 is nil but betCard!.team1 is fine
}
Basically an attempt to retrieve the information I just set comes back as nil.
CBMatch:lookup and CBScore:addForBet are trivial:
class func lookup(moc: NSManagedObjectContext, matchNumber: Int) -> CBMatch? {
var fetchRequest = NSFetchRequest(entityName: "CBMatch")
var predicate = NSPredicate(format: "matchNumber == %d", matchNumber)
fetchRequest.fetchLimit = 1
fetchRequest.predicate = predicate
// fetchRequest.returnsObjectsAsFaults = false
// With or without didn't make a difference
var err:NSError? = nil
let matches = moc.executeFetchRequest(fetchRequest, error: &err)!
if matches.count == 1 && err == nil {
let fetchedMatch = matches[0] as CBMatch
return fetchedMatch
}
println("Could not find match \(matchNumber)")
return nil
}
class func addForBet(moc: NSManagedObjectContext, match:CBMatch) -> CBScore {
var entity = NSEntityDescription.insertNewObjectForEntityForName("CBScore", inManagedObjectContext: moc) as CBScore
entity.betFor = match
return entity
}
I cannot simplify the data model by simply discarding bet.team1 etc, since there will be situations where bet.team1 and match.team1 will be different and I need to be able to compare them. For this (simplest) case they are identical, as this is the first time Score objects are being initialized.
I have tried several things, including returnsObjectsAsFault = false during the fetch etc. When I open the .sqlite file using sqlite3, I can see the entries being made. I tried using sql debug flag set to 3, but nothing jumped out. I am not saying that avenue is exhausted, but I definitely am.
Any thoughts on what could be going on here? I am stuck on this for quite some time and any thoughts would be appreciated.
The error is EXC_BAD_ACCESS, with the problem that match.bet.team1 is nil . This is the same assignment I did barely a few lines above.
It seems to me that there is a flaw in your data model. You are referencing each team twice, which is really not necessary. The Bet entity does not need to know about the teams, as it has a to-one relationship with the Match entity. Bet could contain something along the lines of firstValue and secondValue.
While theoretically your setup should also work, you are introducing unnecessary complexity. You are not giving enough information to troubleshoot your error, but I think it is not necessary to even bother hunting down that bug.
Simply use match.team1 instead of match.bet.team1.
BTW, the obvious thing to look out for is that either match is nil, score is nil, or match.team1 is nil - in all cases score.team1 is also nil.
Found the bug in my code - the team1 and team2 relationships were 1:1, but in order for allowing multiple allocations they needed to be 1:N.

Resources