iOS Realm数据持久化--Realm基础知识 (一)

本文主要介绍Realm数据库的相关用法以及需要注意的问题,旨在为大家提供一个简单的参考,如有疑问欢迎指出。
iOS Realm数据持久化--Realm基础知识 (一)
iOS Realm数据持久化--数据分页与复用原理 (二)
iOS Realm数据持久化--List容器分页(三)
iOS Realm数据持久化--Realm集合分页(四)

目录

1、Realm简介
2、集成Realm
3、初始化配置
-> 3.1、初始化RLMRealmConfiguration
-> 3.2、获取Realm对象
-> 3.3、多账号配置
---> 3.3.1、对RLMRealmConfiguration添加扩展
---> 3.3.2、添加Realm分类
---> 3.3.3、使用Realm对象
4、对象模型
5、Realm数据写入、更新、删除
-> 5.1、插入记录
-> 5.2、更新记录
---> 5.2.1、 更新某一个属性
---> 5.2.2、 key-value更新
---> 5.2.3、 通过主键更新
-> 5.3、删除记录
---> 5.3.1、删除一个记录
---> 5.3.2、删除部分记录
---> 5.3.3、删除所有记录
6、Realm通知
-> 6.1、Realm对象通知
-> 6.2、集合通知
-> 6.3、RLMObject对象通知
7、Realm查询
-> 7.1、查询所有数据
-> 7.2、条件查询
-> 7.3、链式查询
-> 7.4、结果排序
-> 7.5、限量加载

1、Realm简介

Realm是新兴的跨平台数据库解决方案,提供多语言支持(JAVA、Objective-C、Swift、JS、.Net),你可以轻松的在iOS、Android等移动平台接入。Realm平台主要提供数据存储和云同步等服务,数据存储服务免费,云同步是收费的,不排除未来数据存储有收费的可能性,但个人认为这种可能性非常低,可以放心使用。

你可以去Realm官网进一步了解Realm相关信息。
Realm是开源的,相关iOS源代码可以去https://github.com/realm/realm-cocoa查阅

2、集成Realm

Realm iOS提供动态库和静态库链接方式,你可以使用Cocoapods或者Carthage快速集成, 目前最新版本为3.17.3。
使用Cocoapos集成,

pod 'Realm', '~> 3.17.3'

3、初始化配置

3.1、初始化RLMRealmConfiguration

RLMRealmConfiguration包含创建Realm数据库所需要的一切,你应当为每个Realm数据库创建一个RLMRealmConfiguration对象,缓存并复用它,因为设置objectClasses会有一定的开销。
下面是RLMRealmConfiguration类介绍

typedef BOOL (^RLMShouldCompactOnLaunchBlock)(NSUInteger totalBytes, NSUInteger bytesUsed);

@interface RLMRealmConfiguration : NSObject<NSCopying>
// 默认Realm配置,设置默认配置后你可以通过 [RLMRealm defaultRealm]很方便的获取Realm对象
+ (instancetype)defaultConfiguration;
+ (void)setDefaultConfiguration:(RLMRealmConfiguration *)configuration;
// Realm本地文件url,即数据库路径
@property (nonatomic, copy, nullable) NSURL *fileURL;
// Realm内存数据库的标志,如果仅需要使用内存缓存可以配置此参数(fileURL将会无效)
@property (nonatomic, copy, nullable) NSString *inMemoryIdentifier;
// 用于加密数据的64字节密钥
@property (nonatomic, copy, nullable) NSData *encryptionKey;
// 只读数据库,如果只需要读取Realm文件不需要写入数据则可以设置为YES
@property (nonatomic) BOOL readOnly;
// 数据库版本号
@property (nonatomic) uint64_t schemaVersion;
// 你需要配置此迁移block来配合数据库升级
@property (nonatomic, copy, nullable) RLMMigrationBlock migrationBlock;
//  如果设置为YES,当存储的版本和提供的版本不一致将会删除并重建Realm数据库文件
@property (nonatomic) BOOL deleteRealmIfMigrationNeeded;
// 压缩数据库
@property (nonatomic, copy, nullable) RLMShouldCompactOnLaunchBlock shouldCompactOnLaunch;
// 指定Realm管理的类
@property (nonatomic, copy, nullable) NSArray *objectClasses;
@end

RLMRealmConfiguration 支持NSCoding协议,使用时应该注意一下几点:

  • + (instancetype)defaultConfiguration;接口返回的是s_defaultConfiguration全局变量的copy实例,并非s_defaultConfiguration对象本身,第一次调用会初始化s_defaultConfiguration实例,这种设计可以在多线程之间自由共享和修改
  • 使用+ (void)setDefaultConfiguration:(RLMRealmConfiguration *)configuration;接口可以设置一个默认的config实例,如果需要同时操作多个数据库,多个config是需要的,你可以把最常用的一个设置为默认。

3.2、获取Realm对象

Realm对象负责数据的写入、更新或删除等操作,你可以通过以下接口获取:

// 使用config初始化一个realm对象
+ (nullable instancetype)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error;

通常情况下你不应该缓存Realm对象,可以直接调用realmWithConfiguration:error:接口获取,内部已经做了缓存处理。

下面是两种更简洁的方式获取Realm对象

// 如果你设置了`DefaultConfiguration`,调用此接口获取Realm对象是一种很简洁的方式
+ (instancetype)defaultRealm {
    return [RLMRealm realmWithConfiguration:[RLMRealmConfiguration rawDefaultConfiguration] error:nil];
}
// 此接口会创建或者获取一个默认的config对象,并修改fileURL
+ (instancetype)realmWithURL:(NSURL *)fileURL {
    RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration];
    configuration.fileURL = fileURL;
    return [RLMRealm realmWithConfiguration:configuration error:nil];
}
  • Realm对象创建依赖于RLMRealmConfiguration实例,每次使用都返回一个Realm对象,此对象可能是重新分配也可能来自于当前的Realm对象缓存。
  • 如果你通过+[RLMRealmConfiguration setDefaultConfiguration:]设置了一个默认的Realm,则可以使用+ (instancetype)defaultRealm来获取默认Realm对象,
  • + (instancetype)realmWithURL:(NSURL *)fileURL接口也是使用默认config创建的Realm对象但是修改了configfileURL属性。
  • 对于多个config实例,你应该使用realmWithConfiguration:error:接口来获取指定的Realm对象进行数据写入。

3.3、多账号配置

假设我们的APP需要登录注销切换,我们需要为每个用户配置一个Realm数据库:

3.3.1、对RLMRealmConfiguration添加扩展:

static NSString *const rlm_remoteUserRealmFileName = @"remoteUser.realm";
RLMRealmConfiguration *rlm_remoteUserConfiguration;

@implementation RLMRealmConfiguration (MyRealmConfig)

+ (RLMRealmConfiguration *)remoteUserConfiguration {
    RLMRealmConfiguration *configuration;
    @synchronized(rlm_remoteUserRealmFileName) {
        if (!rlm_remoteUserConfiguration) {
            rlm_remoteUserConfiguration = [[RLMRealmConfiguration alloc] init];
        }
        configuration = rlm_remoteUserConfiguration;
    }
    return configuration;
}

#pragma mark - ObjectClass

+ (NSArray*)getObjectClassForRemoteUser
{
    return @[[MyObject class]]; 
}

// 得到指定用户账号的数据库配置
+ (RLMRealmConfiguration*)remoteUserRealmConfiguration:(NSString *)user
{
    RLMRealmConfiguration *config = [RLMRealmConfiguration remoteUserConfiguration];
    NSArray *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    NSString *dataBasePath = [libraryPath objectAtIndex:0];
    NSString *filePath = [dataBasePath stringByAppendingPathComponent:user];
    config.fileURL = [[NSURL URLWithString:filePath]URLByAppendingPathExtension:@"realm"];
    config.objectClasses = [self getObjectClassForRemoteUser];
    return config;
}

3.3.2、添加Realm分类

@interface RLMRealm (MyRealm)
+ (void)setDefaultRealmWithUser:(NSString*)userId;
@end

@implementation RLMRealm (MyRealm)
+ (void)setDefaultRealmWithUser:(NSString*)userId
{
    if (!userId) {
        return;
    }
    RLMRealmConfiguration *config = [RLMRealmConfiguration remoteUserRealmConfiguration:userId];
    config.schemaVersion = 5;
    config.migrationBlock = ^(RLMMigration * _Nonnull migration, uint64_t oldSchemaVersion) {
       // 升级处理
    };
    config.shouldCompactOnLaunch = ^BOOL(NSUInteger totalBytes, NSUInteger usedBytes) {
        // 压缩策略
        NSUInteger oneHundredMB = 100 * 1024 * 1024;
        return (totalBytes > oneHundredMB) && ((double)usedBytes / totalBytes) < 0.5;
    };
    [RLMRealmConfiguration setDefaultConfiguration:config];
    [RLMRealm defaultRealm];
}
@end

如需配置一个无账号数据库混合使用,设置一个RLMRealmConfiguration *rlm_localUserConfiguration;全局实例初始化Realm是必要的。

3.3.3、使用Realm对象

// 优先配置好数据库文件
[RLMRealm setDefaultRealmWithUser:@"用户id"];

// 使用默认Realm写入、更新或删除
NSError *error;
RLMRealm *defaultRealm = [RLMRealm defaultRealm];
BOOL ret = [localUserRealm transactionWithBlock:^{
            // 存储逻辑Code
 } error:&error];

4、对象模型

Realm对象模型对代码有较强的侵入性,所有的可存储对象都需继承RLMObject, 对象存储属性的gettersetter方法将被重写,任何对存储属性的settergetter修改都将被忽略。

修改一下官方的代码实例:

#import <Realm/Realm.h>

@class Person;

// Dog model
@interface Dog : RLMObject
@property NSString *name;
@property Person   *owner;
@end
RLM_ARRAY_TYPE(Dog) // define RLMArray<Dog>

// Implementations
@implementation Dog
@end // none needed

// Person model
@interface Person : RLMObject
@property NSString *personId;
@property int age;
@property int sex;
@property NSString *desc;
@property NSString *address;
@property (readonly) NSString *name; // read-only properties are automatically ignored
@property NSString *firstName;
@property NSString *lastName;
@property NSDate               *birthdate;
@property RLMArray<Dog *><Dog> *dogs;
@end
RLM_ARRAY_TYPE(Person) // define RLMArray<Person>

@implementation Person
+ (NSString *)primaryKey {
    return @"personId";
}

//  添加索引
+ (NSArray *)indexedProperties {
    return @[@"address"];
}

// 默认值
+ (NSDictionary *)defaultPropertyValues {
    return @{@"age" : @20, @"sex": @(0)};
}

// 忽略desc属性
+ (NSArray *)ignoredProperties {
    return @[@"desc"];
}

// 只读的数据将会被忽略
- (NSString *)name {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
@end // none needed

使用Realm对象需要注意以下问题:

  • 集合属性RLMArray是集合属性,这里的RLMArray<Dog *><Dog> *dogs保持对Dog对象实例的持有,假设用户新添加了一个宠物Dog,你需要先存储Dog对象然后将Dog加入dogs中,如果反复加入,则会保持多份持有。没有特别的需要,你应该确保dogs中持有一个Dog对象,避免重复。
  • 主键:覆盖+primaryKey可以设置模型的主键,声明主键可以有效地查找和更新对象,并为每个值强制实现唯一性。将具有主键的对象添加到Realm后,无法更改主键。Realm不支持联合主键,也不支持自增序列。
  • 索引属性:要索引属性,请覆盖+indexedProperties。与主键一样,索引使写入速度稍慢,但可以极大的提高查询速度(它还会使您的Realm文件略大,以存储索引)。最好只在特定优化读取性能的情况下添加索引。 Realm支持对字符串,整数,布尔值和NSDate属性进行索引。如果需要频繁写入大量数据,你可以忽略索引。
  • 忽略属性: 如果你不想将模型中的字段保存到Realm,请覆盖+ignoredPropertiesRealm不会干扰这些属性的正常运行; 他们将得到ivars的支持,你可以自由地覆盖重写他们的gettersetter方法。忽略的属性与普通属性完全相同。它们不支持任何特定于Realm的功能(例如,它们不能在查询中使用,也不会触发通知)。仍然可以使用KVO观察它们。
  • 默认属性: +defaultPropertyValues每次创建对象时覆盖以提供默认值。

5、Realm数据写入、更新、删除

Realm的所有写入、更新和删除操作都需要在事务中进行

5.1、插入记录

Dog *myDog = [[Dog alloc] init];
myDog.name = @"Fido";
myDog.age = 1;

NSError *error;
RLMRealm *realm = [RLMRealm defaultRealm];
BOOL ret = [realm transactionWithBlock:^{
         [realm addObject:myDog];
} error:&error];
if (ret)
{
  // 添加成功
}

如果设置了主键,可以使用addOrUpdateObject接口添加记录,记录存在则会更新覆盖所有属性。

5.2、更新记录

5.2.1、 更新某一个属性

对于已存在的记录,在Realm事务中修改其属性值将会直接修改掉其在磁盘上的存储结果

Dog *myPuppy = [[Dog allObjects] firstObject];
[realm transactionWithBlock:^{
    myPuppy.age = 2;
}];

5.2.2、 key-value更新

RLMResults<Person *> *persons = [Person allObjects];
[[RLMRealm defaultRealm] transactionWithBlock:^{
  // 更新第一个人的名字为Sam
    [[persons firstObject] setValue:@"Sam" forKeyPath:@"name"];
  // 将记录中所有人的名字设置为Sam
  [persons setValue:@"Sam" forKeyPath:@"name"];
}];

5.2.3、 通过主键更新

覆盖更新会替换所有的属性值,谨慎使用

Person *person = [[Person alloc] init];
person.name = @"Jack";
person.address = @"ShenZhen";
person.personId = @"ABCDEF";

[realm beginWriteTransaction];
[realm addOrUpdateObject:person];
[realm commitWriteTransaction];

对已存在的记录做部分属性值修改

[realm beginWriteTransaction];
[Person createOrUpdateModifiedInRealm:realm withValue:@{@"personId": @"ABCDEF", @"name": @"Sam"}];
[realm commitWriteTransaction];

5.3、删除记录

删除操作会使所有被缓存的RLMObject记录失效,如果你缓存了一个RLMObject,然后执行了删除操作,但在删除之前没有移除这个对象,则会引发程序Crash,后面我们将会给出解决方案。

5.3.1、删除一个记录

RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
[realm deleteObject:person];
[realm commitWriteTransaction];

5.3.2、删除部分记录

RLMRealm *realm = [RLMRealm defaultRealm];
RLMResults *results = [Person objectsWhere:@"查询条件"];

[realm beginWriteTransaction];
[realm deleteObjects:results];
[realm commitWriteTransaction];

5.3.3、删除所有记录

删除全部记录会删除所有表的数据,请谨慎使用

RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
[realm deleteAllObjects];
[realm commitWriteTransaction];

6、Realm通知

Realm允许你添加通知,你可以对对象和集合添加通知,当Realm被更新或者对象插入、删除和修改等都会收到对应的通知。

6.1、Realm对象通知

注册了Realm通知,每次提交涉及该Realm的写入事务时,无论写入事务发生在哪个线程或进程上,都将触发通知处理程序:

RLMRealm *realm = [RLMRealm defaultRealm];
RLMNotificationToken *token = [realm addNotificationBlock:^(NSString *notification, RLMRealm * realm) {
    [myViewController updateUI];
}];
// 强引用Realm Token,确保通知正常接收
self.token =  token;

// 在不用的时候释放token,注销通知
[self.token invalidate];

6.2、集合通知

注册集合通知不会收到整个Realm的通知,而是收到此集合的详细更改说明。它们包括自上次通知以来已添加、删除或修改的对象索引。集合通知是异步传递的,首先收到初始结果通知,之后每次事务带来的集合对象的任何改变都会再次收到通知。

__weak typeof(self) weakSelf = self;
id<RLMCollection> collection  =  [Person objectsWhere:@"查询条件"]; 
 self.notificationToken = 
      [collection addNotificationBlock:^(RLMResults<Person *> *results, RLMCollectionChange *changes, NSError *error) {
        
        if (error) {
            NSLog(@"Failed to open Realm on background worker: %@", error);
            return;
        }

        UITableView *tableView = weakSelf.tableView;
        // Initial run of the query will pass nil for the change information
        if (!changes) {
            [tableView reloadData];
            return;
        }

        // Query results have changed, so apply them to the UITableView
        [tableView beginUpdates];
        [tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0]
                         withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView insertRowsAtIndexPaths:[changes insertionsInSection:0]
                         withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0]
                         withRowAnimation:UITableViewRowAnimationAutomatic];
        [tableView endUpdates];
    }];

6.3、RLMObject对象通知

Realm支持对象级通知。您可以在特定Realm对象上注册通知,以便在删除对象时或在对象上的任何托管属性修改其值时收到通知。

Person *aPerson = [[Person allObjects]firstObject];
__block RLMNotificationToken *token = [aPerson addNotificationBlock:^(BOOL deleted,
                                                                      NSArray<RLMPropertyChange *> *changes,
                                                                      NSError *error) {
if (deleted) {
       NSLog(@"The object was deleted.");
} 
else if (error) {
       NSLog(@"An error occurred: %@", error);
} 
else {
     for (RLMPropertyChange *property in changes)  {
          if ([property.name isEqualToString:@"age"] && [property.value integerValue] > 30)   {
               // 做一些处理
            }
        }
    }
}];

7、Realm查询

Realm查询相当简洁,完美支持OC的谓词查询。查询返回一个id<RLMCollection>集合即RLMResults实例。如果在事务中对查询的结果进行修改将会直接改变磁盘上的数据。

7.1、查询所有数据

RLMResults<Person *> *persons = [Person allObjects]; 

查询将会返回一个RLMResults集合对象,支持快速枚举等,只有真正开始访问集合数据时,磁盘数据才载入到内存,使用完毕立即释放,即用即取特性极大减小了内存占用,但频繁读取也增加了IO开销

7.2、条件查询

Realm提供了丰富的条件查询接口,你可以根据需要选择合适的查询方式

// 查询中年龄大于20,姓名以`S`开头的所有person
RLMResults<Person *> *persons = [Person objectsWhere:@"age > 2o AND name BEGINSWITH 'S'"];

// 使用 NSPredicate查询
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age > %@ AND name BEGINSWITH %@",
                                                     @(20), @"B"];
persons = [Person objectsWithPredicate:predicate];

7.3、链式查询

RLMResults集合支持对象查询操作,你可以对查询的结果进行再次过滤

RLMResults<Dog *> *tanDogs = [Dog objectsWhere:@"color = 'tan'"];
RLMResults<Dog *> *tanDogsWithBNames = [tanDogs objectsWhere:@"name BEGINSWITH 'B'"];

7.4、结果排序

// 查询所有的Dog,并按照name升序排序
RLMResults<Dog *> *sortedDogs = [[Dog objectsWhere:@"color = 'tan' AND name BEGINSWITH 'B'"]
                                    sortedResultsUsingKeyPath:@"name" ascending:YES];

// 查询所有的Dog Owners 并按照Dog的年龄对主人排序
RLMResults<Person *> *dogOwners = [Person allObjects];
RLMResults<Person *> *ownersByDogAge = [dogOwners sortedResultsUsingKeyPath:@"dog.age" ascending:YES];

7.5、限量读取

当数据库记录过多时可以分批读取数据,RLMResults集合提供了按索引查找、谓词查询、条件排序等能力,并未提供合适的分页方案。后面我们会介绍到如何巧妙的使用Realm集合进行分页。

// 只有前5条数据被读入到内存
RLMResults<Dog *> *dogs = [Dog allObjects];
for (NSInteger i = 0; i < 5; i++) {
    Dog *dog = dogs[i];
    // ...
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269