Разработка 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]

Проблемы с добавлением?

add-problems
Хорошо, когда все хорошо добавляется. А что делать, если все “плохо добавилось” или не добавилось совсем ? Где искать виновных? На примере видно, что если сохранение не пройдет, то в результате в [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.2009

2 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

Спасибо, очень помогло.

Оставить комментарий