Core Data: импорт данных с минимумом кода

в 8:56, , рубрики: core data, MagicalRecord, разработка под iOS, метки: ,

Как и многие разработчики, я не очень люблю писать много кода, особенно там, где это кажется не нужным — на ранних стадиях стараюсь придумать, как этот код оптимизировать и обобщить. Что касается непосредственно Core Data, мне всегда казалось, что все эти бесконечные фетчи и создания новых объектов можно упростить. Тогда я открыл для себя часто упоминаемый на хабре паттерн ActiveRecord и его очень хорошую (на мой взгляд) реализацию на Objective-C — MagicalRecord. Углубляться в описание не буду — все очень доступно описано на странице проекта.
Следующим шагом упрощения должен был быть маппинг данных, поступающих извне.

Для себя решил эту проблему в лоб и долгое время так и работал. Каждый ManagedObject-наследник содержал в себе следующий метод, который парсит входящий JSON-словарь:

- (void)mapPropertiesFrom:(NSDictionary *)dictonary {
    self.identifier = [NSNumber numberWithInt:[[dictonary objectForKey:@"id"] intValue]];
    self.privacy = [NSNumber numberWithInt:[[dictonary objectForKey:@"public"] intValue]];
    self.author = [KWUser findFirstByAttribute:@"profileId" withValue:[dictonary objectForKey:@"profile"]];
    self.profileId = [NSNumber numberWithInt:[[dictonary objectForKey:@"profile"] intValue]];
    self.latitude = [NSNumber numberWithDouble:[[dictonary objectForKey:@"lat"] doubleValue]];
    self.longitude = [NSNumber numberWithDouble:[[dictonary objectForKey:@"lon"] doubleValue]];
    self.text = [dictonary objectForKey:@"text"];
    self.category = [dictonary objectForKey:@"category"];
    self.firstName = [dictonary objectForKey:@"firstname"];
    self.lastName =  [dictonary objectForKey:@"lastname"];
}

Очевидно, что таких объектов достаточно много, к тому же в них есть еще и отношения. Первая мысль, которая приходит на ум — перебирать атрибуты и отношения в рантайме и сохранять результат в объекты. К сожалению, радости, я не люблю изобретать велосипеды и недавно наткнулся на очень интересную реализацию данного подхода.

Magical Import

MagicalImport представляет собой набор категорий, которые расширяют функционал MagicalRecord и позволяют настроить маппинг JSON-объектов непосредственно в Core Data, при этом написание кода сведено к неприличному минимуму. Я не буду углубляться в подробности реализации и цели которые перед собой ставили разработчики, про все это хорошо написано вот тут. Остановимся на конкретном примере.

Получение данных

Возьмем для примера простенький запрос к Forsquare API, который будет возвращать нам список объектов поблизости от Эйфелевой башни.

URL запроса:
api.foursquare.com/v2/venues/search?v=20120602&ll=48.858%2C2.2944&client_secret=ILG5POWGBRBZDXLNPAGECAZOBC0KFPQAQ5SYOP51KFYANZ1B&client_id=HDEHROGPMARZ2O1JTK55VHXE4TTNGE0NQR4DBCKHFZULURJV>

Для получения респонса я использовал AFNetworking. Класс-обертка для API:

+ (LDFourSquareAPIClient *)sharedClient {
    static LDFourSquareAPIClient *_sharedClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedClient = [[LDFourSquareAPIClient alloc] initWithBaseURL:[NSURL URLWithString:kBaseURL]];
    });
    return _sharedClient;
}

- (id)initWithBaseURL:(NSURL *)url {
    if (self = [super initWithBaseURL:url]) {
        [self registerHTTPOperationClass:[AFJSONRequestOperation class]];
        [self setDefaultHeader:@"Accept" value:@"application/json"];
    }
    return self;
}

Сформируем параметры запроса

   NSString *latLon = @"48.858,2.2944";
    NSString *clientID = [NSString stringWithUTF8String:kCLIENTID];
    NSString *clientSecret = [NSString stringWithUTF8String:kCLIENTSECRET];
    NSDictionary *queryParams;
    queryParams = [NSDictionary dictionaryWithObjectsAndKeys:latLon, @"ll", clientID, @"client_id", clientSecret, @"client_secret", @"20120602", @"v", nil];

kCLIENTID и kCLIENTSECRET — ключи для авторизации. Можно зарагистрировать свое приложение, можно использовать мои.

Запрос к серверу и получение данных выглядит так:

   [[LDFourSquareAPIClient sharedClient] getPath:@"v2/venues/search" parameters:queryParams success:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSArray *venues = [[responseObject objectForKey:@"response"] objectForKey:@"venues"];
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        
    }];

Массив venues будет содержать список мест, которые мы и будем мапить в нашу модель данных. JSON-объект venue:

categories =     (
                {
            icon =             {
                name = ".png";
                prefix = "https://foursquare.com/img/categories/building/default_";
                sizes =                 (
                    32,
                    44,
                    64,
                    88,
                    256
                );
            };
            id = 4bf58dd8d48988d12d941735;
            name = "Monument / Landmark";
            pluralName = "Monuments / Landmarks";
            primary = 1;
            shortName = Landmark;
        }
    );
    contact =     {
        formattedPhone = "+33 892 70 12 39";
        phone = "+33892701239";
    };
    hereNow =     {
        count = 3;
        groups =         (
                        {
                count = 3;
                items =                 (
                );
                name = "Other people here";
                type = others;
            }
        );
    };
    id = 4adcda09f964a520dd3321e3;
    likes =     {
        count = 0;
        groups =         (
        );
    };
    location =     {
        address = "Parc du Champ de Mars";
        city = Paris;
        country = France;
        crossStreet = "5 av. Anatole France";
        distance = 42;
        lat = "48.85836229464931";
        lng = "2.2945761680603027";
        postalCode = 75007;
        state = "U00cele de France";
    };
    name = "Tour Eiffel";
    specials =     {
        count = 0;
        items =         (
        );
    };
    stats =     {
        checkinsCount = 31211;
        tipCount = 430;
        usersCount = 22307;
    };
    url = "http://www.tour-eiffel.fr";
    verified = 0;
}

Data Model

Модель данных в данном примере состоит из двух объектов — Venue и Location и отношения one-to-one между ними.

Core Data: импорт данных с минимумом кода

Core Data: импорт данных с минимумом кода

Data Import

Если JSON-ответ сервиса 'идеальный' и его модель полностью соответствует модели нашей базы, то для того, чтобы сделать импорт, необходимо добавить следующий код в completion блоке:

self.data = [Venue MR_importFromArray:venues];

для импорта всего массива данных, или

Venue *myVenue = [Venue MR_importFromObject:[venues objectAtIndex:0]];

для создания одного объекта.

Сразу оговорюсь, что MR_importFromArray почему-то не работает(тикет на github) поэтому для импорта я использовал следующий код:

NSMutableArray *arr = [NSMutableArray array];
for (NSDictionary *venueDict in venues) {
     [arr addObject:[Venue MR_importFromObject:venueDict]];
}
self.data = arr;

К несчастью, ответы сервера не всегда нас радуют своей аккуратностью и название объектов и отношений зачастую не соответсвуют модели. Здесь на помощь приходит словарь UserInfo, который имеется у каждой сущности, атрибута или отношения. Он позволяет сконфигурировать маппинг для каждого из этих объектов.

Для перенастройки маппинга атрибута, необходимо добавить в этот словарь пару 'mappedKeyName' — 'название атрибута из JSON':

Core Data: импорт данных с минимумом кода

Также, этот маппинг поддерживает KVC, что очень полезно, если нет желания создавать вложеные сущности в модели (избавляемся от сущности Stats, получаем доступ к количеству чекинов):

Core Data: импорт данных с минимумом кода

Каждый объект в модели должен иметь нечто вроде primaryKey: MagicalImport будет искать атрибут с именем objectNameID либо мы можем указать такой атрибут сами в UserInfo (на примере отношения между Location и Venue):

Core Data: импорт данных с минимумом кода

Прошу прощения за использования lat как 'primary key', естественно это было сделано только ради примера.

Import сallbacks

Механизм импорта предоставляет коллбэки, которые могут быть использованы для проверки/правки обрабатываемых данных (реализуются внутри сабклассов NSManagedObject):

  • willImport:
  • didImport:
  • shouldImport'relationshipName'(id)data:

Для примера реализуем проверку, исходя из которой будет устанавливать отношения только с теми объектами Location, которые содержат адрес:

- (BOOL)shouldImportLocation:(id)location {
    NSString *address = [location objectForKey:@"address"];
    return address ? YES : NO;
}

Заключение

Рассмотреный пример является тривиальным и поэтому не позволяет ознакомиться со всеми тонкостями MagicalImport (как и тот факт, что документации по нему пока еще нет), но как мне кажется, позволяет ощутить те плюсы, ради которых он и задумывался: отсутствие лишнего кода и гибкость реализации при импорте данных.
Тестовый проект можно найти здесь. (Для подключения MagicalRecord и AFNetworking был использован CocoaPods).

Автор: garnett

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js