Objective‑C版本的 Realm 能够让您以一种安全、耐用以及迅捷的方式来高效地编写应用的数据模型层
数据模型(Model)
Realm数据模型是基于标准 Objective‑C 类来进行定义的,使用属性来完成模型的具体定义。通过简单的继承 RLMObject
或者一个已经存在的模型类,您就可以创建一个新的 Realm 数据模型对象。
Realm模型对象在形式上基本上与其他 Objective‑C 对象相同 - 您可以给它们添加您自己的方法(method)和协议(protocol),和在其他对象中使用类似。
您只需要为对象的类型列表添加目标类型的属性,或者 RLMArray
,就可以创建数据关系(relationship)和嵌套数据结构(nested data structure)。
#import <Realm/Realm.h>
@class Person;
// 狗狗的数据模型
@interface Dog : RLMObject
@property NSString *name;
@property Person *owner;
@end
RLM_ARRAY_TYPE(Dog) // 定义RLMArray<Dog>
// 狗狗主人的数据模型
@interface Person : RLMObject
@property NSString *name;
@property NSDate *birthdate;
@property RLMArray<Dog> *dogs;
@end
RLM_ARRAY_TYPE(Person) // 定义RLMArray<Person>
支持的类型
Realm支持以下的属性类型:BOOL
int
NSInteger
long
float
double
NSString
NSDate
NSData
以及 被特殊类型标记的 NSNumber 。
您可以使用 RLMArray<Object *><Object>
和 RLMObject
的子类来建立诸如一对多、一对一之类的关系模型。
在 Xcode 7 以及之后的版本中,RLMArray支持编译时的 Objective‑C 泛型(generics)。下面是不同属性定义方法的意义以及用途:
// RLMArray: 属性类型。
// <Object *>: 属性的特别化(generic specialization),这可以阻止在编译时使用错误对象类型的数组。
// <Object>: 此RLMArray遵守的协议,可以让 Realm 知晓如何在运行时确定数据模型的架构。
关系(Relationships)
RLMObject
能够借助 RLMObject
以及 RLMArray
属性来和另一个 RLMObject
建立联系。 RLMArray
的接口和 NSArray
非常类似,在 RLMArray
中的对象能够通过索引下标(indexed subscripting)进行访问。 与 NSArray
所不同的是,RLMArray
的类型是固定的,其中只能存放简单的 RLMObject
子类类型。 要了解更详细的信息,请参阅 RLMArray。
假设现在您已经定义好了 Person 数据模型(见上文),让我们创建另一个名为 Dog 的数据模型:
// Dog.h
@interface Dog : RLMObject
@property NSString *name;
@end
对一(To-One)关系
对于多对一(many-to-one)或者一对一(one-to-one)关系来说,只需要声明一个 RLMObject
子类类型的属性即可:
// Dog.h
@interface Dog : RLMObject
// 其余属性声明...
@property Person *owner;
@end
您可以非常简单的通过这个属性完成关系的绑定:
Person *jim = [[Person alloc] init];
Dog *rex = [[Dog alloc] init];
rex.owner = jim;
当使用 RLMObject
属性的时候,您可以通过正常的属性访问语法来访问嵌套属性。比如说,rex.owner?.address.country
会依次读取对象的属性,然后自动从 Relam 中匹配所需的每一个对象。
对多(To-Many)关系
通过 RLMArray
类型的属性您可以定义一个对多关系。RLMArray
中可以包含简单类型的 RLMObject
,其接口与 NSMutableArray
非常类似。
RLMArray
可能会包含多个相同 Realm 对象的引用,即便对象带有主键也是如此。例如,您或许会创建一个空的 RLMArray
,然后连续三次向其中插入同一个对象;当使用 0、1、2 的索引来访问元素的时候,RLMArray
将会返回对应的对象,而所返回的这三个对象都是同一个对象。
如果要给我们的 Person
数据模型添加一个 “dogs”
属性,以便能够和多个 “dogs”
建立关系,也就是表明一个「人」可以养多条「狗」,那么我们首先需要定义一个 RLMArray<Dog>
类型。通过对应数据模型接口文件下的宏命令即可完成:
//Dog.h
@interface Dog : RLMObject
// 属性声明...
@end
**
RLM_ARRAY_TYPE(Dog) // 定义一个 RLMArray<Dog> 类型
RLM_ARRAY_TYPE 宏创建了一个协议,从而允许 RLMArray<Dog> 语法的使用。如果该宏没有放置在模型接口的底部的话,您或许需要提前声明该模型类。
**
接下来您就能定义RLMArray<Dog>类型的属性了:
```objc
// Person.h
@interface Person : RLMObject
// 其余的属性声明...
@property RLMArray<Dog *><Dog> *dogs;
@end
您可以和之前一样,对 RLMArray 属性进行访问和赋值:
// jim 是 rex 以及所有名字叫“Fido”的狗狗的主人
RLMResults<Dog *> *someDogs = [Dog objectsWhere:@"name contains 'Fido'"];
[jim.dogs addObjects:someDogs];
[jim.dogs addObject:rex];
注意:虽然可以给 RLMArray 属性赋值为 nil,但是这仅用于“清空”数组,而不是用以移除数组。这意味着您总是可以向一个 RLMArray 属性中添加对象,即使其被置为了 nil。
RLMArray 属性将确保其当中的插入次序不会被扰乱。
这里需要强调的是嵌套数据结构和数据关系的使用,即对 RLMArray 的使用。RLMArray 是Realm的数组,只能存放对象类型的数据。在使用 RLMArray 时需要注意:RLM_ARRAY_TYPE 宏创建了一个协议,从而允许 RLMArray<Dog> 语法的使用。如果该宏没有放置在模型接口的底部的话,您或许需要提前声明该模型类。
实用例
#import <Realm/Realm.h>
@interface BKUserPayWay : RLMObject
@property NSString *userPayWayName;
@property NSString *userPayWayValue;
@end
RLM_ARRAY_TYPE(BKUserPayWay) //定义RLMArray< BKUserPayWay >
@interface BKUserStatusInfo : RLMObject
@property NSString * userNum;
@property NSString * userPwd;
@property NSString * deveiceToken;
//有糖小店支付 信息
@property NSString * userSecretKey;
@property NSString * userValidTime;
@property RLMArray <BKUserPayWay *><BKUserPayWay>* userPayWays;
反向关系(Inverse Relationship)
链接是单向性的。因此,如果对多关系属性 Person.dogs
链接了一个 Dog
实例,而这个实例的对一关系属性 Dog.owner
又链接到了对应的这个 Person
实例,那么实际上这些链接仍然是互相独立的。为 Person
实例的 dogs
属性添加一个新的 Dog
实例,并不会将这个 Dog
实例的 owner
属性自动设置为该 Person
。但是由于手动同步双向关系会很容易出错,并且这个操作还非常得复杂、冗余,因此 Realm 提供了 “链接对象 (linking objects)”
属性来表示这些反向关系。
借助链接对象属性,您可以通过指定的属性来获取所有链接到指定对象的对象。例如,一个 Dog
对象可以拥有一个名为 owners
的链接对象属性,这个属性中包含了某些 Person
对象,而这些 Person
对象在其 dogs
属性中包含了这一个确定的 Dog
对象。您可以将 owners
属性设置为 RLMLinkingObjects
类型,然后重写 +[RLMObject linkingObjectsProperties]
来指明关系,说明 ownders
中包含了 Person
模型对象。
@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@property (readonly) RLMLinkingObjects *owners;
@end
@implementation Dog
+ (NSDictionary *)linkingObjectsProperties {
return @{
@"owners": [RLMPropertyDescriptor descriptorWithClass:Person.class propertyName:@"dogs"],
};
}
@end
可空属性(Optional Properties)
通常情况下,NSString *
、NSData *
以及 NSDate *
属性可以设置为 nil。如果你不需要实现此功能,你可以重写您的 RLMObject
子类的 +requiredProperties
方法。
比如对于以下的模型定义来说,如果尝试给 name
属性设置为 nil
将会抛出一个异常,但是将 birthday
属性设置为 nil
却是允许的:
@interface Person : RLMObject
@property NSString *name;
@property NSDate *birthday;
@end
@implementation Person
+ (NSArray *)requiredProperties {
return @[@"name"];
}
@end
存储可空数字目前已经可以通过 NSNumber *
属性完成。
由于 Realm 对不同类型的数字采取了不同的存储格式,因此设置可空的数字属性必须是 RLMInt
、RLMFloat
、RLMDouble
或者 RLMBool
类型。所有赋给属性的值都会被转换为其特定的类型。
请注意:NSDecimalNumber
的值只能分配给类型为 RLMDouble
的 Realm 属性,此外 Realm 将会存储近似于双精度浮点的数值,而不是存储基本的十进制数值。
比如说,如果我们存储一个用户的年龄(age)而不是存储他们的生日,同时还要允许当您不知道该用户的年龄的时候将 age
属性设置为 nil
@interface Person : RLMObject
@property NSString *name;
@property NSNumber<RLMInt> *age;
@end
@implementation Person
+ (NSArray *)requiredProperties {
return @[@"name"];
}
@end
RLMProperty
的子类属性始终都可以为 nil,因此这些类型不能够放在 requiredProperties
中,并且 RLMArray
不支持存储 nil
值。
简单说明
这个表格提供了关于声明模型属性的简易参考:
类型 | 非可选值形式 | 可选值形式 |
---|---|---|
Bool | @property BOOL value; | @property NSNumber<RLMBool> *value; |
Int | @property int value; | @property NSNumber<RLMInt> *value; |
Float | @property float value; | @property NSNumber<RLMFloat> *value; |
Double | @property double value; | @property NSNumber<RLMDouble> *value; |
String | @property NSString *value; | @property NSString *value; |
Data | @property NSData *value; | @property NSData *value; |
Date | @property NSDate *value; | @property NSDate *value; |
Object | n/a: 必须是可选值 | @property Object *value; |
List | @property RLMArray<Object *><Object> *value; | n/a: 必须是非可选值 |
LinkingObjects | @property (readonly) RLMLinkingObjects<Object *> *value; | n/a: 必须是非可选值 |
- Objective‑C 引用类型的必需属性必须要声明在联合体当中:
@implementation MyModel
+ (NSArray *)requiredProperties {
return @[@"value"];
}
@end
- 链接对象属性必须连带
+linkingObjectsProperties
方法一同声明:
@implementation MyModel
+ (NSDictionary *)linkingObjectsProperties {
return @{ @"property": [RLMPropertyDescriptor descriptorWithClass:Class.class propertyName:@"link"] };
}
@end
属性特性(attributes)
注意由于 Realm 在自己的引擎内部有很好的语义解释系统,所以 Objective‑C 的许多属性特性将被忽略,如 nonatomic
, atomic
, strong
, copy
和 weak
等。 因此为了避免误解,我们推荐您在编写数据模型的时候不要使用任何的属性特性。 当然,如果您已经设置了这些属性特性,那么在 RLMObject
对象被写入 Realm 数据库前,这些特性会一直生效。 无论 RLMObject
对象是否受到 Realm 管理,您为其编写的自定义 getter
和 setter
方法都能正常工作。
如果您在 Swift 中使用 Objective-C 版本的 Realm 的话,模型的属性前面需要加上 dynamic var
,这是为了让这些属性能够被底层数据库数据所访问。
索引属性(Indexed Properties)
重写 +indexedProperties
方法可以为数据模型中需要添加索引的属性建立索引:
@interface Book : RLMObject
@property float price;
@property NSString *title;
@end
@implementation Book
+ (NSArray *)indexedProperties {
return @[@"title"];
}
@end
Realm 支持字符串、整数、布尔值以及 NSDate
属性作为索引。
对属性进行索引可以减少插入操作的性能耗费,加快比较检索的速度(比如说 = 以及 IN 操作符)。
属性默认值
重写+defaultPropertyValues
可以每次在对象创建之后为其提供默认值。
@interface Book : RLMObject
@property float price;
@property NSString *title;
@end
@implementation Book
+ (NSDictionary *)defaultPropertyValues {
return @{@"price" : @0, @"title": @""};
}
@end
对象的自更新特性
RLMObject
实例是底层数据的动态表现,其会进行自动更新,这意味着对象不需要进行刷新。修改某个对象的属性会立刻影响到其他所有指向同一个对象的实例。
Dog *myDog = [[Dog alloc] init];
myDog.name = @"小白";
myDog.age = 1;
[realm transactionWithBlock:^{
[realm addObject:myDog];
}];
Dog *myPuppy = [[Dog objectsWhere:@"age == 1"] firstObject];
[realm transactionWithBlock:^{
myPuppy.age = 2;
}];
myDog.age; // => 2
RLMObject
的这个特性不仅让 Realm 保证速度和效率,它同时还让代码更加简洁、更为灵活。比如说,如果您的 UI 代码是基于某个特定的 Realm 对象来现实的,那么在触发 UI 重绘之前,您不用担心数据的刷新或者重新检索等问题。
您也可以查看 Realm 通知 一节以确认 Realm 数据何时被更新,比如说由此来决定应用 UI 何时需要被更新。此外,还可以使用 键值编码,当某个 RLMObject
的特定属性发生更新时去发送通知。
主键(Primary Keys)
重写 +primaryKey
可以设置模型的主键。声明主键之后,对象将允许进行查询,并且更新速度更加高效,而这也会要求每个对象保持唯一性。 一旦带有主键的对象被添加到 Realm 之后,该对象的主键将不可修改。
@interface Person : RLMObject
@property NSInteger id;
@property NSString *name;
@end
@implementation Person
+ (NSString *)primaryKey {
return @"id";
}
@end
忽略属性(Ignored Properties)
重写 +ignoredProperties
可以防止 Realm 存储数据模型的某个属性。Realm 将不会干涉这些属性的常规操作,它们将由成员变量(ivar)提供支持,并且您能够轻易重写它们的 setter
和 getter
。
@interface Person : RLMObject
@property NSInteger tmpID;
@property (readonly) NSString *name; // 只读属性将被自动忽略
@property NSString *firstName;
@property NSString *lastName;
@end
@implementation Person
+ (NSArray *)ignoredProperties {
return @[@"tmpID"];
}
- (NSString *)name {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
@end
忽略属性的行为与 Objective-C 或者 Swift 类当中的普通对象相似。它们并不支持任何一种 Realm 特定的功能。例如,无法通过查询来检索忽略属性,也无法实现自动更新,即便另一个相同对象的实例的忽略属性值发生了变更。此外忽略属性发生更改的时候也不会触发通知,尽管仍然可以使用 KVO 来实现简直观察。
模型继承
Realm 允许模型能够生成更多的子类,也允许跨模型进行代码复用,但是由于某些 Cocoa 特性使得运行时中丰富的类多态无法使用。以下是可以完成的操作:
父类中的类方法,实例方法和属性可以被它的子类所继承
子类中可以在方法以及函数中使用父类作为参数
以下是不能完成的:
多态类之间的转换(例如子类转换成子类,子类转换成父类,父类转换成子类等)
同时对多个类进行检索
多类容器 (RLMArray 以及 RLMResults)。
向 Realm 中增加此特性已经在规划当中,并且我们暂时提供了一些代码示例,以便能够对更常见的模式进行处理。
另外,如果您的代码实现允许的话,我们建议您使用以下模式,也就是使用类组合模式来构建子类,以便能够包含其他类中的相关逻辑:
// 基础模型
@interface Animal : RLMObject
@property NSInteger age;
@end
@implementation Animal
@end
// 包含有 Animal 的模型
@interface Duck : RLMObject
@property Animal *animal;
@property NSString *name;
@end
@implementation Duck
@end
@interface Frog : RLMObject
@property Animal *animal;
@property NSDate *dateProp;
@end
@implementation Frog
@end
// 用法
Duck *duck = [[Duck alloc] initWithValue:@{@"animal" : @{@"age" : @(3)}, @"name" : @"Gustav" }];
集合
Realm 拥有一系列能够帮助表示一组对象的类型,我们称之为「Realm 集合」:
1、RLMResults
类,表示从检索 中所返回的对象集合。
2、RLMArray
类,表示模型中的对多关系。
3、RLMLinkingObjects
类,表示模型中的反向关系。
4、RLMCollection
协议,定义了所有 Realm 集合所需要遵守的常用接口。
Realm 集合实现了 RLMCollection
协议,这确保它们能够保持一致。这个协议继承自 NSFastEnumeration
,因此它应当与其他 Foundation 当中的集合用法一致。 其他常用的 Realm 集合 API 也在这个协议当中进行了声明,例如其中包括检索、排序以及聚合操作。 RLMArray
拥有额外的修改操作,这些操作不在协议接口当中有定义,例如添加和删除对象。
使用 RLMCollection
协议,您可以编写能够操作任意 Realm 集合的泛型代码:
@implementation MyObject
- (void)operateOnCollection:(id<RLMCollection>)collection {
// collection 既可以是 RLMResults,也可以是 RLMArray
NSLog(@"对集合 %@s 进行操作", collection.objectClassName);
}
@end
对象存储
对对象的所有更改(添加,修改和删除)都必须通过写入事务(transaction)完成。
Realm 的对象可以被实例化并且作为unmanaged
对象使用(也就是还未添加到 Realm 数据库中的对象),和其他常规Objective‑C对象无异。
如果您想要在多个线程中共享对象,或者在应用重启后重复使用对象,那么您必须将其添加到 Realm 数据库中——这个操作必须在写入事务中完成。
因为写入事务将会产生不可忽略的性能消耗,因此你应当检视你的代码以确保减少写入事务的次数。
由于写入事务像其余硬盘读写操作一样,会出现失败的情况,因此 -[RLMRealm transactionWithBlock:]
以及 -[RLMRealm commitWriteTransaction]
可以选择加上 NSError
指针参数 因此你可以处理和恢复诸如硬盘空间溢出之类的错误。此外,其他的错误都无法进行恢复。简单起见,我们的代码示例并不会处理这些错误,但是您应当在您应用当中注意到这些问题。
创建对象
当定义完数据模型之后,您可以将您的 RLMObject
子类实例化,然后向 Realm 中添加新的实例。我们以下面这个简单的模型为例:
// 狗狗的数据模型
@interface Dog : RLMObject
@property NSString *name;
@property NSInteger age;
@end
// 实现文件
@implementation Dog
@end
我们可以用多种方法创建一个新的对象:
// (1) 创建一个狗狗对象,然后设置其属性
Dog *myDog = [[Dog alloc] init];
myDog.name = @"大黄";
myDog.age = 10;
// (2) 通过字典创建狗狗对象
Dog *myOtherDog = [[Dog alloc] initWithValue:@{@"name" : @"豆豆", @"age" : @3}];
// (3) 通过数组创建狗狗对象
Dog *myThirdDog = [[Dog alloc] initWithValue:@[@"豆豆", @3]];
使用指定初始化器(designated initializer)创建对象是最简单的方式。请注意,所有的必需属性都必须在对象添加到 Realm 前被赋值。
通过使用恰当的键值,对象还可以通过字典完成创建。
最后,RLMObject
子类还可以通过数组完成实例化,数组中的值必须和数据模型中对应属性的次序相同。
嵌套属性(Nested Object)
如果某个对象中有 RLMObject
或者 RLMArray
类型的属性,那么通过使用嵌套的数组或者字典便可以对这些属性递归地进行设置。您只需要简单的用表示其属性的字典或者数组替换每个对象即可:
// 这里我们就可以使用已存在的狗狗对象来完成初始化
Person *person1 = [[Person alloc] initWithValue:@[@"李四", @30, @[aDog, anotherDog]]];
// 还可以使用多重嵌套
Person *person2 = [[Person alloc] initWithValue:@[@"李四", @30, @[@[@"小黑", @5],
@[@"旺财", @6]]]];
即使是数组以及字典的多重嵌套,Realm 也能够轻松完成对象的创建。注意 RLMArray
只能够包含 RLMObject
类型,不能包含诸如 NSString
之类的基础类型。
添加数据
向 Realm 中添加数据的步骤如下:
// 创建对象
Person *author = [[Person alloc] init];
author.name = @"大卫·福斯特·华莱士";
// 获取默认的 Realm 实例
RLMRealm *realm = [RLMRealm defaultRealm];
// 每个线程只需要使用一次即可
// 通过事务将数据添加到 Realm 中
[realm beginWriteTransaction];
[realm addObject:author];
[realm commitWriteTransaction];
等您将某个对象添加到 Realm 数据库之后,您可以继续使用它,并且您对其做的任何更改都会被保存(必须在一个写入事务当中完成)。当写入事务提交之后,使用相同 Realm 数据源的其他线程才能够对这个对象进行更改。
请注意:如果在进程中存在多个写入操作的话,那么单个写入操作将会阻塞其余的写入操作,并且还会锁定该操作所在的当前线程。
这个特性与其他持久化解决方案类似,我们建议您使用该方案常规的最佳做法:将写入操作转移到一个独立的线程中执行。
由于 Realm 采用了 MVCC 设计架构,读取操作 并不会 因为写入事务正在进行而受到影响。除非您需要立即使用多个线程来同时执行写入操作,不然您应当采用批量化的写入事务,而不是采用多次少量的写入事务。
查看RLMRealm和RLMObject来获得更多内容。
更新数据
Realm 提供了一系列用以更新数据的方式,这些方式都有着各自所适应的情景。请选择最符合您当前需求的方式来使用:
内容直接更新
您可以在写入事务中通过设置某个对象的属性从而完成对象的更新操作。
// 在一个事务中更新对象
[realm beginWriteTransaction];
author.name = @"托马斯·品钦";
[realm commitWriteTransaction];
通过主键更新
如果您的数据模型中设置了主键的话,那么您可以使用+[RLMObject createOrUpdateInRealm:withValue:]
来更新对象,或者当对象不存在时插入新的对象。
// 创建一个带有主键的“书籍”对象,作为事先存储的书籍
Book *cheeseBook = [[Book alloc] init];
cheeseBook.title = @"奶酪食谱";
cheeseBook.price = @9000;
cheeseBook.id = @1;
// 通过 id = 1 更新该书籍
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:cheeseBook];
[realm commitWriteTransaction];
如果主键 id 为1的 Book 对象已经存在于数据库当中了,那么对象就会简单地进行更新。而如果不在数据库中存在的话,那么这个操作将会创建一个新的 Book 对象并添加到数据库当中。
您同时通过传递您想要更新值的集合,从而更新带有主键的某个对象的部分值,比如说如下所示:
// 假设带有主键值 `1` 的“书籍”对象已经存在
[realm beginWriteTransaction];
[Book createOrUpdateInRealm:realm withValue:@{@"id": @1, @"price": @9000.0f}];
// 这本书的`title`属性不会被改变
[realm commitWriteTransaction];
如果对象没有定义主键的话,那么您不能够调用出现在本章的这些方法(也就是那些以 OrUpdate
结尾的方法)。
请注意:当对象更新的时候,NSNull
仍然会被认为是可选属性 的有效值。如果您提供了一个属性值为 NSNull
的字典,那么这些都会应用到您的对象当中,并且对应的属性都将为空。为了确保不出现任何意外的数据丢失,请在使用此方法的时候再三确认只提供了您想要进行更新的属性。
键值编码
RLMObject
、RLMResult
以及 RLMArray
都遵守键值编码(Key-Value Coding)(KVC)机制。 当您在运行时才能决定哪个属性需要更新的时候,这个方法是最有用的。
将 KVC 应用在集合当中是大量更新对象的极佳方式,这样就可以不用经常遍历集合,为每个项目创建一个访问器了。
RLMResults<Person *> *persons = [Person allObjects];
[[RLMRealm defaultRealm] transactionWithBlock:^{
[[persons firstObject] setValue:@YES forKeyPath:@"isFirst"];
// 将每个人的 planet 属性设置为“地球”
[persons setValue:@"地球" forKeyPath:@"planet"];
}];
删除数据
通过在写入事务中将要删除的对象传递给 -[RLMRealm deleteObject:]
方法,即可完成删除操作。
// 存储在 Realm 中的 cheeseBook 对象
// 在事务中删除一个对象
[realm beginWriteTransaction];
[realm deleteObject:cheeseBook];
[realm commitWriteTransaction];
您也能够删除存储在 Realm 中的所有数据。注意,Realm 文件的大小不会被改变,因为它会保留空间以供日后快速存储数据。
// 从 Realm 中删除所有数据
[realm beginWriteTransaction];
[realm deleteAllObjects];
[realm commitWriteTransaction];