NSPredicate example of one to many relationship query - swift

This is my core data model
The relationship is as follows:
enter image description here
The relationship is one Route to many Locations. First question is should there be a name attribute (routeName) in Location? In DB2 for example, you always name your primary keys, but most examples I have seen in Core Data they handle primary keys under the covers in the relationship definition.
Here are Table excerpts from DB Browser for sqlite.
Table Excerpts
Here is the code:
final class CoreDataManager {
static let sharedInstances = CoreDataManager()
let managedContext: NSManagedObjectContext
private init(){
let application = UIApplication.shared.delegate as! AppDelegate
managedContext = application.persistentContainer.viewContext
}
func createRoute(name: String) -> Route {
let routeEntity = NSEntityDescription.entity(forEntityName: "Route", in: managedContext)!
let route = NSManagedObject(entity: routeEntity, insertInto: managedContext)
route.setValue(name, forKeyPath: "name")
route.setValue(Date().timeIntervalSince1970, forKey: "ts")
do {
try managedContext.save()
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
return route as! Route
}
func createLocation(loc: CLLocation, route: Route) {
let locationEntity = NSEntityDescription.entity(forEntityName: "Location", in: managedContext)!
let location = NSManagedObject(entity: locationEntity, insertInto: managedContext)
location.setValue(loc.coordinate.latitude, forKeyPath: "latitude")
location.setValue(loc.coordinate.longitude, forKeyPath: "longitude")
location.setValue(loc.altitude, forKeyPath: "altitude")
location.setValue(loc.timestamp, forKeyPath: "ts")
location.setValue(route, forKeyPath: "route")
do {
try managedContext.save()
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
func getAllRoutes () -> [Route] {
let routeFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Route")
let routeResults = try! managedContext.fetch(routeFetch)
return routeResults as! [Route]
}
func getLocationsByRoute (name: String) -> [Location] {
let locFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Location")
//select * from ZLOCATION, ZROUTE where ZLOCATION.ZROUTE = ZROUTE.Z_PK and ZROUTE.ZNAME = ?
locFetch.predicate = NSPredicate(format: "locations.route = %#", name)
locFetch.sortDescriptors = [NSSortDescriptor.init(key: "ts", ascending: false)]
let locations = try! managedContext.fetch(locFetch)
return locations as! [Location]
}
}
If no primary key in Location Table, how do I create a query to get all Locations for a given Route? You can see in the code I have a getLocationsByRoute method, but cannot get the query to work. Any help would be appreciated.

If you have the Route object, get the locations from the locations property
If you have to fetch the objects use the predicate format
locFetch.predicate = NSPredicate(format: "route.name = %#", name)
The key path is always <relationship name>.<attribute name> – in this case get all Location objects whose name of the related route is equal to the given name.

Stop thinking in database terms, Core Data is an ORM.
You get all locations for a route by accessing the locations property of the Route class since it corresponds to the one to many relationship between Route and Location
let locations = someRoute.locations

Related

Core data not retaining older values

When I save to Core Data and then try to read from it, only the most recently saved value is retained.
The rest are nil when I try to print them out.
In my .xcdatamodeld, my entity is named CD_Cookbook and it has an attribute of name.
I'm not sure what I'm doing wrong.
func newCookbook(cookbook: String) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "CD_Cookbook", in: managedContext)!
let item = NSManagedObject(entity: entity, insertInto: managedContext)
item.setValue(cookbook, forKey: "name")
do {
try managedContext.save()
}
catch {
print("did not save cookbook name to core data", error)
}
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "CD_Cookbook")
do {
let cd = try managedContext.fetch(fetchRequest)
print(cd)
}
catch {
print("Failed to fetch cookbook names from Core Data", error)
}
}
Instead of:
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "CD_Cookbook")
Do this instead:
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "CD_Cookbook")

Correct method to get and set data using CoreData

I'm having an issue with CoreData, I set some data in the AppDelegate. Then later in a view controller's class I get those values and display them in a list view. When I try to fetch the values the in the view from CoreData I get nothing back.
This is the code I'm using to create the context:
public func createMainContext() -> NSManagedObjectContext {
let modelUrl = Bundle.main.url(forResource: "ItemModel", withExtension: "momd")
guard let model = NSManagedObjectModel.init(contentsOf: modelUrl!) else { fatalError("model not found") }
let psc = NSPersistentStoreCoordinator(managedObjectModel: model)
try! psc.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.persistentStoreCoordinator = psc
return context
}
This is the code I'm using to get the values:
var fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Topic")
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = createMainContext()
let result = try! context.fetch(fetchRequest)
debugPrint("---- \(result)")
Insert a new Entity
let context = self.createMainContext()
let entity = NSEntityDescription.insertNewObject(forEntityName: "Topic", into: context)
entity.setValue(name.stringValue, forKey: "name")
When I set the values initially I perform a fetch & I can see the values, however, when perform a fetch from another view I get nothing back.
Does anyone have a suggestion on what I'm doing wrong or a correct way to do this?
Thanks

Fetch Request Error

I've saved a user input name in the previous storyboard into the xcdatamodel under the entity "UserInfo" with the name "name". I'm trying to fetch it in the next storyboard to display in a label to greet the user. I'm getting the error "Cannot Invoke Initializer for type 'NSFetchRequet' with an Argument with list of type '(entityName:String, attributeName: String)"
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
//getting the managed context where the entity we need is
let managedContext = appDelegate.persistentContainer.viewContext
//make fetch request
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "UserInfo", attributeName: "name")
//try to fetch the entity we need, else print error
do {
Username = try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
There is no initializer for NSFetchRequest that takes the argument attributeName:, just like the error says. Your options are NSFetchRequest(entityName:) or NSFetchRequest().
When in doubt, look up the class in the API reference to make sure you understand how to use it.
NSFetchRequest doesn't have an initializer entityName:attributeName, you have to use
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "UserInfo")
fetch returns always an array. If there is only one record in the entity, get the first item and the value for attribute name:
do {
let users = try managedContext.fetch(fetchRequest)
if let user = users.first {
Username = user.value(forKey: "name")
}
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
If there are multiple records you might apply a predicate.
And don't guard AppDelegate. If this class didn't exist the app won't even launch. The exclamation mark is safe as safe can.
let appDelegate = UIApplication.shared.delegate as! AppDelegate

Understanding Core Data When Deleting Objects that Depend on Each Other

This question is asking for the best practice in the following scenario:
Attached are images showing my work orders and services core data entities. Note that the Delete Rule is currently No Action for Work Order. (Note changing to Nullify will not fix my issue, just causes same issue). Also take note that on Service I have constraints on id. This won't allow duplicates. As such I aded a merge policy below:
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
The merge policy will take the new data I send and overwrite what is in the database as the default. Without this my program will throw an error with how its written.
If I run my code with these settings, and I do a batch delete on workorders BUT NOT SERVICES (because I want to keep those) what happens is when I restart my program it crashes when I try to add **a reference to a Service with the same id.
My question is why would it crash and what is the best way to work around this? My current theory is that these entities might have another unique identifier and because I deleted the work order its reference was to a different contexted version of services... and when I create the new one using the same id as the old services it assumes the same internal id possibly. I am not sure if this is happening though or how to confirm that.
My code happens in viewDidLoad method of one of my controllers and looks like this.
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
let context = gm_getContext()
//Create default fetch request to get all workorders
let fetchRequest: NSFetchRequest<Workorders> = Workorders.fetchRequest()
do{
//Run fetch request to get search results.
let searchResults = try context.fetch(fetchRequest)
//If no results were found and demo mode = true, lets create some default records.
if(searchResults.count<=0 && g_demoMode==true){
print("create default data")
//Uncomment the following lines if you want to prove that the Merge Policy
//Is working for Unique Constraints.
let serviceFetchRequest: NSFetchRequest<Service> = Service.fetchRequest()
let serviceSearchResults = try context.fetch(serviceFetchRequest)
print("Services Count = \(serviceSearchResults.count)")
//First we have to create a sample service
let entity = NSEntityDescription.entity(forEntityName: "Service", in: context)
let service = NSManagedObject(entity: entity!, insertInto: context)
service.setValue(1, forKey: "id")
service.setValue("Tire Repair Service Sample", forKey: "name")
service.setValue("<html>Test Service Field</html>",forKey:"templatedata")
//add reference to the global
g_services.append(service as! Service)
//Proof that service is indeed a Service object and stored in global
print("g_services[0].name = "+g_services[0].name!)
//Save the service object (overwriting an old one with same id if needed)
do {
try context.save()
print("Saved context with service")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
print("Could not save, unknown error")
}
//Now create 3 sample work orders all using the same service template.
let workorderEntity1 = NSEntityDescription.entity(forEntityName: "Workorders", in: context)
let workorder1 = NSManagedObject(entity: workorderEntity1!, insertInto: context)
print("created work order variable 1")
workorder1.setValue(1, forKey: "id")
workorder1.setValue("11402 Kensington Rd, Los Alamitos, CA, 90720", forKey: "address")
workorder1.setValue("33.797472", forKey: "lat")
workorder1.setValue("-118.084136", forKey: "lng")
workorder1.setValue(15,forKey: "client_id")
workorder1.setValue("Need to fix their tire fast", forKey: "desc")
workorder1.setValue("(562)810-4384", forKey: "phone")
workorder1.setValue(g_services[0], forKey: "service")
print("Created first work order")
let workorderEntity2 = NSEntityDescription.entity(forEntityName: "Workorders", in: context)
let workorder2 = NSManagedObject(entity: workorderEntity2!, insertInto: context)
workorder2.setValue(2, forKey: "id")
workorder2.setValue("17078 Greenleaf Street, Fountain Valley, CA, 92708", forKey: "address")
workorder2.setValue("33.714992", forKey: "lat")
workorder2.setValue("-117.958874", forKey: "lng")
workorder2.setValue(16,forKey: "client_id")
workorder2.setValue("This guy does not know what he wants", forKey: "desc")
workorder2.setValue("(562)777-3344", forKey: "phone")
workorder2.setValue(g_services[0], forKey: "service")
let workorderEntity3 = NSEntityDescription.entity(forEntityName: "Workorders", in: context)
let workorder3 = NSManagedObject(entity: workorderEntity3!, insertInto: context)
workorder3.setValue(3, forKey: "id")
workorder3.setValue("17045 South Pacific Avenue", forKey: "address")
workorder3.setValue("33.713565", forKey: "lat")
workorder3.setValue("-118.067535", forKey: "lng")
workorder3.setValue(17,forKey: "client_id")
workorder3.setValue("Tire damaged by the beach", forKey: "desc")
workorder3.setValue("(714)234-5678", forKey: "phone")
workorder3.setValue(g_services[0], forKey: "service")
//Don't need signature, pictures and videos because they just don't exist yet.
//add reference to the global
g_workOrders.append(workorder1 as! Workorders)
g_workOrders.append(workorder2 as! Workorders)
g_workOrders.append(workorder3 as! Workorders)
print("Preparing to save to context for work orders")
//Save the work order objects (overwriting any old ones with same id if needed)
do {
try context.save()
print("Saved context with workorders")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
print("Could not save, unknown error")
}
}else{
print("WorkOrders Count = \(searchResults.count)")
let workorderFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Workorders")
//let workorderFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Workorders")
let deleteWorkOrderRequest = NSBatchDeleteRequest(fetchRequest: workorderFetchRequest) //Deletes ALL workorders
//Perform Actual Deletion On Database Tables
do{
try context.persistentStoreCoordinator!.execute(deleteWorkOrderRequest, with: context)
}catch{
fatalError("Bad Things Happened \(error)")
}
print("deleted workorders")
}
} catch {
print("Error with request: \(error)")
}
print("service table view controller loaded")
}
My context and global variables to track the coreData values are defined globally in a globals.swift file like this.
var g_workOrders = [Workorders]()
var g_services = [Service]()
//Shortcut method to get the viewcontext easily from anywhere.
func gm_getContext () -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
//For unique constraints it will overwrite the data.
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context
}
Core Data Model References:
Other Notes & Things I've Tried:
I know it crashes at this line (workorder1.setValue(g_services[0], forKey: "service")), which is how I know its related to service, and changing the rule to cascade delete for workorders fixes the crash however it deletes the Services that were attached to it! ... which makes sense but not what I wanted.
I have recently found the answer to my question, and the problem is related to multiple things.
First my core data stack was set incorrectly. I've now changed it to this (courtesy my friendly developer friend who pointed this out).
import UIKit
import CoreData
class DataController: NSObject {
var managedObjectContext: NSManagedObjectContext
static var dataController: DataController!
override init() {
// This resource is the same name as your xcdatamodeld contained in your project.
guard let modelURL = Bundle.main.url(forResource: "WorkOrders", withExtension: "momd") else {
fatalError("Error loading model from bundle")
}
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = psc
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let docURL = urls[urls.endIndex-1]
/* The directory the application uses to store the Core Data store file.
This code uses a file named "DataModel.sqlite" in the application's documents directory.
*/
let storeURL = docURL.appendingPathComponent("WorkOrders.sqlite")
do {
let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]]
try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
} catch {
fatalError("Error migrating store: \(error)")
}
}
class func sharedInstance() -> DataController {
if (dataController != nil) {
return dataController
}
dataController = DataController()
return dataController
}
}
Whenever I need to access coreData I should be doing it this way now...
let context = DataController.sharedInstance().managedObjectContext
Another thing to note is the concurrency setting in the Datacontroller is set to work on the main thread. This was also part of the problem since I was running my code in a thread.
Its set to the main thread on this line in DataController
managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
So everytime you are going to access or save data to coreData always wrap it in a call to the main thread like below...
DispatchQueue.main.async {
AppDelegate.appDelegate.saveContext()
}
Finally, the last problem I had was I was doing a batch delete with the following command below.
let workorderFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Workorders")
let deleteWorkOrderRequest = NSBatchDeleteRequest(fetchRequest: workorderFetchRequest) //Deletes ALL workorders
let context = DataController.sharedInstance().managedObjectContext
//Save the work order objects (overwriting any old ones with same id if needed)
do {
try context.execute(deleteWorkOrderRequest)
context.reset()
print(">>> cleared old data!")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
print("Could not save, unknown error")
}
The key here is understanding that batch commands currently work directly on the database and ignore the managed context, this means my managed context and database were getting out of sync after I ran this command. The easy fix is to always make sure after doing batch commands to run...
context.reset()
This will forcefully load back the data from the database into the managed context so everything is in sync. After I made these changes everything worked fine. Hope this helps someone.

Update attribute for specific element using core data

I am working with core data to store different activities and keep track of how many times each activity has been performed. The entity is called "Activities" with attributes "name" and "total". When an activity has been performed more than once, I want to change the attribute "total" (By adding +1) for that specific activity, instead of adding a new activity. How can I do this? This is my code so far:
let appDel = (UIApplication.sharedApplication().delegate as! AppDelegate)
let context: NSManagedObjectContext = appDel.managedObjectContext
let request = NSFetchRequest(entityName: "Activities")
request.returnsObjectsAsFaults = false
request.predicate = NSPredicate(format:"(name == %#)", name)
do {
let results = try context.executeFetchRequest(request) as! [Activities]
if results.count == 0 {
newActivity.setValue(1, forKey: "total")
newActivity.setValue(name), forKey: "name")
do {
try context.save()
}
catch {
print(error)
}
}
}
catch let error as NSError {
print(error)
}
}
If you want to update (if I am not mistaken), you can just change the total like this:
results.total = YourWantedNumber or results.setValue(YourWantedNumber, forKey: "total")
context.save()