Core Data: unnecessary queries? - ios

Good evening,
I need some help with Core Data. I have two entities, Images & Album (ImageGroup in code), that have a many-to-many relationship. Each image can have many Albums and an Album can have many images.
In the application, the user can create an Album. Once the album is created, all the new pictures get added to that album. This is working fine.
But now I have a question regarding the fetch request in Core Data. Please have a look at my current "fetch" code:
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSError *error;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"ImageGroup" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
[fetchRequest release];
// Load all the other albums
for (ImageGroup *group in fetchedObjects) {
NSString *albumName = group.name;
NSSet *groupSet = group.group;
for (Images *image in groupSet) {
NSLog(#"imageTN: %#", image.nameTN);
}
}
This is working fine as well. However, during the "for in" loop for groupSet, a select query is executed for EACH step in the loop. This is fine if you only have a few images, however, if you have 1000+ images stored, this code would execute 1000+ queries.
Is there a way to load all the images that are associated with the Album in one single query?
(SELECT * from Images WHERE album_ID = ? something like that).
This is the first time i'm using Core Data and not sqlite. Any help is truly appreciated.

This is possible, if you don't go through the set, or you prefetch the images in group (since your current fetch request fetch ALL ImageGroup entities, you probably should avoid that).
To prefetch the to-many relationship add to your request:
[fetchRequest setRelationshipsForPrefetching:#[#"group"]]
i would also add:
[fetchRequest setIncludesPropertyValues:YES] and:
[fetchRequest setReturnsObjectsAsFaults:NO]
If you want to issue a single request per ImageGroup you could use:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Images"];
[fetchRequest setIncludesPropertyValues:YES]
[fetchRequest setReturnsObjectsAsFaults:NO]
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"%# IN <albom set>",group.objectID]; //not tested
note: by the end of the external loop you have faulted (allocated in memory) all alboms and all their attached images.
You should fault each ImageGroup you are done with to keep memory peeks at bay.

Related

How do I handle a relationship with a NSFetchedResultsController?

Core data is already handling the relationship for me, do I need to build a new query to use the NSFetchedResultsController?
I have "Albums" that contain "Photos".
Album *anAlbum = [...getalbum...];
in the *cellForItemAtIndexPath: I'd like to do something with:
anAlbum.photos
However, I can't convert the indexPath to a NSSet member index. (obviously)
Without Core Data I'd typically just generate the query required myself. I'd like to make use of Core Data (again, obviously).
The "Photo" entity (anAlbum.photos is the relationship) contains the
asset url. I have no issue with the displaying it was more of a
concern of how do I use the NSSet (Core Data relationship) with the
NSFectchedResultsController -- or direct with the view
(collection/table).
First of all I would use a NSFetchedResultsController for this. This component is made to work in conjunction with tables and allows to load data in a lazy loading manner.
Second, the fetch request I would use should be run against Photo entity and not against Album. In other words, you should select all the photos that belong to a specific album.
Here the code, I would use...
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Photo" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"album == %#", anAlbum];
[request setPredicate:predicate];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"takeAt" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext sectionNameKeyPath:nil
cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
return _fetchedResultsController;
}
So now in your tableView:cellForRowAtIndexPath: you would use
Photo *photo = [_fetchedResultsController objectAtIndexPath:indexPath];
// access the album the photo belongs to
Album* album = photo.album;
You can grasp the main concepts for NSFetchedResultsController at Core Data Tutorial for iOS: How To Use NSFetchedResultsController.
Usually, you're going to convert the set to an NSArray (maybe with NSSet's allobjects) and perhaps sort it or filter it with a NSSortDescriptor or NSPredicate or both. Then, you can access the array by index using the index path section/row.
You can obviously use a NSMutableArray if that's what you need, but it is a pain to try to access NSSet items individually as you essentially have to enumerate them to find what you're looking for. Just create an array. Here's an example where I've done this:
NSArray *tableViewData = [NSArray arrayWithArray:[album.photos allObjects]];
Then, you just access the tableViewData objects with:
UIImage *obj = UIImageJPEGRepresentation([tableViewData objectAtIndex:indexPath.row], 0.50f);
Something like that.

Access related objects in Core Data

I've made a relationship between my Songs and Playlists entities. Now I'm trying to access the Songs in the selected Playlist. The selected playlist is saved in the object
optionsSingle.selectedRowNumber
I have this fetch method where I'm accessing all the songs object and save them in a NSMutableArray called playlists. How do I only fetch the songs which are related to the give playlist object (optionsSingle.selectedRowNumber)?
- (void)fetchDevices {
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Playlists"];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Playlists" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
playlists = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
}
Core Data Model:
I think you have a better design choice. While the corrections suggested by Sebastian are very important, you should redesign your table view. (I infer it is a table view displaying your playlists from your question).
You should use a NSFetchedResultsController rather than creating an array and populating your table with the contents of the array. The FRC will take care of all the fetching and memory management specific for the table view.
You can get a working example of a NSFetchedResultsController easily by opening the master-detail template ("New Project" in Xcode) and checking the "Core Data" option.
Now you can get to the playlist very easily:
Playlist *playlist = [self.fetchedResultsController objectAtIndexPath:indexPath];
You will find that it will be much easier to write clean, concise and readable code.
There are some issues with your code and model. Entity classes should be named singular (Song and Playlist).
The method is called fetchDevices (seems like a copy&paste error and should rather be fetchPlaylists or something?)
You are doing too much to retrieve them. The method can be reduced to:
- (void)fetchPlaylists {
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Playlist"];
playlists = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
}
You can also try to reduce the relation so that one Playlist has many Song(s). That should be enough for your requirements. Then you can proceed and access a list of songs that will automatically be fetched with your playlist.

How to run simultaneous searches in core-data?

I am trying to understand how to run simultaneous searches in core-data.
Here is my example, but it doesn't work, because one of the GCDs seems to never activate
If I leave the custon MOC in there I get an error "unable to find model for entity 'Recipe''
-(void)performSearch:(NSString*)name{
//TODO: Is there a better way
//In case the previous search hasn't finished
if (globaDispatchRequestInprogress) {
//Send on GCD
dispatch_queue_t searchQueque = dispatch_queue_create("search queque 2", NULL);
dispatch_async(searchQueque, ^{
NSLog(#"\n\nDispatch 2 In Progress*******\n\n");
//Init local variables
NSFetchRequest *fetchRequest =[[NSFetchRequest alloc]init];
NSError *error;
//Create own MOC for multiThread
NSManagedObjectContext *tempContext = [[NSManagedObjectContext alloc]init];
[tempContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
NSPredicate *recipeName = [NSPredicate predicateWithFormat:#"ANY recipe.name ==[c] %#",name];
//Set predicate to fetch request
[fetchRequest setPredicate:recipeName];
//Set query. We are searching recipes
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Recipe" inManagedObjectContext:tempContext];
//sets up fetch request details
[fetchRequest setEntity:entity];
//Attempt to speed up program
[fetchRequest setReturnsObjectsAsFaults:NO];
//Perform fetch assign to return array
NSArray*records = [tempContext executeFetchRequest:fetchRequest error:&error];
//Add to temporary results
//TODO: Add to NSDictionary
[self addResultsToTemporaryResults:records];
NSLog(#"Total results = %i",[_temporaryResultsArray count]);
NSLog(#"\n\nDispatch 2 END**************\n\n");
});
}
//Send on GCD
dispatch_queue_t searchQueque = dispatch_queue_create("search queque", NULL);
dispatch_async(searchQueque, ^{
NSLog(#"\n\nDispatch In Progress*******\n\n");
//Set flag
globaDispatchRequestInprogress=YES;
//Init local variables
NSFetchRequest *fetchRequest =[[NSFetchRequest alloc]init];
NSError *error;
//Create own MOC for multiThread
NSManagedObjectContext *tempContext = [[NSManagedObjectContext alloc]init];
[tempContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
NSPredicate *recipeName = [NSPredicate predicateWithFormat:#"ANY recipe.name ==[c] %#",name];
//Set predicate to fetch request
[fetchRequest setPredicate:recipeName];
//Set query. We are searching recipes
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Recipe" inManagedObjectContext:tempContext];
//sets up fetch request details
[fetchRequest setEntity:entity];
//Attempt to speed up program
[fetchRequest setReturnsObjectsAsFaults:NO];
//Perform fetch assign to return array
NSArray*records = [tempContext executeFetchRequest:fetchRequest error:&error];
//Add to temporary results
//TODO: Add to NSDictionary
[self addResultsToTemporaryResults:records];
NSLog(#"Total results = %i",[_temporaryResultsArray count]);
globaDispatchRequestInprogress=NO;
NSLog(#"\n\nDispatch END**************\n\n");
});
}
I see several things that make me suspicious, but no obvious smoking gun.
If you're seeing "unable to find model", that suggests that your persistent store coordinator is not being configured the way you think it is. It would be interesting to NSLog self.persistentStoreCoordinator.managedObjectModel, and also self.persistentStoreCoordinator.managedObjectModel.entitiesByName.
The preferred GCD approach to Core Data is to use performBlock: or performBlockAndWait:, with the proper concurrency type for your managed object context. See http://developer.apple.com/library/mac/#releasenotes/DataManagement/RN-CoreData/index.html
You're keeping the results of your fetch around, in your addResultsToTemporaryResults: call. We don't see the source for it, but is it thread-safe? Those records you found have no existence outside of the tempContext you fetched them in, and may only be accessed from the thread that found them. You probably want to be using NSManagedObjectIDs there (and perhaps you already are).
Your second call to dispatch_queue_create() will always be executed. Did you mean to do an if-else instead of a simple if?
When you do -executeFetchRequest:error:, check the result. If it's a nil result, take a look at the NSError you passed in.

How to change value of an attribute of all entities in Core Data at once?

I am building application that has 'checklist' functionality. The checklist item is stored as Core Data entity. There is a 'checked' attribute stored as BOOL in the Datamodel as well. The view controller handling this checklist functionality is based on UITableViewController.
Basically, I would like to implement the UIRefreshControl which allow users to reset the 'checked' status of all checklist entities in Core Data. For example, all the items would be reseted and shown as 'unchecked' once user pulls down the UITableView.
However, NSFetchedResultsController only provides access to one entity at a time via [fetchedResultsController objectAtIndexPath:indexPath]. Would there be a way to get the whole collection of entities from Core Data as NSArray or NSDictionary therefore I could enumerate all entities and change their 'checked' attribute?
Agrees with coverback...lets say you want to fetch all the objects from the entity named "Test":
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Test"
inManagedObjectContext:context];
NSError *error;
[fetchRequest setEntity:entity];
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
fetchObjects array contains all the objects in "Test" entity
[fetchedResultsController fetchedObjects] will do just fine for your task.
In this case it is also not needed to use NSFetchedResultsController, you can get away with a simple NSFetchRequest.
I needed to update a lot of entities so the solution using NSFetchRequest were too slow. Luckily Apple added NSBatchUpdateRequest in iOS 8. Here's a simple example:
NSBatchUpdateRequest *batchUpdate = [[NSBatchUpdateRequest alloc] initWithEntityName:#"EntityName"];
batchUpdate.propertiesToUpdate = #{ #"attribute": #(0) };
batchUpdate.resultType = NSBatchDeleteResultTypeStatusOnly;
[managedObjectContext executeRequest:batchUpdate error:nil];
And here's a good blog post on the subject: https://www.bignerdranch.com/blog/new-in-core-data-and-ios-8-batch-updating/.

ios NSFetchRequest for two related tables

my program has a sqlite database with two related tables. One called "Rank" and other one called "Requirement"
I want to fetch all rows from the "Requirement" table that has a relationship with the specific row in a "Rank" table. Following is my code, it grabs the whole table, but I get the specified rows only according to the above mentioned rule.
-(NSArray *) getAllRequirementsForTheRank:(Rank *) rank
{
NSError *error;
NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init]autorelease];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Requirement" inManagedObjectContext:self.context];
[fetchRequest setEntity:entity];
NSPredicate *searchType = [NSPredicate predicateWithFormat:#"Rank = %#", rank];
[fetchRequest setPredicate:searchType];
NSArray *scoutRequirementArray = [self.context executeFetchRequest:fetchRequest error:&error];
for (Requirement *r in scoutRequirementArray)
{
NSLog(#"Requirementttt : %# :", r.requirementName);
}
return scoutRequirementArray;
}
If you have the relationship modelled in core data, just get the linked objects from the relationship property. You don't need another fetch request. rank.requirements will give you an NSSet of everything you need. (I'm assuming names for your object and properties here).

Resources