Почти всем, кто использует CoreData, рано или поздно приходится создавать запросы с агрегатными функциями и группировками. Однако, синтаксис таких запросов в CoreData сложен для понимания и неоправданно многословен.
Используя конструктор запросов мы можем, например, сделать вот такой запрос:
Этот запрос эквивалентен такому:
В данной статье мне бы хотелось рассказать о небольшой библиотеке для работы с CoreData, которая появилась как обобщение моего скромного опыта разработки под iOS. Библиотека доступна в cocoapods.
Ядром библиотеки является синглтон класс ALCoreDataManager, который отвечает за инициализацию и подключение стэка CoreData и возвращает NSManagedObjectContext. Замечу, что это совершенно ординарная вещь и аналогов в этом плане — очень много. Все вкусности содержатся в категории ALFetchRequest+QueryBuilder и фактори классе ALManagedObjectFactory. Но обо всем по порядку.
Будем полагать, что модель Product определена так:
Используя конструктор запросов, мы можем формировать, например, следующие запросы:
Метод execute используется непосредственно для выполнения запроса. Для получения сформированного NSFetchRequest предназначен метод request. Например,
По имеющемуся запросу можно получить обьект, реализующий протокол UITableViewDataSource и управлеяемый NSFetchedResultsControllerом:
Что избавит нас от написания скучного кода для реализации протокола UITableViewDataSource и делегата для NSFetchedResultsController-а. Аналогичный объект можно получить для UICollectionViewDataSource используя метод collectionViewDataSource.
Создание и удаление обьектов возможно с использованием такого API:
Последнее, что нужно отметить — вы обязаны перегрузить метод +entityName, если Entity Name некоторого ManagedObject-а не совпадает с его Class Name (естественно, сделать это необходимо в соответствующей категории).
Продемонстрируем профит от использования библиотеки на примере. После скачивания и разархивирования библиотеки необходимо установить зависимости:
В Storyboard-е все достаточно просто:
В первом TableViewController-е отображается список всех Product-ов; во втором отображается информация по выбранному Product-у, которую можно там же отредактировать.
Заполнение таблицы происходит с помощью упоминавшегося ранее ALTableViewDataSource-а:
Верите или нет, но это весь код для TableView.
По нажатию на Add, создаем элемент, сказав:
Покажем некоторую статистическую информацию по нажатию на Statistics:
После выбора типа статистики отрабатывает код:
Вот и весь код с агрегаторными функциями (для сравнения — request с агрегатной функцией (stackoverflow)).
После выполнения получим такой AlertView:
Формирование запроса начинается с вызова:
То есть просто приводит к созданию NSFetchRequest-а:
Практически весь код builder-а находится в ALFetchRequest+QueryBuilder.m.
Каждый из вызовов вида
Просто приводит к добавлению необходимых настроек в созданный NSFetchRequest, например:
Мотоды execute и request:
Можно считать, что это просто syntactic sugar для NSFetchRequest. Очевидно, overhead практически равен нулю. Чуть больше примеров можно найти в тестах.
На этом повествование спешу закончить. Благодарю за внимание.
Используя конструктор запросов мы можем, например, сделать вот такой запрос:
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 практически равен нулю. Чуть больше примеров можно найти в тестах.
На этом повествование спешу закончить. Благодарю за внимание.
ncix
Naftic
Это вместо NSFetchRequest-а. Упомянутый вами request полностью эквивалентен
Но читать его легче, на мой взгляд.
ncix
Я просто немного не понимаю в чем тут «сахар», если есть язык SQL, на котором все это выглядит куда понятнее. И этому языку уже не один десяток лет.
Я довольно далек от iOS — разработки, наверное дело в том, что нельзя просто так взять и сделать SQL-запрос из данного приложения к этим данным?
Naftic
Да, просто SQL запрос в CoreData сделать не получится. Но дело не только в этом.
В CoreData есть такая замечательная пара классов как NSFetchedResultsController и UITableViewController. Они очень оптимизированы и удобны в использовании. Так например, даже если в результате выполнения запроса мы получаем миллион записей, то FetchedResultsController загрузит в память только ту часть, которая видна из TableView. Если где-то в другом контроллере кто-то добавит, удалит или изменит содержимое отображаемых данных, то FRC автоматически стянет измененные данные в TableView с использованием приятных глазу анимаций.
greenkaktus
Выгоды от CoreData не всегда могут быть столь однозначны. Можно к SQLite взять ActiveRecords или еще какой FMDB и количество кода для UITableViewContoller будет почти таким же, а запросы будут на чистом SQL.
Тут каждый решает сам в каждом конкретном кейсе.
Naftic
По правде, я не использовал FMDB и интересно извещает ли он об изменениях. То есть, допустим я сделал селект всех продуктов, перешел на другой скрин и там добавил новый продукт. В первый TableView мне придет извещение, что данные изменились и их надо забрать?
stuit
Да, если Вы работаете только с sqlite. Но возможности CoreData намного шире чем работа только с одной базой данных.
greenkaktus
Конечно, я про sqlite паттерны только и написал. UIDocument никто не отменял вообщем-то)