从 API 到数据库的数据映射。将对象序列化到持久存储。在大多数数据密集型应用程序中,我们不断将一种类型的数据转换为另一种类型。
REST api 美观且易于解析,但即使在使用纯 REST api 的情况下,几乎从未有从服务器获取的内容到您在应用程序内部处理的内容之间干净的一对一映射。
我们使用了一些简单的映射机制,使我们能够轻松且声明性地将一种格式的数据映射到另一种格式。
我们还将添加一些入门文档,但它还远未准备好。但我们认为无论如何都应该分享,因为它可能对其他人有帮助。
EFDataMappingKit 有一个可用的 Podspec,因此如果您使用 CocoaPods,请将以下内容添加到您的 Podfile 中。
pod 'EFDataMappingKit'
到您的 Podfile。
文档可在此处查看 这里。
仍在早期开发中,但有一个很棒的代码生成器可在此处获得 这里。它接受您的 JSON 并为您创建基本映射。
以下是将 JSON 描述的用户映射到我们的 MYUser
和 MYMessage
对象的示例
{
"user_id": 42,
"username": "john.doe",
"messages": [
{
"message_id": 1,
"published_at": "2014-02-13",
"read": true,
"text": "FYI, tomorrow night I am hanging out with the guys!"
},
{
"message_id": 2,
"published_at": "2014-02-14",
"read": false,
"text": "Just kidding, romantic dinner by candle light awaits you!"
},
{
"message_id": 3,
"published_at": "2014-02-15",
"read": false,
"text": "Darling?!"
}
],
"website": "http://www.example.com"
}
这些对象具有以下接口
@interface MYUser : NSObject
@property (nonatomic, assign) NSUInteger userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, strong) NSArray *messages;
@property (nonatomic, strong) NSURL *website;
@end
@interface MYMessage : NSObject
@property (nonatomic, assign) NSUInteger messageId;
@property (nonatomic, strong) NSDate *publicationDate;
@property (nonatomic, assign) BOOL read;
@property (nonatomic, copy) NSString *text;
@end
您可以使用共享实例
EFMapper *mapper = [EFMapper sharedInstance];
但不需要
EFMapper *mapper = [[EFMapper alloc] init];
映射描述了从外部源检索的值应如何映射到内部实体上。
对于每个映射,您至少需要指定 externalKey
和 internalKey
(或使用 key
将两者设置相同)并指定您期望的值类型。对于例如 BOOL
、int
、CGFloat
这样的原始类型使用 NSNumber
。
我们建议为您的事务类创建一个类别,并在其中添加 + mappings
方法。
MYUser (Mappings)
实现
+ (NSArray *)mappings {
return @[
[EFMapping mapping:^(EFMapping *m) {
m.internalClass = [NSNumber class];
m.externalKey = @"user_id";
m.internalKey = @"userId";
m.requires = [EFRequires exists];
}],
[EFMapping mapping:^(EFMapping *m) {
m.internalClass = [NSString class];
m.key = @"username";
m.requires = [EFRequires exists];
}],
[EFMapping mappingForArray:^(EFMapping *m) {
m.internalClass = [MYMessage class];
m.key = @"messages";
}],
[EFMapping mapping:^(EFMapping *m) {
m.internalClass = [NSURL class];
m.key = @"website";
m.transformationBlock = ^id(id value, BOOL reverse) {
if (reverse) {
return [(NSURL *)value absoluteString];
} else {
return [NSURL URLWithString:(NSString *)value];
}
};
}]
];
}
MYMessage (Mappings)
实现
+ (NSArray *)mappings {
return @[
[EFMapping mapping:^(EFMapping *m) {
m.internalClass = [NSNumber class];
m.externalKey = @"message_id";
m.internalKey = @"messageId";
m.requires = [EFRequires exists];
}],
[EFMapping mapping:^(EFMapping *m) {
m.internalClass = [NSDate class];
m.externalKey = @"published_at";
m.internalKey = @"pulicationDate";
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd"];
m.formatter = dateFormatter;
}],
[EFMapping mappingForNumberWithKey:@"read"],
[EFMapping mappingForStringWithKey:@"text"]
];
}
使用 formatter
, transformer
和 transformBlock
来修改值。这对于日期与 NSDateFormatter
结合使用非常适用。您可以通过在 requires
属性上设置一个或多个 EFRequires
来进一步声明对值的要求。
为每个实体类注册一个 EFMapping
对象数组。
EFMapper *mapper = [EFMapper sharedInstance];
[mapper registerMappings:[MYUser mappings] forClass:[MYUser class]];
[mapper registerMappings:[MYMessage mappings] forClass:[MYMessage class]];
您可以将值应用到已存在的实例上,也可以请求初始化一个新对象。在应用值之前,映射器将验证值并通知您可能存在的问题。
应用到一个现有对象上:
EFMapper *mapper = [EFMapper sharedInstance];
NSDictionary *incomingValues = ...;
MYUser *existingObject = ...;
NSError *error;
if (![mapper setValues:incomingValues onObject:existingObject error:&error]) {
NSLog(@"Could not set values due to error: %@", EFPrettyMappingError(error));
}
创建一个新对象:
EFMapper *mapper = [EFMapper sharedInstance];
NSDictionary *incomingValues = ...;
NSError *error;
MYUser *newObject = [mapper objectOfClass:[MYUser class] withValues:incomingValues error:&error]);
if (!newObject) {
NSLog(@"Could not create new object due to error: %@", EFPrettyMappingError(error));
}
使用 formatter
, transformer
和 transformBlock
来修改值。这对于日期与 NSDateFormatter
结合使用非常适用。
在映射的 requires
属性上,您可以设置一个或多个 EFRequires
实例。所有 EFRequires
都必须通过才能认为值是有效的。您可以通过使用 +[EFRequires either:or:]
和 +[EFRequires not:]
来创建更复杂的要求。
包含值的数组或字典。
注册初始化器是可选的。如果没有指定初始化器,将通过调用对象的 alloc
和 init
方法来创建一个对象。如果您希望避免同一实体的多个实例,也可以返回一个现有的对象。注意,如果采取这种方法,可能会引入保留循环。
EFMapper *mapper = [[EFMapper alloc] init];
[mapper registerInitializer:^id(__unsafe_unretained Class aClass, NSDictionary *values) {
NSString *username = values[@"user_name"];
return [[aClass alloc] initWithUsername:username];
} forClass:[MYUser class]];
在某些情况下,您可能需要为特定类提供特殊的处理。您可以注册自定义映射器。
EFDataMappingKit 可以执行更多的操作以利用映射。
您也可以使用这些映射为您的对象快速添加 NSCoding
协议。
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
[[EFMapper sharedInstance] decodeObject:self withCoder:aDecoder];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeInteger:0 forKey:@"version"];
[[EFMapper sharedInstance] encodeObject:self withCoder:aCoder];
}
这些方法应在编码和解码过程中只调用一次,因此如果您的对象的超类已经调用了 EFMapper
的编码和解码方法,那么不要在您的子类中再次调用它们。
您可以将实体转换回字典/JSON 表示形式。
EFMapper *mapper = [EFMapper sharedInstance];
MYUser *userObject = ...;
NSDictionary *userDictionaryRepresentation = [mapper dictionaryRepresentationOfObject:userObject];
默认情况下,返回所有键,尽管您可以使用 -[EFMapper registerDictionaryRepresentationKeys:forClass:]
限制到子集。