Разработка iPhone/iPod touch приложений: CoreData. Что в имени твоем? (Часть 2) – Контексты и запросы
Разобравшись в основных понятиях Core Data, можно начинать им пользоваться. Для простоты и удобства, здесь и далее, будет предполагаться, что NSPersistenceStoreCoordinator, NSManagedObjectContext, NSManagedObjectModel уже созданы, и к ним есть доступ. Модель, которая будет использоваться в примерах, выглядит следующим образом:
Типы данных свойств объектов особого значения для примеров не имеют. Модель взята из “детского набора инженера-генетика”, который позволяет в домашних условиях выращивать животных нечто, с одним телом, одной головой, некоторым количеством рук и ног. Каждая часть тела имеет определенные свойства.
Начать стоит, как обычно, с заполнения БД. Необходимо напомнить, что Core Data – это не БД. Просто мы испольуем ее возможность сохранения данных из Core Data в БД.
Добавление объектов
Для добавления объекта нам понадобится – контекст 1 штука. Этого должно хватить. Инженер-генетик начинает с добавления тела жертвы:
[sourcecode language='objc']
//Простой вариант - объект сразу добавится в контекст
NSManagedObjectContext * context = [[StorageProvider sharedInstance] managedObjectContext];
// Получаем описание сущности через контекст (context -> storeeCorordinator -> model)
NSEntityDescription * entityDescription = [NSEntityDescription entityForName:@"Body" inManagedObjectContext:context];
Body * body = [[[Body alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:context] autorelease];
[body setWeight:[NSNumber numberWithInt:100]];
// Вариант создания без использования контекста
NSManagedObjectModel * model = [[StorageProvider sharedInstance] managedObjectModel];
// Получаем описание сущности через модель
entityDescription = [[model entitiesByName] objectForKey:@"Body"];
body = [[[Body alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:nil] autorelease];
[body setWeight:[NSNumber numberWithInt:200]];
//Положим объект в контекст позже
[context insertObject:body];
// Сохраняем контекст с двумя телами
NSError * error;
if (![context save:&error]) {
NSLog(@"Ошибка при сохранении в Базу : %@", [error userInfo]);
}
[/sourcecode]
Класс StorageProvider в примере – это мой класс-синглтон, в котором хранятся модель, контекст и многое другое.
Теперь надо прояснить ситуацию – откуда взялся NSEntityDescription? Объекты класса NSEntityDescription описывают ссвойства и связи сущности в модели (Entity). Так, на каждую сущность в модели есть свое описание. Это описание можно получить двумя способами (технически одним, конечно). В примере показаны оба способа – получение описания через контекст + имя сущности, и получение описания через модель + имя сущности. На самом деле оба способа работают одинаково – просто первый вариант получает по контексту persistenceStoreCoordinator, из него получает модель, а уже из модели по имени описание.
Здесь надо отметить две вещи – имя сущности может не совпадать с именем класса, и использование строковых констант не самый лучший выбор. Для устранения обоих недостатков можно, например, каждому классу сущности определить метод, который возвращает имя сущности в модели:
[sourcecode language='objc']
// Body.m
...
+(NSString *) entityName {
return @"SomeBody";
}
...
[/sourcecode]
Проблемы с добавлением?

Хорошо, когда все хорошо добавляется. А что делать, если все “плохо добавилось” или не добавилось совсем ? Где искать виновных? На примере видно, что если сохранение не пройдет, то в результате в [NSError userInfo] будет код ошибки. Так-то оно так, но “вменяемым” оно будет в том случае, если ошибку вызовет один объект. Если ошибку вызвали одновременно несколько объектов, то необходимы небольшие танцы с бубном, чтобы вытащить описание ошибки. Вот так выглядит [NSError userInfo], если “руки(arms) сделать обязательным параметром для тела(body)” :
[sourcecode language='objc'] NSDetailedErrors = ( Error Domain=NSCocoaErrorDomain Code=1580 UserInfo=0xf2e6c0 "Operation could not be completed. (Cocoa error 1580.)", Error Domain=NSCocoaErrorDomain Code=1580 UserInfo=0xf2e9e0 "Operation could not be completed. (Cocoa error 1580.)" ); [/sourcecode]
Я бы не стал рисковать сообщать инженеру-генетику о том, что он не может создать животных только потому, что возникли две ошибки 1580. Можно, конечно, подсмотреть в документации, и узнать, что произошла ошибка при валидации, но этого мало. Важно знать, какие объекты вызвали ошибку.
Делается это достаточно просто. Из той же документации узнаем, что по ключу NSDetailedErrorsKey в [error userInfo], в случае многочисленных ошибок, будет лежать массив с этими ошибками. Немного переписав код обработки ошибки при сохранении, получим следующее:
[sourcecode language='objc']
NSError * error;
if (![context save:&error]) {
NSDictionary * dict = [error userInfo];
NSArray * detailedErrors = [dict objectForKey:NSDetailedErrorsKey];
if (detailedErrors) {
NSLog(@"Все хорошо, но контекст не сохранен потому что : ");
for (NSError * detailedError in detailedErrors) {
NSLog(@"Есть ошибка : %@", [detailedError userInfo]);
}
} else {
NSLog(@"Ошибка при созранении в Базу : %@", [error userInfo]);
}
}
[/sourcecode]
В результате получим причну ошибки в более “удобочитаемом” виде.
Логика сохранения
Если не было ошибок и все прошло удачно, то все объекты были успешно сохранены в БД. В случае, если произошла ошибка, то все объекты, которые не были сохранены в БД, будут возвращены в [NSError userInfo]. Эти объекты можно получить по ключу NSValidationObjectErrorKey (в случае ошибок валидации). Это значит, что в случае, если из 100 объектов добавленых в контекст, не сохранится всего один, то 99 из них успешно будут добавлены в БД, и только один будет оставаться в контексте. Этот объект можно подправить, и снова вызвать [context save:&error]. И так до тех пор, пока не будут устранены все ошибки. Для нашего случая это может выглядеть следующим образом :
[sourcecode language='objc']
// Если нам нельзя сохранить объект
NSLog(@"Ошибка при созранении в Базу : %@", [error userInfo]);
Body * object = [[error userInfo] objectForKey:NSValidationObjectErrorKey];
[object setWeight:[NSNumber numberWithInt:[[object weight] intValue] + 10]];
// То создаем руку
Arm * arm = [[Arm alloc] initWithEntity:[NSEntityDescription entityForName:@"Arm" inManagedObjectContext:context]
insertIntoManagedObjectContext:context];
[arm setLength:[NSNumber numberWithInt:5]];
// Добавляем руку к телу
[object addArmsObject:arm];
// Снова сохраняем
if (![context save:&error]) {
...
}
[/sourcecode]
После того, как надоедает просто добавлять объекты в БД, возникает вполне логичный вопрос – а как их оттуда забирать?
NSFetchRequest – формирование запросов к БД
В Core-Data для запросов к БД используется класс с названием NSFetchRequest. Этот запрос выполняется конткекстом, и, в результате его выполнения, в контексте оказывается набор объектов, готовых к дальнейшему использованию.
NSFetchRequest очень “многофункциональный”. Мало того, что у него есть масса свойств и методов, которые влияют на возвращаемый результат, так у него есть еще и свойство/параметр predicate, который является аналогом SQL-запроса в БД.
Итак. Сразу начнем с примера – заберем из “лаборатории” все тела, вес которых превышает 150 кг:
[sourcecode language='objc'] //Создаем запрос NSFetchRequest * request = [[[NSFetchRequest alloc] init] autorelease]; // Указываем, какого типа будет возвращаемый результат и куда ложить результат [request setEntity:[NSEntityDescription entityForName:@"Body" inManagedObjectContext:context]]; // Указываем, что вес должен быть больше 150 кг [request setPredicate:[NSPredicate predicateWithFormat:@"weight > 150"]]; // Получаем результат NSArray * result = [context executeFetchRequest:request error:&error]; [/sourcecode]
Если вдруг, по каким-то причинам, у вас еще старая версия iPhone SDK 3.0, то надо добавить еще сортировку результата. Да и в принципе, лучше всегда сортировать данные, которые получаете из базы – порядок их возврата не известен даже самой базе;). Остортируем возвращаемые тела по размеру головы:
[sourcecode language='objc']
[request setSortDescriptors:
[NSArray arrayWithObject:
[[NSSortDescriptor alloc] initWithKey:@"head.size" ascending:YES]
]
];
[/sourcecode]
Формат предикатов я сейчас детально описывать не буду – для этого есть какая-никакая документация про формат предикатов (ключевое слово – “никакая”). Приведу несколько примеров формирования запросов :
[sourcecode language='objc']
// Тела с двумя руками
// В документации @count описан где-то далеко-далеко.
[request setPredicate:[NSPredicate predicateWithFormat:
@"arms.@count == 2"]];
//Получаем всех безруких и безногих
// AND и && работают одинаково
[request setPredicate:[NSPredicate predicateWithFormat:
@"arms.@count == 0 AND legs.@count == 0"]];
// Всех безногих, но с суммарной ддлиной рук больше 20
[request setPredicate:[NSPredicate predicateWithFormat:
@"arms.@sum.length > 20 AND legs.@count == 0"]];
// С короткими ногами и длинными руками
[request setPredicate:[NSPredicate predicateWithFormat:
@"legs.@avg.length < 5 AND arms.@avg.length > 10"]];
[/sourcecode]
Кстати, надо заметить что можно еще делать что-то на подобие такого:
[sourcecode language='objc']
- (NSNumber *) averageArmLength {
return [self valueForKeyPath:@"arms.@avg.length"];
}
[/sourcecode]
Однако использовать такие конструкции надо осторожно – не факт, что оно работает быстрее, чем цикл с вычислением среднего (не проверено). В общем, если что – я предупредил.
Несколько советов по повышению производительности запросов
Использование Page’инга
Стандартный прием, когда данные грузятся не все сразу, а по несколько штук. Для этого у NSFetchRequest’а есть свойства fetchLimit и fetchOffset. FetchLimit ограничивает количество отдаваемых результатов, а fetchOffset – смещение от начала.
Использование FetchedProperties
Для понимания принципа ускорения с помощью этого метода, надо знать о том, что после выполнения запроса в памяти объекты результата присуствуют не полностью. Используется т.н. отложенная инициализация. Если быть точнее, у этих объектов есть только id – а все остальные свойства все еще лежат в базе данных. И как только происходит попытка доступа к другим свойствам – только тогда идет запрос в БД за полной информацией об объекте.
То есть, если, допустим, необходимо использовать только одно свойство, но у всех объектов, то для ускорения следует включить его в fetchedProperties:
[sourcecode language='objc']
// Вытаскиваем следующие 100, начиная с 10
[request setFetchLimit:100];
[request setFetchOffset:10];
// Если дописать эту часть, то будет НАМНОГО быстрее,
// чем если было бы без нее
[request setPropertiesToFetch:[NSArray arrayWithObject:@"weight"]];
// В результате (в памяти) , кроме id объектов, есть и
// свойство weight
NSArray * result = [context executeFetchRequest:request error:&error];
int sum = 0;
for (Body * somebody in result) {
sum += [[somebody weight] intValue];
}
[/sourcecode]
В примере выполнится ровно один запрос в базу данных. Если бы свойство weight не указывалось в fetchedProperties, то количество запросов в базу было бы равно количеству объектов в результате + 1.
Используя fetchedProperties надо помнить и про память – чем больше свойств объектов вытягивается из базы данных – тем больше памяти используется.
Пора оставить инженера-генетика, и дать ему попробовать осознать и попробовать возможности Core Data, о которых он узнал.
Продолжение следует.









Григорий
в 10:24, 28.09.2009В чем диаграммку модели делали?
Классный пост, наглядный (если бы я еще понимал что-то в айфон девелопменте :)))
Kilew
в 11:47, 28.09.2009Прямо в Xcode просто выделил ее – вот она и синяя. Она обычно в розовых тонах ;)
olgerd
в 10:08, 29.09.20092 Kilew: сенк за посты – ужо почитал, теперь осталось мне повторить это же в коде для закрепления!
svolskiy
в 13:53, 07.01.2010толково расписал, будет куда отсылать “молодых” читать о кордате. 10x
Sergey
в 22:12, 15.01.2010Описка – “Ошибка при созранении”
EvGeniyLell
в 0:36, 01.04.2010доброго времени суток
меня интересует где можно почитать или у кого по спрашивать про запросы к базе коредата?
при изучении и тестах нашел некоторые особенности (странности)
есть класс с некиеми полями и с ссылками (релятионшип) сам на себя
1) прямая ссылка “чаилдс” 1:м
2) обратная ссылка “парент” 1:1
пытаясь для тестов из базы выбрать только первичные классы (у кого нет родителей)
Parent.@count == 0
но запрос возвращает полностью все записи как будто запрос был вообще без условия
но если изменить на
Parent.@count == 1
то все правильно показывает!! только тех у кого есть родитель
также правильно отображается (а точнее ничего не выводится)если указать
Parent.@count > 1
так как родителей у такой структуры больше одного нет
странно то что в выборку Parent.@count == 0 входят данные из выборки Parent.@count == 1
получается что коре дата утверждает что
0==1 но при этом 1!=0
кто может объяснить этот феномен? кто глючит? я или коре?
очень буду рад если кто то откликница
evgeniylell@gmail.com
Kilew
в 1:33, 01.04.2010>EvGeniyLell
Ответил письмом, а вообще есть Google группа iphone разработчиков украинских ;) Люди там добрые, помогут чем смогут
http://groups.google.com/group/iphonedevcampua
Monochrome
в 2:19, 14.11.2010Добрый день. Очень интересно чуть более углубленное касание темы Core Data. Проблема в NSManagedObjectContext.
1-ая проблема.
Есть простая сущность состоящая из
record
parentRecord – отношение само на себя.
Допустим, я записал группу в базу. Сохранил объект в переменной.
Теперь хочу создать дочернюю запись.
Почему приложение крашится еще на присвоении
record.myParent = parentGroup; ????
parentGroup в базе есть. Немного поэкспериментровав – я понял, что несмотря на то, что parentGroup в базе уже лежит – её надо добавлять в NSManagedObjectContext еще раз! Меня это несколько удивило.
Не могли бы осветить этот вопрос?
2-ая проблема.
Я создал родительскую запись и подчиненную, с указанием на родителя.
А теперь я хочу создать еще одну, с тем же родителем.
Добавляю родительскую запись (она так и хранится в переменной) в NSManagedObjectContext, создаю подчиненную запись; её отправляю в контекст также…
Все вылетает при попытке сохранения. Debbuger говорит, что такой объект в базе уже существует (конечно существует) и записать ничего не получится. Все мои представления о реляционных СУБД тут похоже идут мимо.
Не поможете??
iTux
в 14:05, 11.02.2011Не очень толково, используем в коде свой класс а что там в классе не пишем :)
Павел Тайкало
в 14:51, 11.02.2011Все файлы были сгенерированы по модели на рисунке. никакой дополнительной логики не писалось ;)
kozak_ss
в 15:32, 30.03.2011як додавать руки і ноги код є, я зіштовхнувся із проблемою коли загружаю “тіло”, масив рук і ніг = 0, хоча якщо відкрити файл бази даних на диску – то вони є. поясніть пліз як правильно загружати масиви
zayats
в 2:55, 20.06.2011Добрый день.
Можно немного подробней о:
“Класс StorageProvider в примере – это мой класс-синглтон, в котором хранятся модель, контекст и многое другое.”
DBDeveloper
в 12:53, 17.08.2011Спасибо, очень помогло.