iOSアプリのDBをCoredataからRealmへ

現時点の最新バージョン v0.96.2 での内容なので、バージョンによって違うかもしれないです。

GUIをインストール

ブラウザが無いと閲覧できない。最新バージョンのzipをDL。
公式の日本語ドキュメントは最新バージョン用ではない可能性があるのでgithubのURLから直接落とす方が安心。
https://github.com/realm/realm-browser-osx/releases/

Xcode Plugin

XcodeでModelファイルを新規作成するにはPluginを入れると簡単になるらしい。
Xcode Pluginは公式ドキュメントにならってAlcatrazからインストールする。

Xcode Pluginのインストールディレクトリパスはこちら

${HOME}/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins

Alcatrazのインストール

コマンドラインから実行

curl -fsSL https://raw.githubusercontent.com/supermarin/Alcatraz/deploy/Scripts/install.sh | sh

Xcodeを再起動する。

AlcatrazからRealmPluginをインストール

Alcatrazパッケージマネージャを起動

RealmPluginをインストール

開発の前に

開発する前に知っておかなければいけないこと。

Realmのテーブル定義方法

  • RLMObjectサブクラスをコードで定義する(CoreDataのようなGUIは無く、ビューワのみ)
  • カラムは@propertyで宣言する
  • @propertyの(nonatomic, strong)などは原則として無視される
    • ただしメモリ上でのみ生きている場合はretainCountを意識する必要があるらしい(詳細は不明)
  • リレーションシップを貼ることができる
    • ただしSQLのON DELETE CASCADEのような機能は現状無い
  • SQLのLIMITに対応するものは無い

クラス名(テーブル)

63 bytes以下のUTF-8文字列

プロパティ名(カラム)

63 bytes以下のUTF-8文字列

対応しているデータ形

BOOL, bool, int, NSInteger, long, long long, float, double, NSString, NSDate truncated to the second, NSData, and NSNumber

NSData

データの保存は16MBまで

NSDate

マイクロ秒は保存されないのでNSTimeInterval推奨

プロパティのGetter/Setter

Overrideダメなので使うならKey-Value Observingで対応?

マルチスレッド非対応

RLMRealmオブジェクトはスレッドごとに取得する必要があるみたい。
[RLMRealm defaultRealm]は問題ないのかもしれないけど、よくわからない。

開発する

CoreDataの開発をする場合は*.xcdatamodel(d)を作成してModelクラスを作成する流れが多いかと思いますが、RealmはModelクラスだけを作成するのが一般的なようでした。
*.xcdatamodel(d)に対応する*.realmファイルはどうやって作成+プロジェクトに組み込むといった説明が特に見当たらなかったので、実機から抜き出すのかなーとか思っています。

サンプル作る

とりあえずキーバリューなRealmサンプル作る

KVModel.h

/**
 KeyValueデータモデル
 */
@interface KVModel : RLMObject
/** キー名 */
@property (nonatomic, strong)  NSString * _Nonnull name;
/** バリュー値の保存用データ */
@property (nonatomic, strong) NSData * _Nonnull value;
// @property (nonatomic, strong) id _Nonnull value;

/**
 valueのセッター。
 データ形を判定して、セットする
 */
- (void)setObjectValue:(nonnull id)objectValue;
/**
 valueのゲッター
 適当なデータ形に変換して返す
 */
- (nullable id)objectValue;

@end

// This protocol enables typed collections. i.e.:
// RLMArray<KVModel>
RLM_ARRAY_TYPE(KVModel)

KVModel.m

@interface KVModel () {
    id _objectValue;
}
@end


@implementation KVModel

+ (NSString *)primaryKey {
    return @"name";
}

+ (NSDictionary *)defaultPropertyValues {
    return @{};
}

// Specify properties to ignore (Realm won't persist these)

+ (NSArray *)ignoredProperties {
    return @[];
}

- (void)setObjectValue:(nonnull id)objectValue {
    self.value = [NSKeyedArchiver archivedDataWithRootObject:objectValue];
}

- (nullable id)objectValue {
    if (!self.value) {
        return nil;
    }
    id object = [NSKeyedUnarchiver unarchiveObjectWithData:self.value];
    return object;
}

- (NSString *)description {
//    return [NSString stringWithFormat:@"<%@ %p; name: %@, value: %@>",
//            NSStringFromClass([self class]), self, self.name, self.value];
    
    return [NSString stringWithFormat:@"<%@ %p; name: %@, objectValue: %@>",
            NSStringFromClass([self class]), self, self.name, [self objectValue]];
}

AppDelegate.m

起動するだけで動作確認できるようにした

- (nonnull NSString *)realmPath {
    // ドキュメントディレクトリにrealmファイルを作成する
    NSString *documentDirPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *path = [documentDirPath stringByAppendingPathComponent:@"dataStore.realm"];
    NSLog(@"realm file path: %@", path);
    return path;
}

- (nonnull NSData *)realmEncryptionKey {
    // ランダムな暗号化キーを生成します
    NSMutableData *key = [NSMutableData dataWithLength:64];
    SecRandomCopyBytes(kSecRandomDefault, key.length, (uint8_t *)key.mutableBytes);
    return key;
}

- (nullable RLMRealm *)realm {
    // realm設定
    RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
    // realmファイルの暗号化設定
//    config.encryptionKey = [self realmEncryptionKey]; // default null
    
    // ファイルパスを設定
//    config.path = [self realmPath];
    
    NSError *error;
    RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
    if (!realm) {
        // もし暗号化キーが間違っている場合、`error`オブジェクトは"invalid database"を示します
        NSLog(@"Error opening realm: %@", error);
        return nil;
    }
    
    return realm;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 1つ目のモデル保存
    KVModel *e1 = [[KVModel alloc] init];
    NSDate *date = [NSDate date];
    e1.name = [NSString stringWithFormat:@"%f", [date timeIntervalSince1970]];
    [e1 setObjectValue:[NSString stringWithFormat:@"test value %f", [date timeIntervalSince1970]]];
    
    RLMRealm *realm = [self realm];
    [realm beginWriteTransaction];
    [realm addObject:e1];
    [realm commitWriteTransaction];
    
    // 2つ目のモデル保存
    KVModel *e2 = [[KVModel alloc] init];
    date = [NSDate date];
    e2.name = [NSString stringWithFormat:@"%f", [date timeIntervalSince1970]];
    [e2 setObjectValue:@(1234567890)];

    [realm beginWriteTransaction];
    [realm addObject:e2];
    [realm commitWriteTransaction];
    
    // 読み込み
    RLMResults *results = [KVModel allObjects];
    for (KVModel *entity in results) {
        NSLog(@"%@", entity);
    }
    
    return YES;
}

感想

とりあえず動いた。

KVModelのvalueをid型にしたり、NSData型にしてNSKeyArchiverを使ったりしたのでソースは汚いです。

KVModel.valueをid型にして、NSStringで保存、NSNumberで保存、とした時、Realmファイルを覗いたら型名がAnyとなっていました。なのでid型も一応保存可能みたいです。

ただid型はなんとなく先を考えるとリスクが高そうな気がするのでNSData+NSKeyedArchiver(UnArchiver)使った方が構造的にはシンプルかなと思いました。

あとは、今後マイグレーションする必要が出るかもしれないことを考えるとRealmファイル名をデフォルトから変更した方が幸せになれそうかなと思いました。