Struggling with CoreData relationships and deleting objects - xcode

I'm relatively new to Objective-C and trying to work my way through learning CoreData, albeit with tons of Google searches. I have a good grasp of mySQL and relational tables, but I can't seem to wrap my mind around how to get entities to relate to each other in CoreData.
I like to create little projects for myself when learning something like CoreData, so what I put together was the simple concept of an automotive service tracker (like oil changes, brake work, etc).
I've got 2 entities representing Vehicles and Oil Changes like so ...
and
... as well as their respective relationships. I'm not certain if this would require a one-to-one or one-to-many relationship as a vehicle would have many oil changes, but there would also be many vehicles ... so I'm guessing that's the first thing I need some help understanding.
I'm using SQLLite Manager to watch what's going on with the data model. When I add Vehicles to my Vehicle entity (basically saving UITextFields) I can see that the objects are in the correct entity ...
... and when I add Oil Changes to a particular Vehicle, I can see that those objects are being put in their proper entity as well ...
.. but as you can see from the following, there seems to be a disconnect when trying to associate a particular oil change with a particular vehicle ...
Now, for the important questions ...
1). How do I get individual oil changes to relate only to their respective vehicles? Right now all oil changes are displayed regardless of which vehicle is selected. I'm sure that this would be related to updating my fetched results query, however as you can see from the displayed tables, I'm not getting the respective oil change to the respective vehicle.
2). And this might be solved by understanding question 1 ... but if I delete a particular vehicle, then I obviously need to also delete any oil changes that were related to that vehicle.
My save method is below.
- (IBAction)save:(id)sender {
Oil *oil = [NSEntityDescription insertNewObjectForEntityForName:#"Oil" inManagedObjectContext:self.managedObjectContext];
Vehicle *vehicle = [NSEntityDescription insertNewObjectForEntityForName:#"Vehicle" inManagedObjectContext:self.managedObjectContext];
// Which Vehicle
// newOil.vehicle = whichVehicle.text;
oil.vehicle = vehicle; // not working
// Format: Mileage
NSNumberFormatter *numberFormat = [[NSNumberFormatter alloc] init];
[numberFormat setNumberStyle:NSNumberFormatterDecimalStyle];
NSNumber *myNumber = [numberFormat numberFromString:oilMileage.text];
oil.oilMileage = myNumber;
// Format: Date
NSDateFormatter *dateFormat = [[NSDateFormatter alloc]init];
[dateFormat setDateFormat:#"MM-dd-yyyy"];
oil.oilDate = [dateFormat dateFromString:oilDate.text];
// Format: Notes
oil.oilNotes = oilNotes.text;
// save context
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[appDelegate saveContext]; }
->
->
->
->
OK, I'm attempting to integrate what Dan Shelly has suggested (which makes total sense) however I've run into a snag that I need a little more guidance on.
My mainVC isn't using a tableView to display my Vehicles (I've limited the total vehicles to 4 and I'm representing them with icons and tying them back to vehicleIDs in CoreData) ... anyway, since it's not a tableView I can't use the following:
Vehicle *existingVehicle = (Vehicle *)[self.fetchedResultsController objectAtIndexPath:indexPath];
... therefore I'm having trouble creating an instance to the fetched Vehicle object that I can then pass to the OilViewController. I've got a fetchRequest that's returning the correct vehicleID out of CoreData, but I need to know how to actually create the object so that I can pass that over to the OilViewController. My fetchRequest looks like this ...
- (void)fetchObjects {
NSLog(#"DVC-fetching Objects\n\n");
// Create Fetch Request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Vehicle" inManagedObjectContext:[self managedObjectContext]];
[fetchRequest setEntity:entity];
// Set Search Criteria
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"vehicleID == %#", vehicleID];
[fetchRequest setPredicate:predicate];
NSError *error;
NSArray *array = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
NSLog(#"Object ID: %#", [array valueForKey:#"objectID"]);
NSManagedObjectID *moID = [array valueForKey:#"objectID"];
if (array != nil) {
NSUInteger count = [array count]; // May be 0 if the object has been deleted.
Vehicle *existingVehicle = (Vehicle *)[self.managedObjectContext existingObjectWithID:moID error:&error];
NSLog(#"Count: %lu", (unsigned long)count);
NSLog(#"Fetched Vehicle ID: %#",predicate);
NSLog(#"existingVehicle: %#", existingVehicle);
} else {
// Deal with error.
}
}
but I'm getting the following error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArrayI isTemporaryID]: unrecognized selector sent to instance 0x11d5b660'
The moID is returning correctly as "0x8bb1980 x-coredata://6681B331-29F0-448B-82F1-4660E033A460/Vehicle/p1"
The array count is returning "1" [correct]
And the FetchVehicle ID is returning "vehicleID == 1" [also correct]
How do I create an instance of Vehicle to this returned object so that I can pass it over??

1) How do I get individual oil changes to relate only to their respective vehicles?
You already do ... only you are currently relating the oil changes to an empty Vehicle object. this is also visible in your vehicle table (Z_PK = [3,4,5]).
When you need to add a new oil change, you probably already have access to the relative Vehicle object this oil change is adde for.
This Vehicle object is probably the one you select in you vehicles view controller.
so instead of inserting a Vehicle object each time you create an oil change, simply set the relationship to an existing vehicle (you are building an object graph, think in terms of existing object and attaching them to each other).
2) Deletion:
from the pictures it seems that you set your deletion rules as they should be in order to achieve your goal. when you delete a Vehicle object the cascade rule will delete the related oil change object along with it. when you delete an oil change object the related Vehicle oil relationship will be nullified.
So to summarise:
Get an existing Vehicle object:
//in your vehicle view controller :
Vehicle* existingVehicle = (Vehicle*)[self.fetchedResultsController objectAtIndexPath:indexPath];
Pass this object along to your "oil change view controller".
When you add a new oil change, set the vehicle to the existing object:
Oil *oil = [NSEntityDescription insertNewObjectForEntityForName:#"Oil" inManagedObjectContext:self.managedObjectContext];
oil.vehicle = existingVehicle; // the one you passed along to this scope
//The rest of the oil content setting
//DON'T FORGET TO SAVE!
You will probably want to change you relationship type to a to-many type of relation ship (a Vehicle may have many oil changes).
This will not change your oil change creation code.
To view oil changes for a specific Vehicle object, you will have to use the existing vehicle object you passed to the oil change view controller and use it in the predicate of the FRC fetch request:
NSPredicate* p = [NSPredicate predicateWithFormat:#"vehicle = %#",existingVehicle];
fetchRequest.predicate = p;

Related

Xcode 8.1 core data fetching specific properties

I am trying to fetch a specific property from Core Data using the following code:
NSFetchRequest *test = [[NSFetchRequest alloc] init];
test.entity = [NSEntityDescription entityForName:#"MyEntity" inManagedObjectContext:self.currentMainContext];
test.predicate = [NSPredicate predicateWithFormat:#"TRUEPREDICATE"];
test.resultType = NSDictionaryResultType;
test.returnsDistinctResults = YES;
test.propertiesToFetch = #[#"property"];
NSArray *results = [self.currentMainContext executeFetchRequest:test error:error];
but I get an empty array back.
If I comment out test.resultType = NSDictionaryResultType; then I get back an array of all the entities in my database as expected. What is it about the NSDictionaryResultType that is wrong?
Fetch request with resultType of NSDictionaryResultType has one peculiarity.
Documentation for includesPendingChanges states that:
If the value is NO, the fetch request doesn't check unsaved changes and only returns objects that matched the predicate in the persistent store.
A value of YES is not supported in conjunction with the result type NSDictionaryResultType, including calculation of aggregate results (such as max and min). For dictionaries, the array returned from the fetch reflects the current state in the persistent store, and does not take into account any pending changes, insertions, or deletions in the context.
So make sure that you have saved your changes, or use NSManagedObjectResultType.

Core Data Relationship Designing - Load only desired entity

I need help in designing the database for my application using Core Data.
Namely:
I have 2 Entites called Verse and Translation.
Verse has one-to-many relationship with translation table.
Translation table contains translations in different languages such as EN,DE,AR.
I know if i load the Verse table then corresponding translations will also be lazy loaded.
But i want to load only desired translation of each Verse. E.g only EN.
Because if i load 1000 Verse and with 5 different Translation (DE,EN,TR,AR,FR) then i have a big amount of data which i want to avoid >> 1000x5 = 5000.
I don‘t want load everytime the translations which i dont need. Only the selected translation.
thank you for any advice.
Regards,
Core data does something called faulting, it should only load data you actually ask for. It might load some metadata for all those objects, but it won't actually load the translation until you ask for it. I would suggest looking into faulting more if you want to know more than that because it seems fairly complex. At least the rest of what faulting is seems complex.
i found a solution which is exactly what i wanted. I tested and it returns only the desired Translations.
- (NSArray *) loadAllVersesByLanguage
{
NSManagedObjectContext *_managedObjectContext = [self managedObjectContext];
NSArray *fetchedObjects;
NSString *turkish = #"TR";//should be parameterized
NSString *arabic = #"AR";
NSPredicate *pred = [NSPredicate predicateWithFormat:#"languageCode==%# OR languageCode==%#",turkish,arabic];
NSFetchRequest *fetch = [[NSFetchRequest alloc] initWithEntityName:#"Verse"];
NSError * error = nil;
fetchedObjects = [_managedObjectContext executeFetchRequest:fetch error:&error];
if(fetchedObjects != nil && fetchedObjects.count > 0 ){
for(Verse *verse in fetchedObjects){
NSSet *verseSet = [verse.translations filteredSetUsingPredicate:pred];
[verse.translations setSet:verseSet];
}
return fetchedObjects;
}else{
return nil;
}
return nil;
}

Fetched Results Controller objectAtIndexPath without using TableView

I have a Core Data financial app that needs to accumulate sales quantities contained in the Trans Entity for each product and then update the totals into the appropriate attribute of the Product Entity.
I am able to achieve this by nesting a for (transArray) inside and tableView (product).
However I need to sort and format the tableView based on the results first.
General Question: Can fetched results be used without tableViews?
- (void)calculateAmounts {
NSIndexPath *indexPath=0;
for (Product *product in self.fetchedResultsController.fetchedObjects){ // All product records
selectedProduct = [self.fetchedResultsController objectAtIndexPath:indexPath];
// >>>>>NSLog shows correct number of object, however selectedProduct # Index Path Are NULL
for (id product1 in transProductArray) { // An array of all of the trans for product
if ((NSNull *)product1 == [NSNull null]) {
}
else if ([product1 isEqualToString:selectedProduct]) {
float qty = [#"1" floatValue];
NSNumber *numQty=[NSNumber numberWithFloat:qty]; // Update quantity sold in product by 1
NSNumber *quantity = [NSNumber numberWithFloat:([selectedProduct.quantitySold floatValue] + [numQty floatValue])];
selectedProduct.quantitySold = quantity;
[self.product.managedObjectContext save:nil];
}
} // Next Trans
} // Next Product
}
This is a good question. Not (clearly) knowing the implementation, I will bet it is possible to use fetched results outside a tableview. However, the Overview of NSFetchedResultsController documentation has this as the first line:
You use a fetched results controller to efficiently manage the results
returned from a Core Data fetch request to provide data for a
UITableView object.
While table views can be used in several ways, fetched results
controllers are primarily intended to assist you with a master list
view.
This infers that the two objects are linked for efficiency and ease of use. Plus, there are monitors under-the-hood to watch for changes, etc.
What you may be looking for is a 'plain-old' NSFetchRequest. A tutorial.
Hi I am not to sure whether you still require an answer to this however I had the same issue myself. I overcame as described in my own question on the subject.
Display multiple core data entities objects in 1 Non-Table View Controller
I inserted a small tableview into my VC and set it to alpha and used the - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath to enable displaying details in labels.
This was the only way I could overcome it. Hope this helps.
Thanks for the answer. Like a lot of things the answer was too easy! It's about the fetch, not the tableview. Just do a fetch into an array, then loop though the array to accumulate your values: Also useful for finding a specific object or output to a CSV.
NSError *error;
NSNumber *total=0;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"TransDetail" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
for (TransDetail *trans in fetchedObjects) {
total = total + trans.amount;
}

Understanding Core Data Queries(searching data for a unique attribute)

Since I am coming from those programmers who have used sqlite extensively, perhaps I am just having a hard time grasping how Core Data manages to-many relationships.
For my game I have a simple database schema on paper.
Entity: Level - this will be the table that has all information about each game level
Attributes: levelNumber(String), simply the level number
: levelTime(String), the amount of time you have to finish the level(this time will vary with the different levels)
: levelContent(String), a list of items for that level(and that level only) separated by commas
: levelMapping(String), how the content is layed out(specific for a unique level)
So basically in core data i want to set up the database so i can say in my fetchRequest:
Give me the levelTime, levelContent and levelMapping for Level 1.(or
whatever level i want)
How would i set up my relationships so that i can make this type of fetchRequest?
Also I already have all the data ready and know what it is in advance. Is there any way to populate the entity and its attributes within XCode?
As you've described it, it's a single Core Data entity, called Level that has four string attributes. Since there's just the one entity, there are no relationships. You'd create the one entity and add properties so that it looks just like you've described it above:
Getting just one Level is basic Core Data fetching:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Level"];
NSString *levelNumber = #"1";
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"levelNumber = %#", levelNumber];
[request setPredicate:predicate];
NSError *error = nil;
NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:&error];
NSManagedObject *level = nil;
if ([results count] > 0) {
level = [results objectAtIndex:0];
}
// Use level...
If it was me I'd use one of the numeric types for levelNumber, but maybe you have some reason to use a string there. I'd also probably break levelContent into a separate entity, because (a) comma delimited strings are ugly, no matter how you slice 'em, and (b) you might well want the items to have more attributes, and a separate entity would hold those.

How to implement a read only attribute in a Core Data app

I'm developing a Core Data app that uses a model object called Location. The user can enter locations and provide their latitude and longitude in decimal format. I use plain NSTextFields for this. As a feedback to the user, also their degrees/minutes/seconds counterparts are shown (using a label). The logic for transforming from decimal format to degrees etc. is implemented with the Location model object, which is a subclass of NSManagedObject.
Ideally I want to implement these as read-only attributes and have them tied in some way to their decimal counterpart, so that when the user changes the decimal representation, the degrees/minutes/seconds representation gets updated as well.
I've tried the following:
Set the controller as an NSTextFieldDelegate to intercept edits but this only works if the user actually edits the fields. This fails when the user accepts 0 as defaults for both latitude and longitude, which is actually a valid location.
I've looked at transient attributes for Core Data but found the documentation on this point not very helpful, although they might be the answer for this...
Any ideas on how to approach this?
EDIT:
As suggested by Francis McGrew, I implemented the following class method for Location:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *result = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:#"latitudeToDegrees"] || [key isEqualToString:#"longitudeToDegrees"]) {
result = [result setByAddingObjectsFromSet:[NSSet setWithObjects: #"latitude", #"longitude", nil]];
}
return result;
}
Slightly different from his answer, as the DMS is just one attribute represented as a formatted String, calculated by Location. I then added bindings in the UI to latitudeToDegrees and longitudeToDegrees and ...boom!.., a nicely updated UI.
If someone could explain transient properties to me I'd love to hear it as well.
Since DMS is easily calculated from the latitude and longitude, there's no real reason to store those values as attributes in your model. I would just write methods that calculate and return the current degrees, minutes and seconds based on the saved latitude and longitude attributes.
Then, to have Core Data automatically notify obervers of changes, you would implement the following method in your Location class:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *result = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:#"degrees"] ||
[key isEqualToString:#"minutes"] ||
[key isEqualToString:#"seconds"]) {
result = [result setByAddingObjectsFromSet:[NSSet setWithObjects:
#"latitude", #"longitude", nil]];
}
return result;
}
This tells Core Data that the "transient" degrees, minutes and seconds are dependent on your latitude and longitude attributes. Assuming you're using bindings, your user interface should update automatically. To prevent the user from editing the text fields, just set their behavior to "Selectable" or "None" in your XIB. (In XCode 3 I think you just uncheck the "Editable" box)

Resources