次世代モバイルデータベース「Realm」のリレーションシップ

More than 1 year has passed since last update.

Realmって何?って思った方はまずは下記の記事を読んで下さい。簡単に言えばCoreDataやSQLiteに変わる次世代モバイルデータベースです。

概要

図はCoreDataで作ったものですが、今回はこの構成をRealmで実装します。
RealmRelationships.png

よくある構成で、記事(article)、サムネイル(thumbnail)、コメント(comment)、カテゴリ(category)、のそれぞれを次の3種類のリレーションシップで実装します。

  • One-to-One
    記事とサムネイルの関係。記事のアイキャッチ的な画像なので1記事に対して0以上1以下存在します。記事が消えるとサムネイルのレコードも削除される必要があります。
  • One-to-Many
    記事とコメントの関係。コメントは1記事に対して複数存在します。記事とコメントは親子関係となるため、記事が消えたらそれに紐付いたコメント全てが消えるべきです。
  • Many-to-Many
    記事とカテゴリの関係。記事は複数のカテゴリを複数持つことが出来ますが、そのカテゴリは他の記事でも使用するため親子関係は無く削除の影響を受けません。

MySQLなど一般的なRDBではこれらの関連に外部キーの定義が必須となりますが、CoreDataやRealmではオブジェクトそのものの参照を持つためキーは不要です。キーを用いたクエリを書かなくても関連したオブジェクトを芋づる式で引き出せるのでコードがとても簡潔になります。

尚、試したRealmのバージョンは 0.89.2 です。

One-to-One

記事とサムネイルが1対1となる最も単純なリレーションです。まずはArticleとThumbnailのモデルを定義しましょう。まだここではリレーション関係はありません。

OneToOne1.swift
class Article: RLMObject {
    dynamic var title = ""
    dynamic var body = ""
}

class Thumbnail: RLMObject {
    dynamic var imageData:NSData?
}

このAritcleクラスにThumbnailのリレーションプロパティを追加します。今回は1対1なので単にプロパティの型宣言を参照先のモデルクラスにすればよいだけです。

OneToOne2.swift
class Article: RLMObject {
    dynamic var title = ""
    dynamic var body = ""
    dynamic var thumbnail:Thumbnail? //←追加
}

このプロパティにThumbnailのインスタンスをセットすればリレーションの完成です。

OneToOne3.swift
let realm = RLMRealm.defaultRealm();
// トランザクション開始
realm.beginWriteTransaction();

// 記事オブジェクト作成
let articleInfo = ["title": "タイトル", "body": "本文"];        
let article = Article.createInDefaultRealmWithObject(articleInfo)
// サムネイルのインスタンスを作成
let thumbnail = Thumbnail(object: ["imageData": NSData()])
// 記事オブジェクトにセット
article.thumbnail = thumbnail

// コミット
realm.commitWriteTransaction();

ただ、このままだと親となるArticleから子へのThumbnailへの一方的な関連となります。子のThumbnailオブジェクトから親のArticleを知ることができません。そこでCoreDataにもあったInverse Relationship(逆関連)を設定することにより相互の関係を結びつけることができます。

CoreDataでのInverse Relationship

CoreDataだとこういうやつ↑

Inverse Relationshipなプロパティはデータベース上の実カラムではなく親のクラス名とプロパティ名から取り出したオブジェクトを返すreadonlyなプロパティを自前で定義する感じです。関連した親オブジェクトを返すメソッドは linkingObjectsOfClass(className: forProperty:) で取得できます。

OneToOne4.swift
class Thumbnail: RLMObject 
{
    dynamic var imageData:NSData?
    dynamic var article:Article? {
       return linkingObjectsOfClass("Article", forProperty: "thumbnail").first as? Article
    }
}

これで子のThumbnailオブジェクトから親のArticleへプロパティでアクセスできるようになりました。

OneToOne5.swift
let thumbnail = article.thumbnail
if (thumbnail.article == article) {
    println("同じだよ!")
}

この linkingObjectsOfClass(className: forProperty:) メソッドは関連するオブジェクトを全て返します。今回は1対1で親が1件特定できれば良いため、最初のオブジェクトを返すようにしましたが、親が複数存在するような多対多の関連でも使用できます。

One-to-Many

記事とコメントの関係は1対多となります。Articleに複数のコメントオブジェクトを保持するにはcommentsプロパティにRLMArrayオブジェクトのインスタンスをセットします。CommentクラスにはInverse RelationshipでArticleへの関連を定義しておきます。

OneToMany1.swift

class Article: RLMObject {
    dynamic var title = ""
    dynamic var body = ""
    dynamic var thumbnail:Thumbnail?
    dynamic var comments = RLMArray(objectClassName: Comment.className())     // ← 追加
}

class Comment: RLMObject {
    dynamic var name = ""
    dynamic var comment = ""
    dynamic var article:Article? {
        return self.linkingObjectsOfClass("Article", forProperty: "comments").first as? Article
    }
}

記事へのコメント追加はcomments.addObject()でCommentインスタンスを追加します。

OneToMany2.swift
realm.beginWriteTransaction();

// コメントオブジェクトを作成
let comInfo = ["comment": "コメント", "name": "氏名"];
let comment = Comment.createInDefaultRealmWithObject(comInfo)
// 記事のプロパティに追加
article.comments.addObject(comment)

realm.commitWriteTransaction();

// Comment->Articleの逆参照も可
println("articleTitle: \(comment.article.title)")

簡単ですね!

Many-to-Many

最後は多対多の関連。記事とカテゴリの関係です。カテゴリオブジェクトはマスター的な扱いで記事ごとに作成されるものではなく他の記事と共有されます。図で表すと↓こんな感じ。

Realmの実装はOne-To-Manyの応用です。Articleクラスにcategoriesプロパティを追加し、CategoryクラスにArticleへの逆参照を定義します。子から親への関連も対多となるため、配列でreturnするところがOne-to-Manyと違います。

ManyToMany.png

ManyToMany1.swift
class Article: RLMObject {
    dynamic var title = ""
    dynamic var body = ""
    dynamic var thumbnail:Thumbnail?
    dynamic var comments = RLMArray(objectClassName: Comment.className())
    dynamic var categories = RLMArray(objectClassName: Category.className())        // ← 追加

}

class Category: RLMObject {
    dynamic var name = ""
    dynamic var key = ""
    var articles:[Article] {
        return self.linkingObjectsOfClass("Article", forProperty: "categories") as [Article]
    }
}

Cascade Delete(?)

と、ここまででモデル間の関連が出来たから参照整合性制約もバッチリ!と思い、Articleオブジェクトをまとめて消してみました。Inverse Relationshipも設定済みなので、親を消せば子も自動で消えるよねってことで10件くらい記事とコメントなどを登録後、全ての記事オブジェクトをサクッと消してみたのが↓のコードです。

Cascade1.swift
// 10件くらい挿入後に全件取得
let allArticles = Article.allObjects()
// まとめて削除
RLMRealm.defaultRealm().deleteObjects(allArticles)

// 子のオブジェクト数を取得
let allThumbnailcount = Thumbnail.allObjects().count
let allCommentCount = Comment.allObjects().count

ThumbnailCount と CommentCount の期待値はいずれもゼロになるはず。

Cascade2.swift
println("ThumbnailCount: \(allThumbnailcount)")
println("CommentCount: \(allCommentCount)")

> ThumbnailCount: 10
> CommentCount: 10

?!

子オブジェクトが消えてない!
Google先生に聞いたところStackOverFlowに↓のような投稿が。。。

http://stackoverflow.com/questions/26647551/delete-objects-in-realm-using-cascade-relationship

どうも現時点のRealmでは参照整合制約ガン無視なので自前で子オブジェクトを削除する必要があるらしい。。

今後に期待ですね!