📜 ⬆️ ⬇️

Requests in CoreData with aggregate functions and grouping in one line

Almost everyone who uses CoreData sooner or later has to create queries with aggregate functions and groupings . However, the syntax of such queries in CoreData is difficult to understand and unnecessarily verbose.

Using the query designer, we can, for example, make such a query:
NSDictionary *productTotalSumAndAveragePriceGroupedByCountries = [[[[[Product all ] aggregatedBy:@[ @[kAggregateSum, @"amount"], @[kAggregatorAverage, @"price"]] ] groupedBy:@[@"country"] ] having:predicate ] execute]; 

This query is equivalent to:
Request in coredata
 NSFetchRequest *fetchRequest = [[ALFetchRequest alloc] init]; fetchRequest.managedObjectContext = managedObjectContext; NSString *entityName = @"Product"; NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:managedObjectContext]; [fetchRequest setEntity:entity]; [fetchRequest setIncludesPendingChanges:YES]; // sum amount NSExpression *fieldExp1 = [NSExpression expressionForKeyPath:@"amount"]; NSExpression *agrExp1 = [NSExpression expressionForFunction:agr arguments:@[fieldExp1]]; NSExpressionDescription *resultDescription1 = [[NSExpressionDescription alloc] init]; NSString *resultName1 = @"sumAmount"; [resultDescription1 setName:resultName1]; [resultDescription1 setExpression:agrExp1]; [resultDescription1 setExpressionResultType:NSInteger64AttributeType]; // average price NSExpression *fieldExp2 = [NSExpression expressionForKeyPath:@"price"]; NSExpression *agrExp2 = [NSExpression expressionForFunction:agr arguments:@[fieldExp1]]; NSExpressionDescription *resultDescription2 = [[NSExpressionDescription alloc] init]; NSString *resultName2 = @"sumAmount"; [resultDescription2 setName:resultName2]; [resultDescription2 setExpression:agrExp2]; [resultDescription2 setExpressionResultType:NSInteger64AttributeType]; // country NSDictionary *availableKeys = [entity attributesByName]; NSAttributeDescription *country = [availableKeys valueForKey:@"country"]; fetch.propertiesToFetch = [NSArray arrayWithObjects:country, resultDescription1, resultDescription2, nil]; fetch.propertiesToGroupBy = [NSArray arrayWithObject:country]; fetch.resultType = NSDictionaryResultType; NSError *error; NSManagedObjectContext *managedObjectContext = self.managedObjectContext; NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:self error:&error]; if (!fetchedObjects || error) { NSLog(@"Error: Execution of the fetchRequest: %@, Failed with Description: %@",self,error); } return fetchedObjects; 



In this article I would like to talk about a small library for working with CoreData, which appeared as a summary of my modest development experience under iOS. The library is available in cocoapods .

The core of the library is the singlelton class ALCoreDataManager, which is responsible for initializing and connecting the CoreData stack and returns NSManagedObjectContext. I note that this is a completely ordinary thing and there are a lot of analogues in this regard. All the goodies are contained in the ALFetchRequest + QueryBuilder category and in the class ALManagedObjectFactory. But first things first.
')

Library features


We will assume that the Product model is defined as:
 @interface Product : NSManagedObject @property (nonatomic, retain) NSString *title; @property (nonatomic, retain) NSNumber *price; @property (nonatomic, retain) NSNumber *amount; @property (nonatomic, retain) NSString *country; @end 

Requests


Using the query designer , we can form, for example, the following queries:
 NSArray *allProducts = [[Product all] execute]; NSArray *productsFilteredWithPredicate = [[[Product all] where:predicate] execute]; NSArray *singleProduct = [[[[Product all] where:predicate] limit:1] execute]; NSArray *onlyDistinctProductTitles = [[[[Product all] properties:@[@"title"]] distinct] execute]; NSArray *countProducts = [[[[Product all] where:predicate] count] execute]; // NSInteger count = [[countProducts firstObject] integerValue]; NSArray *productsOrderedByTitleAndPrice = [[[Product all ] orderedBy:@[ @[@"title", kOrderDESC], @[@"price", kOrderASC], @[@"amount"]] ] execute]; NSArray *totalAmountAndAveragePriceForProducts = [[[[[Product all ] aggregatedBy:@[ @[kAggregateSum, @"amount"], @[kAggregateAverage, @"price"]] ] groupedBy:@[@"country"] ] having:predicate ] execute]; 

The execute method is used directly to execute the query. To get the generated NSFetchRequest, use the request method. For example,
 NSFetchRequest *request = [[[Product all] orderedBy:@[@"title", @"price"]] request]; NSManagedObjectContext *context = [ALCoreDataManager defaultManager].managedObjectContext; NSFetchedResultsController *controller = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:context sectionNameKeyPath:nil cacheName:nil]; [controller performFetch:nil]; 

UITableViewDataSource


Upon request, you can get an object that implements the UITableViewDataSource protocol and is controlled by NSFetchedResultsController :
 ALTableViewDataSource *dataSource = [[[Product all] orderedBy:@[kTitle, kPrice]] tableViewDataSource]; self.dataSource.tableView = self.tableView; 

This will save us from writing boring code to implement the UITableViewDataSource protocol and the delegate for NSFetchedResultsController. A similar object can be obtained for a UICollectionViewDataSource using the collectionViewDataSource method.

Creating and deleting objects


Creating and deleting objects is possible using this API :
 Product *a = [Product create]; Product *b = [Product createWithDictionary:@{ @"title" : @"best product" }]; //   Factory- NSManagedObjectContext *context = [ALCoreDataManager defaultManager].managedObjectContext; ALManagedObjectFactory *factory = [[ALManagedObjectFactory alloc] initWithManagedObjectContext:context]; Product *c = [Product createWithDictionary:nil usingFactory:factory]; c.title = @"best product 2"; Product *d = [Product createWithDictionary:@{ @"title" : @"best product 3", @"price" : @(100) } usingFactory:factory]; [d remove]; //   

The last thing to note is that you must overload the + entityName method if the Entity Name of some ManagedObject does not match its Class Name (of course, this must be done in the appropriate category).
 @implementation Product + (NSString*)entityName { return @"AnItem"; } @end 


Example


Let us demonstrate the profit from using the library with an example . After downloading and unzipping the library, you must install the dependencies:
 cd / Users / you / Downloads / ALCoreDataManager-master / Example
 pod install


In Storyboard, everything is quite simple:

The first TableViewController displays a list of all Products; the second displays information on the selected Product, which can be edited there as well.

The table is populated using the ALTableViewDataSource mentioned earlier:
 - (void)viewDidLoad { [super viewDidLoad]; self.dataSource = [[[Product all] orderedBy:@[kTitle, kPrice]] tableViewDataSource]; __weak typeof(self) weakSelf = self; self.dataSource.cellConfigurationBlock = ^(UITableViewCell *cell, NSIndexPath *indexPath){ [weakSelf configureCell:cell atIndexPath:indexPath]; }; self.dataSource.reuseIdentifierBlock = ^(NSIndexPath *indexPath){ return TableViewCellReuseIdentifier; }; self.dataSource.tableView = self.tableView; } 

Believe it or not, this is all the code for the tableview.

By clicking on Add , we create an element, saying:
 [Product createWithFields:@{ kTitle : title, kPrice : @(0), kAmount : @(0) } usingFactory:[ALManagedObjectFactory defaultFactory]]; 

We will show some statistical information by clicking on Statistics :


After selecting the type of statistics, the code works:
 ALFetchRequest *request = nil; switch (st) { case ALStatsTypeTotalAmount: request = [[Product all] aggregatedBy:@[@[kAggregatorSum, kAmount]]]; break; case ALStatsTypeMedianPrice: request = [[Product all] aggregatedBy:@[@[kAggregatorAverage, kPrice]]]; break; default: break; } NSArray *result = [request execute]; // request    NSDictionaryResultType NSDictionary *d = [result firstObject]; //  : // { // sumAmount = 1473; // } 

This is the whole code with aggregator functions (for comparison, request with aggregate function (stackoverflow) ).

After execution, we get the following AlertView:


How it works


Request generation begins with a call:
 + (ALFetchRequest*)allInManagedObjectContext:(NSManagedObjectContext*)managedObjectContext; + (ALFetchRequest*)all; //   allInManagedObjectContext  defaultContext 

That is, it simply leads to the creation of NSFetchRequest:
 + (ALFetchRequest*)allInManagedObjectContext:(NSManagedObjectContext*)managedObjectContext { ALFetchRequest *fetchRequest = [[ALFetchRequest alloc] init]; fetchRequest.managedObjectContext = managedObjectContext; NSEntityDescription *entity = [self entityDescriptionWithMangedObjectContext:managedObjectContext]; [fetchRequest setEntity:entity]; [fetchRequest setIncludesPendingChanges:YES]; return fetchRequest; } 

Virtually all of the builder code is in ALFetchRequest + QueryBuilder.m .

Each of the calls of the form
 [[[Product all] orderedBy:@[kTitle, kPrice]] limit:1]; 

It simply adds the necessary settings to the created NSFetchRequest, for example:
 - (ALFetchRequest*)limit:(NSInteger)limit { self.fetchLimit = limit; return self; } 

Methods execute and request :
 - (NSArray*)execute { NSError *error; NSManagedObjectContext *managedObjectContext = self.managedObjectContext; NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:self error:&error]; if (!fetchedObjects || error) { NSLog(@"Error: Execution of the fetchRequest: %@, Failed with Description: %@",self,error); } return fetchedObjects; } - (NSFetchRequest *)request { return (NSFetchRequest*)self; } 

We can assume that this is just syntactic sugar for NSFetchRequest. Obviously, overhead is almost zero. Slightly more examples can be found in the tests .

At this story I hasten to finish. Thank you for attention.

Source: https://habr.com/ru/post/265319/


All Articles