воскресенье, 10 июля 2016 г.

43. CoreData Part 3 Fetching

Продолжаем изучение CoreData. Видео к уроку 43 находится здесь.
В этом уроке мы разберемся как извлекать данные из базы, применяя условия, сортировки и т.д.
В прошлом уроке мы добавили сущность TACourse которая соответсвует предметам в университете.
Для вывода названия курса и студентов, добавим метод:

- (void)printCourse:(TACourse *) courseForPrint
{
    NSString *strReturn = [NSString stringWithFormat:@"TACourse: %@. Students:  ",
                           courseForPrint.nameCourse];
    for (TAStudent *student in courseForPrint.relationStudent) {
        strReturn = [NSString stringWithFormat:@"%@ %@", strReturn, student.lastName];
    }
    NSLog(@"%@", strReturn);

}

Так же добавим информацию о  курсе в функции вывода студента и университета.

Создадим университет и добавим в него 5 курсов.
Затем для 100 студентов  добавим курсы (рандомно до 5 курсов для каждого студента).

    TAUniversity *university = [self addUniversity];
    NSArray *arrayCourses = @[
                              [self addCourseWithName:courseName[0]],
                              [self addCourseWithName:courseName[1]],
                              [self addCourseWithName:courseName[2]],
                              [self addCourseWithName:courseName[3]],
                              [self addCourseWithName:courseName[4]]
                              ];
    [university addRelationCourse:[NSSet setWithArray:arrayCourses]];
    

    for (int i = 0; i < 100; i++) {
        TAStudent *student = [self addRandomStudent];
        TACar *car = [self addRandomCar];
        student.carRelation = car;
        [university addRelationStudentObject:student];
        NSInteger numberCourses = arc4random_uniform(5);
        while (student.relationCourse.count < numberCourses) {
            TACourse *courseForStudent = arrayCourses[arc4random_uniform(5)];
            if (![student.relationCourse containsObject:courseForStudent]) {
                [student addRelationCourseObject:courseForStudent];
            }
        }
    }
    
    NSError *error;
    if (![self.managedObjectContext save:&error]) {
        NSLog(@"%@",[error localizedDescription]);
    };


    [self printAllObjects];


Рассмотрим запрос студентов из базы. Нам редко нужны сразу все студенты содержащиеся в базе. К примеру для первичного вывода в таблицу нужны 20 студентов, а остальные записи нужно подгружать по мере скрола таблицы.
Для реализации этой функции в запрос нужно прописать размер пачки, по которой будет браться студенты из базы: [request setFetchBatchSize:20];

    NSFetchRequest *request = [[NSFetchRequest alloc]init];
    
    NSEntityDescription *description  = [NSEntityDescription entityForName:@"TAStudent" inManagedObjectContext:self.managedObjectContext];
    [request setEntity:description];
    [request setFetchBatchSize:20];
    NSError *requestError = nil;
    NSArray *arrayResult = [self.managedObjectContext executeFetchRequest:request error:&requestError];
    

    [self printObjects:arrayResult];

Если например взять первые 50 записей в массиве, то видно, что запрос сделан 3 раза по 20 записей:

    arrayResult = [arrayResult subarrayWithRange:NSMakeRange(0, 50)];

Для того, чтобы взять первые 35 записей из базы, нужно установить лимит:

[request setFetchLimit:35];

Для того, чтобы установить смещение от начала базы устанавливаем оффсет:

    [request setFetchOffset:10];

Теперь по запросу будут выведены студенты с 10 по 45 по 20 штук в пачке.

Если мы выводе студента используем информаци о курсах, то каждый раз делается дополнительный запрос в базу на загрузку курсов связанных с текущим студентом.
Это замедляет работу.
Чтобы данные с нужными связями подгрузились сразу, нужно указать какие связи подгружать:

    [request setRelationshipKeyPathsForPrefetching:@[@"relationCourse"]];

Таким образом, если свойство не будет обязательно использоваться для вывода, то оставляем загрузку по умолчанию, а если свойство 100% нам понадобится, делаем предазгрузку.

Сортировку получаемых данных лучше делать средствами CoreData, а не сортировать массивы после получения, т.к. сортировка через CoreData  занимает меньше времени и ресурсов.
Для сортировки студентов по фамилии, а при одинаковой фамилии по имени задаем два дескриптора в котором указываем по какому полю сортировать и вставляем их в запрос в нужном порядке (сначала сортировка фамилии, затем имени):

    NSSortDescriptor *firstNameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"firstName" ascending:YES];
    NSSortDescriptor *lastNameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastName" ascending:YES];

    [request setSortDescriptors:@[lastNameDescriptor, firstNameDescriptor]];

Для того, чтобы брать из базы студентов, удовлетворяющих определенным условиям, нужно использовать  NSPredicate:

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"score > %@", @3];
    

    [request setPredicate:predicate];

Мы создали предикат и указали, что нам нужны студенты с оценкой больше 3. Добавили предикат к запросу.

Если нам нужно устновить несколько условий (например вывести score >3 и <=3.5):

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"score > 3 AND score <= 3.5"];
или
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"score > %@ && score <= %@", @3, @3.5];

Для того, чтобы вывести студентов с оценкой  3 < score <=3.5 и количеством курсов больше 3, нужно использовать оператор коллекции @count:

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"score > %@ && score <= %@ && relationCourse.@count >= %@", @3, @3.5, @3];

Для того, чтобы к этим условиям добавить новое: имена студентов должны быть в массиве, строка предиката будет выглядеть так:

    NSArray *arrayValideName = @[@"Vanetta",@"Tran",@"Tandra"];
    

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"score > %@ && score <= %@ && relationCourse.@count >= %@ && firstName IN %@", @3, @3.5, @3, arrayValideName];

Сделаем запрос на вывод курсов, у которых средний бал студентов больше 2.9

    NSFetchRequest *request = [[NSFetchRequest alloc]init];
    
    NSEntityDescription *description  = [NSEntityDescription entityForName:@"TACourse" inManagedObjectContext:self.managedObjectContext];
    [request setEntity:description];
    
    [request setRelationshipKeyPathsForPrefetching:@[@"relationStudent"]];
    
    NSSortDescriptor *courseNameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"nameCourse" ascending:YES];

    [request setSortDescriptors:@[courseNameSortDescriptor]]; 
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"@avg.relationStudent.score > %@", @2.9];
    
    [request setPredicate:predicate];
    
    NSError *requestError = nil;
    NSArray *arrayResult = [self.managedObjectContext executeFetchRequest:request error:&requestError];
    

    [self printObjects:arrayResult];

Чтобы вывести курсы с суммой оценок студентов 160

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"@sum.relationStudent.score > %@", @160];

Чтобы вывести курсы, которые имеют студентов с оценкой больше 3.9

   NSPredicate *predicate = [NSPredicate predicateWithFormat:@"@max.relationStudent.score > %@", @3.9];

Чтобы сделать подзапрос для определения курсов у которых студенты ездят более, чем на  3 BMW, предикат выглядит так:

 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SUBQUERY(relationStudent, $student, $student.carRelation.model == %@).@count >= %@", @"BMW", 3];

Но у меня этот запрос вываливается с EXC_BAD_ACCESS (я к сожаению не разобрался, почему).

Альтернативным способом для выборки из базы сущностей с определенными критериями является добавление в файле модели нового Fetch Request.
Назовем наш запрос FetchStudents.
В списке Fetch All выбираем TAStudent.
Добавляем условие отбора  - жмем плюсик и выбираем  score -> is greater than -> 3.0.
Еще одно условие score -> is less than or equal to -> 3.5.
Если поменять режим страницы (правая кнопка), то мы увидим, как эти условия будут выглядеть в предикате: score > 3 AND score <= 3.5

Для того, чтобы воспользоваться интерактивно созданным запросом, нужно указать его имя:

    NSFetchRequest *request = [self.managedObjectModel fetchRequestTemplateForName:@"FetchStudents"];
    NSError *requestError = nil;
    NSArray *arrayResult = [self.managedObjectContext executeFetchRequest:request error:&requestError];
    

    [self printObjects:arrayResult];

Это удобно, когда нужно много раз использовать один и тот же запрос, т.е. вызывать запрос по имени. Если изменить этот запрос, то он изменится во всем приложении, что так же удобно.

Можно в этот запрос добавить сортировочные дескрипторы (в старом xCode для этого надо было сделать копию запроса (сейчас этого делать не нужно):

request = [request copy];

    NSSortDescriptor *firstNameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"firstName" ascending:YES];
    NSSortDescriptor *lastNameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastName" ascending:YES];

    [request setSortDescriptors:@[lastNameDescriptor, firstNameDescriptor]];

Последнее понятие рассмотренное в этом уроке это  Fetched Properties.
Это проперти возвращает всегда массив.
В файле модели выберем сущность TACourse и нажмем правую кнопку с плюсиком - Add Fetched Property.
Назовем новую Property - bestStudents.
Destination выбираем TAStudent.
В Predicate прописываем условие score > 3.5, т.е. в это проперти будет возвращены все студенты курса, у которых оценка больше 3.5 баллов (по аналогии с SUBQUERY).

Чтобы  обращаться через точку к новому проперти, добавим в файл с пропертями TACourse (TACourse+CoreDataProperties.h):

@property (nullable, nonatomic, retain) NSArray *bestStudents;

И объявим в файле TACourse+CoreDataProperties.m

@dynamic bestStudents;

Пересоздадим базу.

Выведем для каждого курса массив с лучшими студентами

    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSEntityDescription * description = [NSEntityDescription entityForName:@"TACourse" inManagedObjectContext:self.managedObjectContext];
    [request setEntity:description];
    [request setRelationshipKeyPathsForPrefetching:@[@"relationStudent"]];
    NSArray *arrayResult = [self.managedObjectContext executeFetchRequest:request error:nil];
    
    for (TACourse *course in arrayResult) {
        NSLog(@"Course: %@. BestStudents:", course.nameCourse);
        [self printObjects:course.bestStudents];
    }

Аналогично создаем  и выводим еще одно Fetched Property для студентов подписанных на много курсов: studentsWithManyCourses
Destination: TAStudent
Predicate: relationCourse.@count >= 4

Когда нужно обратиться к проперти в предикате, то используется выражение:
$FETCH_SOURCE.имя проперти

Файл с исходным кодом по уроку можно скачать здесь.


Комментариев нет:

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