Pull to refresh

Запросы в CoreData с агрегатными функциями и группировкой в одну строку

Reading time7 min
Views6K
Почти всем, кто использует CoreData, рано или поздно приходится создавать запросы с агрегатными функциями и группировками. Однако, синтаксис таких запросов в CoreData сложен для понимания и неоправданно многословен.

Используя конструктор запросов мы можем, например, сделать вот такой запрос:
NSDictionary *productTotalSumAndAveragePriceGroupedByCountries = 
[[[[[Product all
] aggregatedBy:@[
                 @[kAggregateSum, @"amount"],
                 @[kAggregatorAverage, @"price"]]
] groupedBy:@[@"country"]
] having:predicate
] execute];

Этот запрос эквивалентен такому:
Запросу в 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;



В данной статье мне бы хотелось рассказать о небольшой библиотеке для работы с CoreData, которая появилась как обобщение моего скромного опыта разработки под iOS. Библиотека доступна в cocoapods.

Ядром библиотеки является синглтон класс ALCoreDataManager, который отвечает за инициализацию и подключение стэка CoreData и возвращает NSManagedObjectContext. Замечу, что это совершенно ординарная вещь и аналогов в этом плане — очень много. Все вкусности содержатся в категории ALFetchRequest+QueryBuilder и фактори классе ALManagedObjectFactory. Но обо всем по порядку.

Возможности библиотеки


Будем полагать, что модель Product определена так:
@interface Product : NSManagedObject
@property (nonatomic, retain) NSString *title;
@property (nonatomic, retain) NSNumber *price;
@property (nonatomic, retain) NSNumber *amount;
@property (nonatomic, retain) NSString *country;
@end

Запросы


Используя конструктор запросов, мы можем формировать, например, следующие запросы:
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];

Метод execute используется непосредственно для выполнения запроса. Для получения сформированного NSFetchRequest предназначен метод request. Например,
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


По имеющемуся запросу можно получить обьект, реализующий протокол UITableViewDataSource и управлеяемый NSFetchedResultsControllerом:
ALTableViewDataSource *dataSource =    
    [[[Product all] orderedBy:@[kTitle, kPrice]] tableViewDataSource];

self.dataSource.tableView = self.tableView;

Что избавит нас от написания скучного кода для реализации протокола UITableViewDataSource и делегата для NSFetchedResultsController-а. Аналогичный объект можно получить для UICollectionViewDataSource используя метод collectionViewDataSource.

Создание и удаление объектов


Создание и удаление обьектов возможно с использованием такого 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]; // удаляем объект

Последнее, что нужно отметить — вы обязаны перегрузить метод +entityName, если Entity Name некоторого ManagedObject-а не совпадает с его Class Name (естественно, сделать это необходимо в соответствующей категории).
@implementation Product
+ (NSString*)entityName
{
    return @"AnItem";
}
@end


Example


Продемонстрируем профит от использования библиотеки на примере. После скачивания и разархивирования библиотеки необходимо установить зависимости:
cd /Users/you/Downloads/ALCoreDataManager-master/Example
pod install


В Storyboard-е все достаточно просто:

В первом TableViewController-е отображается список всех Product-ов; во втором отображается информация по выбранному Product-у, которую можно там же отредактировать.

Заполнение таблицы происходит с помощью упоминавшегося ранее ALTableViewDataSource-а:
- (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;
}

Верите или нет, но это весь код для TableView.

По нажатию на Add, создаем элемент, сказав:
[Product createWithFields:@{
                            kTitle : title,
                            kPrice : @(0),
                            kAmount : @(0)
                            }
             usingFactory:[ALManagedObjectFactory defaultFactory]];

Покажем некоторую статистическую информацию по нажатию на Statistics:


После выбора типа статистики отрабатывает код:
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;
// }

Вот и весь код с агрегаторными функциями (для сравнения — request с агрегатной функцией (stackoverflow)).

После выполнения получим такой AlertView:


Как это работает


Формирование запроса начинается с вызова:
+ (ALFetchRequest*)allInManagedObjectContext:(NSManagedObjectContext*)managedObjectContext;
+ (ALFetchRequest*)all; // внутри вызов allInManagedObjectContext с defaultContext

То есть просто приводит к созданию 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;
}

Практически весь код builder-а находится в ALFetchRequest+QueryBuilder.m.

Каждый из вызовов вида
[[[Product all] orderedBy:@[kTitle, kPrice]] limit:1];

Просто приводит к добавлению необходимых настроек в созданный NSFetchRequest, например:
- (ALFetchRequest*)limit:(NSInteger)limit
{
	self.fetchLimit = limit;
	return self;
}

Мотоды execute и 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;
}

Можно считать, что это просто syntactic sugar для NSFetchRequest. Очевидно, overhead практически равен нулю. Чуть больше примеров можно найти в тестах.

На этом повествование спешу закончить. Благодарю за внимание.
Tags:
Hubs:
+10
Comments9

Articles

Change theme settings