CoreData Sort by relation - ios

My relation is shown on the picture below.
I'd like to sort Events first by those that are in active promotion and then by start date. Promotion is active if current date is between start and end date. Unfortunately, because of CoreData, I'm not able to use transient properties for sorting. In my controller I'm not using a fetch controller.
Is there any way to achieve that?
Update:
I've following sort descriptors:
// First is incorrect
[NSSortDescriptor(key: "promotion.start", ascending: false),
NSSortDescriptor(key: "start", ascending: true)]
Predicates (They're ok, though):
let promotionsPredicate =
NSPredicate(format: "(%# >= promotion.start && %# <= promotion.end) && " +
"(ANY promotion.cities.id == %#)", NSDate(), NSDate(), objectID)
let eventsPredicate =
NSPredicate(format: "start >= %# && venue.city.id == %#",
NSDate(), objectID)
let subpredicates = [eventsPredicate, promotionsPredicate]
let compoundPredicate NSCompoundPredicate(orPredicateWithSubpredicates: subpredicates)
And this is the Request (I'm using CoreStore, but the idea should be clear):
class func pagedEventsForPredicateSortedByInPromoAndStartDate(predicate: NSPredicate,
descriptors: [NSSortDescriptor],
fetchOffset: Int,
fetchLimit: Int) -> [Event] {
return CoreStore.fetchAll(From(Event),
Where(predicate),
OrderBy(descriptors),
Tweak { (fetchRequest) -> Void in
fetchRequest.fetchOffset = fetchOffset
fetchRequest.fetchLimit = fetchLimit
fetchRequest.returnsObjectsAsFaults = false
}) ?? []
}

As I understood you have to get all Event objects, but just in proper order. To do that with such complicated order, that includes relationship, as far as I know you have to fetch all Events and then sort them using NSArray's method
- (NSArray<ObjectType> *)sortedArrayUsingComparator:(NSComparator)cmptr
Here are the pieces of the code
1. Fetch from Core Data
// get the right context here
NSManagedObjectContext *yourContext;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Event"];
// extra line, predicate is nil by default, any other required predicate could be written here
request.predicate = nil;
__block NSArray *results = nil;
[yourContext performBlockAndWait:^{
NSError *error = nil;
results = [yourContext executeFetchRequest:request error:&error];
if (error) {
// handle error here
}
}];
Fetch is made manually with core methods and you may use Magical Record or any other framework that works with Core Data to make it in a row.
2. Sort the results
__weak typeof(self) weakSelf = self;
NSDate *now = [NSDate date];
NSArray *sortedResults = [results sortedArrayUsingComparator:^NSComparisonResult(Event *_Nonnull obj1, Event *_Nonnull obj2) {
BOOL isObj1InActivePromotion = [weakSelf date:now isBetweenDate:obj1.promotion.start andDate:obj1.promotion.end];
BOOL isObj2InActivePromotion = [weakSelf date:now isBetweenDate:obj2.promotion.start andDate:obj2.promotion.end];
// if they eather are in active promotion or no, just compare them by start date of the Event
if (isObj1InActivePromotion == isObj2InActivePromotion) {
return [obj1.start compare:obj2.start];
} else {
return isObj1InActivePromotion ? NSOrderedAscending : NSOrderedDescending;
}
}];
3. Additional method to work with NSDate
This method was used in sorting method
+ (BOOL)date:(NSDate *)date isBetweenDate:(NSDate *)beginDate andDate:(NSDate *)endDate
{
if ([date compare:beginDate] == NSOrderedAscending) {
return NO;
}
if ([date compare:endDate] == NSOrderedDescending) {
return NO;
}
return YES;
}
I could't check the code for obvious reasons, so sorry for any typos if they are.

I will suggest you to create "isActive" transection property in Promotion Entity to calculate active record.
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Event"];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
initWithKey:#“start” ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
[fetchRequest setIncludesPropertyValues:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sortDescriptor]];
After that you can filter fetch result by sorting :
NSPredicate *predicate = [NSPredicate predicateWithFormat:#“isActive == %#", #1];
NSMutableArray *finalResult = [NSMutableArray arrayWithArray:[results filteredArrayUsingPredicate:predicate]];
HTH.

Related

core data one to many relationship, assign, fetch, predicate on NSSet

Hy,
I wish to fetch all the songs those having playlist_index = 1, suppose. I have created one to many relation and generated accessor.
and but when i am fetching songs through predicate it always give me only one song although i have added same playlist to many songs. This is how i am adding playlist to song by converting pure NSManagedObject to coredata generated model object.
-(void) addPlaylistToAudio {
// Audio Coredata Model
Audio *audioManagedObject = [self getAudioManagedObject:self.artist];
PlaylistModel *playlistObj = [self.arrayPlaylist objectAtIndex:indexPath.row];
// Playlist Coredata Model
Playlist *playlistManagedObject = [self getPlaylistManagedObject:playlistObj];
[audioManagedObject addPlaylist_relationObject:playlistManagedObject];
[playlistManagedObject setAudio_relation:audioManagedObject];
// PLEASE CHECK HERE IF I AM ADDING CORRECTLY
NSManagedObjectContext *context = [[CoreDataHandler sharedInstance]
managedObjectContext];
NSError *error = nil;
if (![context save:&error]) {
}
else {
}
}
- (Audio *)getAudioManagedObject:(Artist *)artist {
// Create Predicate to fetch managed object.
NSPredicate *predicate = [NSPredicate
predicateWithFormat:#"track_index == %ld",artist.track_index];
// Get Predicate array of managed object.
NSArray *arrayAudioManagedObject = [[CoreDataHandler sharedInstance]
fetchAudioManagedObjectsWithPredicate:predicate havePredicate:TRUE];
// Managed object.
Audio *audioManagedObject = [arrayAudioManagedObject lastObject];
return audioManagedObject;
}
-(Playlist *)getPlaylistManagedObject:(PlaylistModel *)playlist {
// Create Predicate to fetch managed object.
NSPredicate *predicate = [NSPredicate
predicateWithFormat:#"playlist_index == %ld",playlist.playlist_index];
// Get Predicate array of managed object.
NSArray *arrayPlaylistManagedObject = [[CoreDataHandler sharedInstance]
fetchPlaylistManagedObjectsWithPredicate:predicate havePredicate:TRUE];
// Managed object.
Playlist *playlistManagedObject = [arrayPlaylistManagedObject
lastObject];
return playlistManagedObject;
}
Fetching:
NSPredicate *predicate_audio = [NSPredicate predicateWithFormat:#"ANY playlist_relation.playlist_index == %ld",self.playlistInfo.playlist_index];
NSArray *arraySongsManagedObject = [[CoreDataHandler sharedInstance] fetchAudioManagedObjectsWithPredicate:predicate_audio havePredicate:TRUE];
I think ANY will return just one result that meets your criteria, what you really need is SUBQUERY where you can replace your delegate with something like:
let predicate_audio = NSPredicate(format: "SUBQUERY(playlist_realtion, $x, $x.playlist_index == %ld).#count > 0", self.playlistInfo.playlist_index)
or in Objective-C
NSPredicate *predicate_audio = [NSPredicate predicateWithFormat:#"SUBQUERY(playlist_realtion, $x, $x.playlist_index == %ld).#count > 0", self.playlistInfo.playlist_index]
Note that $x is just a placeholder to represent each element relationship.
Links:
NSPredicate with SubQuery
http://davidchuprogramming.blogspot.com/2016/10/one-to-many-relationship-and-subquery.html
Let me know if it worked for you

Query max NSDate in core data

Is there a way to query NSDate in CoreData. For example if I want an entity with the highest NSDate value? I see that NSExpression "max:" only takes an NSNumber.
You can actually ask SQL for just that value, not the object with that value:
NSExpression *date = [NSExpression expressionForKeyPath:#"date"];
NSExpression *maxDate = [NSExpression expressionForFunction:#"max:"
arguments:[NSArray arrayWithObject:maxDate]];
NSExpressionDescription *d = [[[NSExpressionDescription alloc] init] autorelease];
[d setName:#"maxDate"];
[d setExpression:maxSalaryExpression];
[d setExpressionResultType:NSDateAttributeType];
[request setPropertiesToFetch:[NSArray arrayWithObject:d]];
NSError *error = nil;
NSArray *objects = [managedObjectContext executeFetchRequest:request error:&error];
if (objects == nil) {
// Handle the error.
} else {
if (0 < [objects count]) {
NSLog(#"Maximum date: %#", [[objects objectAtIndex:0] valueForKey:#"maxDate"]);
}
}
more detail under Fetching Managed Objects -> Fetching Specific Values in the CoreData documentation.
or
Perfomed a query, ordered on Date field DESCENDING, and using setFetchLim it:1.
Its not perfect, but at least it worked.
You can do this directly in SQLite-- without fetching everything and then filtering the result, and without the complexity of NSExpression.
To get the one object that has the max date, do something like (assuming entity name Entity and date attribute timeStamp):
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Event"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"timeStamp = self.#max.timeStamp"];
fetchRequest.predicate = predicate;
Do the fetch. You'll get (at most) one result, which will be the instance with the max date.
If you want to get just the date and not the entire managed object, add this before doing the fetch:
fetchRequest.resultType = NSDictionaryResultType;
fetchRequest.propertiesToFetch = #[ #"timeStamp" ];
you can get it with
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"tablename"];
fetchRequest.fetchLimit = 1;
fetchRequest.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"yourDate" ascending:NO]];
NSError *error = nil;
id person = [managedObjectContext executeFetchRequest:fetchRequest error:&error].firstObject;

NSFetchedResultsController triggered by NSManagedObject's refreshObject. Why?

I have a NSFetchedResultsController that fetches data from a CoreData store. When the frc is initialized I call performFetch and check the number of fetched objects in frc.fetchedObjects the result is 0 as it should be.
In another place in the code I call:
[obj.managedObjectContext refreshObject:obj mergeChanges:NO]
Although I'm not even sure it is needed it has the side effect of causing the frc to fetch some objects which were not fetched initially and shouldn't be fetched anyway considering the query used. These objects are exactly the same objects refreshObject:mergeChanges: was called with.
Why is this happening?
Edit:
It doesn't happen for this query:
query.predicate = [NSPredicate predicateWithFormat:#"(cartEntry != NULL) AND (urlWeb = NULL)", version, dateUrlExpire];
frc = [[NSFetchedResultsController alloc] initWithFetchRequest:query managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
frc.delegate = self;
[frc performFetch:&error];
But when I change the query to this version then it happens (urlWebVersion and urlWebDate are NULL for all records):
query.predicate = [NSPredicate predicateWithFormat:#"(cartEntry != NULL) AND ((urlWeb = NULL) OR (urlWebVersion != %#) OR (urlWebDate > %#))", version, dateUrlExpire];
* Edit #2 *
Here is a minimal code sample that shows the strange behavior (no error during execution):
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSError *error;
// Create an empty entity with the optional fields attr1 (string) and attr2 (date)
Entity *e = [NSEntityDescription insertNewObjectForEntityForName:#"Entity" inManagedObjectContext:[self managedObjectContext]];
// Save entity
[_managedObjectContext save:&error];
// Setup fetched results controller
NSPredicate *pred = [NSPredicate predicateWithFormat:#"(attr1 != %#) AND (attr2 != %#)", #"", [NSDate new], nil];
NSFetchRequest *query = [[NSFetchRequest alloc] initWithEntityName:#"Entity"];
query.predicate = pred;
query.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"attr1" ascending:NO]];
frc = [[NSFetchedResultsController alloc] initWithFetchRequest:query managedObjectContext:_managedObjectContext sectionNameKeyPath:nil cacheName:nil];
frc.delegate = self;
// Load data
[frc performFetch:&error];
// Output #1
NSLog(#"%ld", frc.fetchedObjects.count);
[_managedObjectContext refreshObject:e mergeChanges:NO];
return YES;
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
NSLog(#"%ld", controller.fetchedObjects.count);
}
Output:
2015-01-22 22:53:44.293 CoreDataBug[10740:1445186] 0
2015-01-22 22:53:44.317 CoreDataBug[10740:1445186] 1
Your problem is that NSPredicate has difficulty matching != to a nil value. For example, in your Edit 2 your line:
[NSPredicate predicateWithFormat:#"(attr1 != %#) AND (attr2 != %#)",
#"", [NSDate new], nil];
as you know, returns zero after the fetch request. (There's an extra nil there, but it doesn't affect anything). But changing it to:
NSPredicate *pred = [NSPredicate predicateWithFormat:
#"((attr1 != %#) || (attr1 == nil)) AND ((attr2 != %#) || (attr2 == nil))",
#"", [NSDate new]];
does return the entities, even though logically these are the same.
This is documented behavior, albeit hard to follow for something so counter-intuitive, see "Using Null Values" subsection in Predicate Programming Guide.
It's unclear why refreshing the object would cause the predicate to suddenly match; this may be a bug. However, given the above documentation, != should not be used for values that might be nil (unless checking != nil) without a paired || (x = nil). More than a little annoying and counter intuitive.

Updating object with core data inserts a new record

I'm trying to update a record in core data but it is actually inserting a new record. The following code is suposed to unlock the next level of my game.
- (void)unlockNextLevel
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *levelEntity = [NSEntityDescription entityForName:#"Level"
inManagedObjectContext:[self managedObjectContext]];
[fetchRequest setEntity:levelEntity];
int nextLevelPosition = [self.position intValue] + 1;
NSPredicate *predicate = [NSPredicate predicateWithFormat:
[NSString stringWithFormat:#"position == %i",
nextLevelPosition]];
[fetchRequest setPredicate:predicate];
NSError *error;
NSArray *result = [[self managedObjectContext] executeFetchRequest:fetchRequest
error:&error];
if (error) {
NSLog(#"%#", error.description);
}
if (result.count > 0) {
Level *nextLevel = result[0];
nextLevel.locked = [NSNumber numberWithBool:NO];
[self saveContext];
}
}
Here i fetch an object by the value of it's position, wich is supposed to be unique (by convention), and then change it's "locked" property. I'm expecting to get it updated but core data is inserting a new record...
The object I'm updating has a relation to the world it belongs. So a World has many Levels and a level only has one World.
I think it's worth mentioning that the record inserted in SQLite has no reference to a World (id of world is null).
Hope you can help me.
The following line is where the new "Level" entity is created.
NSEntityDescription *levelEntity = [NSEntityDescription entityForName:#"Level"
inManagedObjectContext:[self managedObjectContext]];
I think you can just initialise the fetch request using the following code instead:
NSFetchRequest *req = [[NSFetchRequest alloc] initWithEntityName:#"Level"];
and remove the following line:
[fetchRequest setEntity:levelEntity];
Update record in core data entity swift 4
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Users")
request.predicate = NSPredicate(format: "age = %# AND username = %#",self.dictData[0].value(forKey: "age") as! CVarArg,self.dictData[0].value(forKey: "username") as! CVarArg)
do {
let result = try context.fetch(request)
let objUpdate = result[0] as! NSManagedObject
let strUN = txtFldUserName.text!
let strPassword = txtFldPassword.text!
let strAge = txtFldAge.text!
// put all entity field
objUpdate.setValue(strUN, forKey: "username")
objUpdate.setValue(strPassword, forKey: "password")
objUpdate.setValue(strAge, forKey: "age")
print("update success!")
try context.save()
} catch {
print("Failed")
}
Hope this help!
Try this way. First fetch the required entity with some unique attribute value like articleID. If you get the entity as nil then create new entity instance or else update it.
Entity * entityInstance = nil;
entityInstance = [self fetchEntityForID:entityInstanceID inContext:context];
if (entityInstance == nil)
{
entityInstance = [NSEntityDescription insertNewObjectForEntityForName:#"Entity" inManagedObjectContext:context];
}
-(Entity *)fetchEntityForID:(NSNumber *) articleID inContext:(NSManagedObjectContext *) writeContext
{
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Entity"];
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"articleID == %#",articleID];
[fetchRequest setPredicate:predicate];
NSArray *fetchedArray = [writeContext executeFetchRequest:fetchRequest error:nil];
if ([fetchedArray count] > 0) {
return [fetchedArray objectAtIndex:0];
}
return nil;
}
Hope it helps :)

How to delete a selected number of rows from CoreData using NSPredicate?

I want to delete a selected list of items from a CoreData table: A number of Persons with certain namesToDelete:
NSError* error = nil;
NSFetchRequest* request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"Person"
inManagedObjectContext:managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"NOT (name IN %#)", namesToDelete];
[request setPredicate:predicate];
NSArray* deleteArray = [managedObjectContext executeFetchRequest:request error:&error];
if (error == nil)
{
for (NSManagedObject* object in deleteArray)
{
[managedObjectContext deleteObject:object];
}
[managedObjectContext save:&error];
//### Error handling.
}
else
{
//### Error handling.
}
This works, but is this the easiest/shortest way to do this in CoreData?
About the best trimming is something like:
NSError* error = nil;
NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:#"Person"];
[request setPredicate:[NSPredicate predicateWithFormat:#"NOT (name IN %#)", namesToDelete]];
NSArray* deleteArray = [managedObjectContext executeFetchRequest:request error:&error];
if (deleteArray != nil)
{
for (NSManagedObject* object in deleteArray)
{
[managedObjectContext deleteObject:object];
}
[managedObjectContext save:&error];
//### Error handling.
}
else
{
//### Error handling.
}
Note also that you check if the array is returned as the success criteria, not tat the error in nil. Likewise for the save: you should check the returned BOOL.
Prior to iOS 9 we delete the object one by one but on iOS 9.0+ we can delete them in a batch.
You can use NSBatchDeleteRequest available on iOS 9.0+, macOS 10.11+, tvOS 9.0+, watchOS 2.0+
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"NOT (name IN %#)", namesToDelete]];
NSFetchRequest *fetchRequest = [Person fetchRequest];
[fetchRequest setPredicate:predicate];
// Create batch delete request
NSBatchDeleteRequest *deleteReq = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fetchRequest];
deleteReq.resultType = NSBatchDeleteResultTypeCount;
NSError *error = nil;
NSBatchDeleteResult *deletedResult = [appDelegate.persistentContainer.viewContext executeRequest:deleteReq error:&error];
if (error) {
NSLog(#"Unable to delete the data");
}
else {
NSLog(#"%# deleted", deleteReq.result);
}
Swift code (from the above link)
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
fetch.predicate = NSPredicate(format: "terminationDate < %#", NSDate())
let request = NSBatchDeleteRequest(fetchRequest: fetch)
do {
let result = try moc.execute(request)
} catch {
fatalError("Failed to execute request: \(error)")
}
NOTE:
I found below comment about execute of moc
Method to pass a request to the store without affecting the contents of the managed object context.
Which means any unsaved data in moc won't be affected. i.e. if you've created/updated entity that falls in the delete request criteria and don't called save on moc then that object won't be deleted.
Yes, that's it. There is no SQL-like delete capability.
This is how i use in Swift.
func deleteRecentWithId(recentID : String)
{
let fetchRequest: NSFetchRequest<Recent_Message> = Recent_Message.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "recentUserOrGroupid = %#", recentID)
let request = NSBatchDeleteRequest(fetchRequest: fetchRequest as! NSFetchRequest<NSFetchRequestResult>)
do {
try context.execute(request)
try context.save()
} catch {
print ("There was an error")
}
}
Just change your table name and predicate condition. Enjoy!!!

Resources