CoreDataはもう古い?新しいモバイルデバイス向けデータベース「Realm」を使ってみた (Swift/Objective-C)

  • 360
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

正月にSwiftでRealmを使ってみてよかったのでまとめてみました。

Realmとは

Realmはモバイルデバイス向けの新しいデータベースです。実体はTightDBというC++で書かれた独自のエンジンを使用しているようで、マルチプラットフォーム化もされていてiOS/Mac OS/Android向けのSDKが公開されています。

■Realm公式

https://realm.io

CoreDataに慣れているとそんなにハマることはなかったですが、Web開発者で特にSQLゴリゴリ書いて開発するタイプの人にはちょっとハマると思ったのでよく使うSQLをベースにまとめてみました。

なお、コードはSwiftですがObjective-CでもAndroidでもたぶん同じ感じだと思います。
2015/2/17: Objective-Cのコードを追記しました。

CREATE DATABASE

RealmはSQLite同様に1データベース1ファイルとなります。データへのアクセスを行うためにはデータベースファイルを指定してRealmインスタンスを作成しますが、何も指定しなければドキュメントフォルダ以下に default.realmというファイルが生成されます。このデータベースファイルは初回インスタンス生成時に自動生成されるので一般的なリレーショナルデータベースと違い、CREATE DATABASE相当の初期化処理は不要です。

Swift
// デフォルトのRealmファイルを取得する。
let realm = RLMRealm.defaultRealm()
Objective-C
// デフォルトのRealmファイルを取得する。
RLMRealm *realm = [RLMRealm defaultRealm];

データベースファイルは名前を変えたり複数のファイルに分けることも可能です。

Swift
// デフォルトのRealmファイルパスを指定する。
RLMRealm.setDefaultRealmPath(".../Documents/app.realm")
let realm = RLMRealm.defaultRealm()

// 別のデータベースファイルからインスタンス作成
let realm2 = RLMRealm(path: "〜〜/another.realm")
Objective-C
// デフォルトのRealmファイルパスを指定する。
[RLMRealm setDefaultRealmPath:@".../Documents/app.realm"];
RLMRealm *realm = [RLMRealm defaultRealm];

// 別のデータベースファイルからインスタンス作成
RLMRealm *realm2 = [RLMRealm realmWithPath:@"〜〜/another.realm"];

CoreDataのNSManagedObjectContext的な位置付けですね。
以降、このインスンタンス経由で各種データ処理をおこないます。

DROP DATABASE

単一のファイルなのでファイルごとバッサリ消しちゃえばOKです。

Swift
let realmFile = RLMRealm.defaultRealmPath()
NSFileManager.defaultManager().removeItemAtPath(realmFile, error: nil)

CREATE TABLE

一般的なRDBではCREATE TABLE文でテーブルを物理的に作成しますが、Realmではモデルクラスを定義するだけで自動でテーブルが作成されます。ここではたとえばこんなCREATE TABLEを想定します。

CREATE TABLE Staff (
    key int PRIMARY KEY,
    name varchar(n) DEFAULT 'no name',
    INDEX (name)
)

RealでのテーブルはRLMObjectクラスを継承したモデルクラスを定義し、列カラムをdynamicプロパティで定義します。

Swift
class Staff: RLMObject {
    dynamic var key = 0
    dynamic var name = "no name"
    override class func primaryKey() -> String! {
        return "key"
    }

    // 0.91.0以降のインデックス指定
    override class func indexedProperties() -> [AnyObject] {
        return ["name"]
    }

    // 0.91.0以前のインデックス指定
    override class func attributesForProperty(propertyName: String!) -> RLMPropertyAttributes
    {
        var attributes = super.attributesForProperty(propertyName)
        if propertyName == "name" {
            attributes |= RLMPropertyAttributes.AttributeIndexed
        }
        return attributes
    }
}
Objective-C
@interface Staff: RLMObject
@property (nonatomic) NSInteger key;
@property (nonatomic) NSString *name;

@end

@implementation Staff
+ (NSString *)primaryKey {
    return @"key";
}

// 0.91.0以降のインデックス指定
+ (NSArray *)indexedProperties 
{
    return @[@"name"];
}

// 0.91.0以前のインデックス指定
+ (RLMPropertyAttributes)attributesForProperty:(NSString *)propertyName {
    RLMPropertyAttributes attributes = [super attributesForProperty:propertyName];
    if ([propertyName isEqualToString:@"name"]) {
        attributes |= RLMPropertyAttributeIndexed;
    }
    return attributes;
}

@end

プライマリキーはprimaryKey()メソッドをオーバライドし、対象カラムを文字列で返します。今のところマルチカラムなプライマリキーはサポートされていない模様。

検索を最適化するためのINDEXはattributesForProperty()メソッドをオーバライドします。引数でプロパティ名が渡されるのでINDEX指定したいプロパティ名の時に RLMPropertyAttributes.AttributeIndexed のビットを追加します。

0.91.0 よりインデックスの指定方式が変わりました。
indexProperties メソッドをオーバーライドし、インデックス指定したいカラム名を文字列配列で返せるようになり、より簡潔に記述できるようになりました。

INSERT

レコードの生成は、モデルのクラスメソッドであるcreateInDefaultRealmWithObject()を使用します。初期値を引数で渡すとその値がプロパティにセットされたモデルオブジェクトが返ります。

Swift
let staff = Staff.createInDefaultRealmWithObject(["key": 1, "name": "太郎"])
Objective-C
Staff *staff = [Staff createInDefaultRealmWithObject:@{@"key": @1, @"name": @"太郎"}];

ただし保存のためにはトランザクションブロック内での生成が必要です。これは一般的なリレーショナルデータベースのBEGIN〜COMMITと同じですね。

Swift
// トランザクションの開始
RLMRealm.defaultRealm().beginWriteTransaction()

let staff = Staff.createInDefaultRealmWithObject(["key": 1, "name": "太郎"])
if (staff == nil) {
    // トランザクションのキャンセル
    RLMRealm.defaultRealm().cancelWriteTransaction()
}
// トランザクションの終了
RLMRealm.defaultRealm().commitWriteTransaction()

Objective-C
// トランザクションの開始
[[RLMRealm defaultRealm] beginWriteTransaction];

Staff *staff = [Staff createInDefaultRealmWithObject:@{@"key": @1, @"name": @"太郎"}];
if (staff == nil) {
    // トランザクションのキャンセル
    [[RLMRealm defaultRealm] cancelWriteTransaction];
}
// トランザクションの終了
[[RLMRealm defaultRealm] commitWriteTransaction];

トランザクションはBlocksやクロージャーでの記述もできます。

Swift
RLMRealm.defaultRealm().transactionWithBlock({ () -> Void in
    let staff = Staff.createInDefaultRealmWithObject(["id": 2, "name": "太郎"])
    if (staff == nil) {
        return
    }
    println("inserted staff name: \(staff.name)")
})
Objective-C
[[RLMRealm defaultRealm] transactionWithBlock:^{
    Staff *staff = [Staff createInDefaultRealmWithObject:@{@"key": @1, @"name": @"太郎"}];
    if (staff == nil) {
        return;
    }
    NSLog(@"inserted staff name: %@", staff.name);
}];

このクロージャーの実行は非同期ではないのでbeginWriteTransaction()〜commitWriteTransaction()での記述とほぼ同等ですが、エラー時はクロージャーから抜ける(returnする)だけなので若干簡略化できます。

SELECT

レコードの取得です。プライマリキーで単一レコードを取得する場合はコンストラクタでプライマリキーの値を指定します。(但しプライマリキーが設定されている場合のみ)

Swift
let staff = Staff(forPrimaryKey:1)
Objective-C
Staff *staff = [Staff objectForPrimaryKey:@1];

複数レコードの取得には条件文としてNSPredicateでのクエリが記述可能です。取得結果はRLMResultという独自の型インスタンスが返されますが、NSFastEnumerationプロトコルに準拠しているのでArrayと同等に扱えます。

Swift
let result = Staff.objectsWithPredicate(NSPredicate(format: "key < 1000"))
for i in 0..<result.count {
    if let staff = result[i] as? Staff {
        println("staff.name: \(staff.name)")
    }
}
Objective-C
RLMResults *result = [Staff objectsWithPredicate:[NSPredicate predicateWithFormat:@"key < 1000"]];
for (Staff *staff in result) {
    NSLog(@"staff.name: %@", staff.name);
}

UPDATE

UPDATE はモデルオブジェクトのプロパティを上書きするだけです。INSERT同様にトランザクションブロック内である必要があります。

Swift
RLMRealm.defaultRealm().transactionWithBlock({ () -> Void in
    let staff = Staff(forPrimaryKey:2)
    staff.name = "次郎"
})
Objective-C
[[RLMRealm defaultRealm] transactionWithBlock:^{
    Staff *staff = [Staff objectForPrimaryKey:@2];
    staff.name = @"次郎";
}];

SQLのように複数レコードに対してカラムを更新することはできませんので、変更したいレコードのオブジェクトを取得して1件づつ更新します。

Swift
let result = Staff.objectsWithPredicate(NSPredicate(format: "key < 1000"))
for i in 0..<result.count {
    if let staff = result[i] as? Staff {
        staff.key += 100
    }
}
Objective-C
RLMResults *result = [Staff objectsWithPredicate:[NSPredicate predicateWithFormat:@"key < 1000"]];
for (Staff *staff in result) {
    staff.key += 100;
}

REPLACE

MySQLのREPLACE INTO〜構文です。レコードが存在しなければ新規作成し存在すればレコードを更新します。特にjsonなどでリモートから取ってきたデータをRealmデータベースに同期するときなどは数行のコードで書けちゃいます。
たとえばこんなJSONがあったとして、

{
  "key": 1,
  "name": "太郎"
}

このJSON文字列をcreateOrUpdateInDefaultRealmWithObject()に渡せばOK。

Swift
Staff.createOrUpdateInDefaultRealmWithObject(json)
Objective-C
[Staff createOrUpdateInDefaultRealmWithObject:json];

ただしJSONの構成とプロパティが一致していること+プライマリキー必須、という前提条件があります。

DELETE

UPDATE同様に一旦オブジェクトを取得して削除する他に、複数オブジェクトをまとめて削除するメソッドも用意されています。

Swift
RLMRealm.defaultRealm().transactionWithBlock({ () -> Void in
    // 単一レコードの削除
    let staff = Staff(forPrimaryKey:2)
    RLMRealm.defaultRealm().deleteObject(staff)
    // 複数レコードの削除
   let result = Staff.objectsWithPredicate(NSPredicate(format: "key < 1000"))
   RLMRealm.defaultRealm().deleteObjects(result)
})
Objective-C
[[RLMRealm defaultRealm] transactionWithBlock:^{
    // 単一レコードの削除
    Staff *staff = [Staff objectForPrimaryKey:@1];
    [[RLMRealm defaultRealm] deleteObject:staff];
    // 複数レコードの削除
    RLMResults *result2 = [Staff objectsWithPredicate:[NSPredicate predicateWithFormat:@"key < 1000"]];
    [[RLMRealm defaultRealm] deleteObjects:result2];
}];

使ってみた感想

CoreData自体は歴史が古く、Mac OS X 10.6あたりには既に存在していましたが、ほとんど進化していなーとずっと思ってました。(昔はCocoa bindingとInterfaceBuilderの連動でCRUDアプリがドラッグ・アンド・ドロップだけで作れたりしてワクワクしていましたがそれも今は昔。。)
さらにSwiftではCoreDataのモデル名にモジュール名(ターゲット名)が必須になってユニットテストターゲットから使用しづらくなった感があったので、個人的にはもうRealmに完全移行でも良いかなと思っています。あとAndroidと設計を共有できるのは良いですね。開発も活発なので今後も期待大です。
次回はリレーションについてまとめます。