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

44. CoreData Part 4 FRC

Это заключительный урок по CoreData.
В этом уроке рассматривается создание графического представления той базы данных, которую мы делали в прошлых уроках на базе стандартного шаблона приложений для CoreData.
Видео к этому урок находится здесь.
Исходный файл с базой и функциями для работы с ней можно скачать в прошлом уроке 43.

Чтобы использовать базу данных из любого контроллера, удобно вынести все функции в синглтон.
Для создания синглатона создаем файл TADataManager наследника от NSObject.

В  TADataManager.h создаем статический метод   + (TADataManager *)sharedManager; .
В .m файле определяем этот метод. Объявляем статическую переменную типа совпадающую с типом класса  и  вставляем сниппет/шаблон для однократного выполнения - dispatch_once:

+ (TADataManager *)sharedManager
{
    static TADataManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[TADataManager alloc] init];
    });
    return manager;

}

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

Теперь при вызове     TADataManager *sharedManager = [TADataManager sharedManager];
Мы получаем один и тот же  объект для работы с базой.
Этот объект не будет удаляться из памяти и будет жить до закрытия приложения.

Теперь перенесем, все что касается CoreData из AppDelegate в TADataManager.


Исходный код (незакончено) проекта к 44 уроку.

воскресенье, 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.имя проперти

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


понедельник, 4 июля 2016 г.

42. CoreData Part 2 Relationships

Продолжаем изучать CoreData.
Сегодня урок посвященный связям между сущностями в базе данных.
Видео с уроком находится здесь.
Продолжаем проект, созданный в прошлом уроке (можно скачать в уроке 42).

Мы создали новый объект - студент, сохранили его в базу.
Создали запрос в базу данных и вывели всех студентов в массив.

Для удаления всех студентов - помечаем их на удаление и сохраняем контекст:
    
    for (TAStudent *student in arrayResult) {
        NSLog(@"%@ %@ = %@",
                              student.firstName,
                              student.lastName,
                              student.score);
        [self.managedObjectContext deleteObject:student];
    }

    [self.managedObjectContext save:nil];

Для удаления большого количества студентов (если нам не нужны их проперти и нужно сократить время)  нужно запрещать парсить проперти.
В прошлом уроке рассматривали этот вопрос:

[request setResultType:NSDictionaryResultType];

А также  есть проперти  у NSFetchRequest:

@property (nonatomic) BOOL includesSubentities 

@property (nonatomic) BOOL includesPropertyValues 


Перенесем код в функции: запрос, вывод, удаление:


- (NSArray *)requestAllStudents
{
    NSFetchRequest *request = [[NSFetchRequest alloc]init];
    
    NSEntityDescription *description  = [NSEntityDescription entityForName:@"TAStudent" inManagedObjectContext:self.managedObjectContext];
    [request setEntity:description];
    NSError *requestError = nil;
    NSArray *arrayResult = [self.managedObjectContext executeFetchRequest:request error:&requestError];
    if (requestError) {
        NSLog(@"Request Error: %@", [requestError localizedDescription]);
    }
    return arrayResult;
}

- (void)printAllObjects
{
    NSArray *arrayResult = [self requestAllStudents];

    for (TAStudent *student in arrayResult) {
        NSLog(@"%@ %@ = %@",
              student.firstName,
              student.lastName,
              student.score);
    }

}


- (void)deleteAllStudents
{
    NSArray *arrayResult = [self requestAllStudents];
    
    for (TAStudent *student in arrayResult) {
        [self.managedObjectContext deleteObject:student];
    }
    [self.managedObjectContext save:nil];
}


Создадим новую сущность - автомобиль и назовем ее: TACar.
Создадим атрибут model типа String.

Студенту добавим связь - carRelation в Destination указываем TACar.
Указываем тип связи To One - один студент имеет одну машину.

В сущности TACar создаем ответную связь owner на студента и указываем связь To One. Машина имеет только одного владельца - студента.
Теперь определим тип правила удаления для TAStudent:

Nulify - это правило означает, что при удалении студента связь owner у соответствующей машины станет Null.

Если установить тип правила удаления  -  Cascade, то при удалении студента соответствующая машина тоже будет удалена.

Тип Deny означает, что если к примеру у студента есть 10 машин, то студент не удалится, пока не удалятся все его машины. Но иногда с ней происходят глюки (не рекомендуется использовать).

NoAction -  ничего не происходит при удалении.

Нам для TAStudent по логике больше подходит Cascade. Если уходит (удаляется) студент, то сведения об его машине нам не нужны, т.е.  TACar соответствующая тоже удаляется.

Для TACar выставляем правило для удаления - Nulify. Т.к. если удаляется машина, то студент может остаться.

Мы поменяли сущности и связи, пересоздадим класс для студента и машины. Для этого выделяем 2 сущности и нажимаем file-> new->file->core data->NSManagedObject subclass.

XCode автоматически сгенерирует файлы для классов в соответствии с настройками в модели.

Теперь у нас появилось 2 сущности TAStudent и TACar.
Чтобы вывести все объекты из базы за один запрос, необходимо создать базовую (родительскую сущность) и от нее унаследовать  наши классы студента и машины.
Создаем родительскую сущность TABaseObject.
А для класса TAStudent и TACar выставляем родителя:  Parent Entity - TABaseObject.

Теперь делаем методы для вывода всех объектов и удаления всех объектов:

- (NSArray *)arrayAllObject
{
    NSFetchRequest *request = [[NSFetchRequest alloc]init];
    
    NSEntityDescription *description  = [NSEntityDescription entityForName:@"TABaseObject" inManagedObjectContext:self.managedObjectContext];
    [request setEntity:description];
    NSError *requestError = nil;
    NSArray *arrayResult = [self.managedObjectContext executeFetchRequest:request error:&requestError];
    if (requestError) {
        NSLog(@"Request Error: %@", [requestError localizedDescription]);
    }
    return arrayResult;

}

- (void)deleteAllObjects
{
    NSArray *arrayResult = [self arrayAllObject];
    
    for (TABaseObject *object in arrayResult) {
        [self.managedObjectContext deleteObject:object];
    }
    [self.managedObjectContext save:nil];
}

- (void)printAllObjects
{
    NSArray *arrayResult = [self arrayAllObject];

    for (TABaseObject *baseObject in arrayResult) {
        NSLog(@"%@ ", baseObject);
    }

}

Метод для создания студента и  машины:

- (TAStudent *)addRandomStudent{
    TAStudent *student = [NSEntityDescription insertNewObjectForEntityForName:@"TAStudent" inManagedObjectContext:self.managedObjectContext];
    student.score = @(arc4random_uniform(201)/100.f +2.f);
    student.dateBirth = [NSDate dateWithTimeIntervalSince1970:arc4random_uniform(20)*60*60*24*365];
    student.lastName = lastNames[arc4random_uniform(50)];
    student.firstName = firstNames[arc4random_uniform(50)];
    return student;
}

- (TACar *)addRandomCar{
    TACar *car = [NSEntityDescription insertNewObjectForEntityForName:@"TACar" inManagedObjectContext:self.managedObjectContext];
    car.model = carModels[arc4random_uniform(5)];
    return car;
}

Для добавления связи, т.е. чтобы студент стал владельцем машины. Создаем студента, машину, указываем в carRelation новую машину и сохраняем контекст:

    TAStudent *student = [self addRandomStudent];
    TACar *car = [self addRandomCar];
    student.carRelation = car;
    NSError *error;
    if (![student.managedObjectContext save:&error]) {
        NSLog(@"%@",[error localizedDescription]);

    };


Для удобного вывода определим методы вывода для каждой сущности:

- (void)printStudent:(TAStudent*) studentForPrint
{
    NSString *strReturn = [NSString stringWithFormat:@"TAStudent: %@ %@ = %@, Car: %@",
                           studentForPrint.firstName,
                           studentForPrint.lastName,
                           studentForPrint.score,
                           studentForPrint.carRelation.model? studentForPrint.carRelation.model: @""];
            NSLog(@"%@", strReturn);
}

- (void)printCar:(TACar*) carForPrint
{
    NSLog(@"TACar: Model %@. Owner: %@ %@",
          carForPrint.model,
          carForPrint.owner.firstName? carForPrint.owner.firstName: @"",
          carForPrint.owner.lastName? carForPrint.owner.lastName: @"");

}


И общий метод вывода любого объекта:

- (void)printAllObjects
{
    NSArray *arrayResult = [self arrayAllObject];

    for (TABaseObject *baseObject in arrayResult) {
        if ([baseObject isKindOfClass:[TAStudent class]]) {
            [self printStudent:(TAStudent *)baseObject];
        }
        else if ([baseObject isKindOfClass:[TACar class]]){
            [self printCar:(TACar *)baseObject];
        }
        else{
            NSLog(@"Unknown type object: %@ ", baseObject);
        }
    }
}


Для проверки перед удалением определим метод в TAStudent.m для TAStudent и  аналогичный для TACar:

- (BOOL)validateForDelete:(NSError **)error
{
    NSLog(@"TAStudent ValidateForDelete");
    return YES;

}

Перед удалением студента вызвался этот метод для машины и студента (по логам).
При удалении машины, метод вызвался только для машины.

Это происходит по тому, что для студента правило удаления стоит  Cascade, а для машину Nulify.

Можно поместить этот метод в базовый класс TABaseObject.m

- (BOOL)validateForDelete:(NSError **)error
{
    NSLog(@"%@ ValidateForDelete", NSStringFromClass([self class]));
    return YES;
}

Проверим как работает связь один ко многим.
Для этого создадим новую сущность TAUniversity. Определим  связь со студентами один ко многим (университет имеет много студентов) и правило удаление - Cascade (удаляется университет - удаляются все студенты).
Для студента определим связь с университетом один к одному (т.к. для студента, только один университет).

Метод для вывода университета:
- (void)printUniversity:(TAUniversity*) universityForPrint
{
    NSString *strReturn = [NSString stringWithFormat:@"TAUniversity: %@. Students:  ",
                           universityForPrint.nameUniversity];
    for (TAStudent *student in universityForPrint.relationStudent) {
        strReturn = [NSString stringWithFormat:@"%@ %@", strReturn, student.lastName];
    }
    NSLog(@"%@", strReturn);

}

Для создания университета и добавления в него студента:

    TAUniversity *university;
        NSFetchRequest *request = [[NSFetchRequest alloc]init];
    
        NSEntityDescription *description  = [NSEntityDescription entityForName:@"TAUniversity" inManagedObjectContext:self.managedObjectContext];
        [request setEntity:description];
        NSError *requestError = nil;
        NSArray *arrayResult = [self.managedObjectContext executeFetchRequest:request error:&requestError];
        if (arrayResult.count > 0) {
            university = [arrayResult firstObject];
        }
        else{
            university = [NSEntityDescription insertNewObjectForEntityForName:@"TAUniversity" inManagedObjectContext:self.managedObjectContext];
            university.nameUniversity = @"NSTU";
        }


    TAStudent *student = [self addRandomStudent];
    TACar *car = [self addRandomCar];
    student.carRelation = car;
    [university addRelationStudentObject:student];
    
    NSError *error;
    if (![student.managedObjectContext save:&error]) {
        NSLog(@"%@",[error localizedDescription]);
    };

В конце добавляется еще сущность TACourse - курс (предмет) в университете.
Расставляются связи и правила в соответствии с логической моделью.

Ссылка на исходник  к уроку 42.

Домашнее задание в завершающем 44 -м уроке по CoreData.