Как и многие разработчики, я не очень люблю писать много кода, особенно там, где это кажется не нужным — на ранних стадиях стараюсь придумать, как этот код оптимизировать и обобщить. Что касается непосредственно Core Data, мне всегда казалось, что все эти бесконечные фетчи и создания новых объектов можно упростить. Тогда я открыл для себя часто упоминаемый на хабре паттерн ActiveRecord и его очень хорошую (на мой взгляд) реализацию на Objective-C — MagicalRecord. Углубляться в описание не буду — все очень доступно описано на странице проекта.
Следующим шагом упрощения должен был быть маппинг данных, поступающих извне.
Для себя решил эту проблему в лоб и долгое время так и работал. Каждый ManagedObject-наследник содержал в себе следующий метод, который парсит входящий JSON-словарь:
Очевидно, что таких объектов достаточно много, к тому же в них есть еще и отношения. Первая мысль, которая приходит на ум — перебирать атрибуты и отношения в рантайме и сохранять результат в объекты. Ксожалению, радости, я не люблю изобретать велосипеды и недавно наткнулся на очень интересную реализацию данного подхода.
MagicalImport представляет собой набор категорий, которые расширяют функционал MagicalRecord и позволяют настроить маппинг JSON-объектов непосредственно в Core Data, при этом написание кода сведено к неприличному минимуму. Я не буду углубляться в подробности реализации и цели которые перед собой ставили разработчики, про все это хорошо написано вот тут. Остановимся на конкретном примере.
Возьмем для примера простенький запрос к Forsquare API, который будет возвращать нам список объектов поблизости от Эйфелевой башни.
URL запроса:
Для получения респонса я использовал AFNetworking. Класс-обертка для API:
Сформируем параметры запроса
kCLIENTID и kCLIENTSECRET — ключи для авторизации. Можно зарагистрировать свое приложение, можно использовать мои.
Запрос к серверу и получение данных выглядит так:
Массив venues будет содержать список мест, которые мы и будем мапить в нашу модель данных. JSON-объект venue:
Модель данных в данном примере состоит из двух объектов — Venue и Location и отношения one-to-one между ними.


Если JSON-ответ сервиса 'идеальный' и его модель полностью соответствует модели нашей базы, то для того, чтобы сделать импорт, необходимо добавить следующий код в completion блоке:
для импорта всего массива данных, или
для создания одного объекта.
Сразу оговорюсь, что MR_importFromArray почему-то не работает(тикет на github) поэтому для импорта я использовал следующий код:
К несчастью, ответы сервера не всегда нас радуют своей аккуратностью и название объектов и отношений зачастую не соответсвуют модели. Здесь на помощь приходит словарь UserInfo, который имеется у каждой сущности, атрибута или отношения. Он позволяет сконфигурировать маппинг для каждого из этих объектов.
Для перенастройки маппинга атрибута, необходимо добавить в этот словарь пару 'mappedKeyName' — 'название атрибута из JSON':

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

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

Прошу прощения за использования lat как 'primary key', естественно это было сделано только ради примера.
Механизм импорта предоставляет коллбэки, которые могут быть использованы для проверки/правки обрабатываемых данных (реализуются внутри сабклассов NSManagedObject):
Для примера реализуем проверку, исходя из которой будет устанавливать отношения только с теми объектами Location, которые содержат адрес:
Рассмотреный пример является тривиальным и поэтому не позволяет ознакомиться со всеми тонкостями MagicalImport (как и тот факт, что документации по нему пока еще нет), но как мне кажется, позволяет ощутить те плюсы, ради которых он и задумывался: отсутствие лишнего кода и гибкость реализации при импорте данных.
Тестовый проект можно найти здесь. (Для подключения MagicalRecord и AFNetworking был использован CocoaPods).
Следующим шагом упрощения должен был быть маппинг данных, поступающих извне.
Для себя решил эту проблему в лоб и долгое время так и работал. Каждый 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 между ними.


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':

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

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

Прошу прощения за использования 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).