ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Swift3でRealmを使って多対多のモデルを作るには

こんにちは。スマートフォンエンジニアのまさです。
最近サーバとやりとりをするアプリを作成していますが、APIをなんども叩くアプリでは、 どうしてもローディングが多くなりがちで、ユーザーにストレスを与えてしまいます。
何度も叩く必要のないAPIはレスポンスをRealmに保存しておいて、より快適なアプリを実現したいですね。
今回は、Realmで多対多のモデルを作る方法をご紹介します。

Realmのインストール

まずは、podでrealmを入れます。

$ pod init
$ vi Podfile
pod 'RealmSwift' # 追加
$ pod install

Realmのモデルを作成する

今回は複数の子供が複数のグループに所属しているモデルを作ってみようと思います。 基本的には、多対多は、中間テーブルを用意し、それぞれのテーブルに一対多にすることで実現します。

f:id:unifa_tech:20170419141521p:plain

import RealmSwift

class Kid: Object {
    dynamic var id = 0
    dynamic var name = ""
    var kidGroups = List<KidGroup>()
     
    override static func primaryKey() -> String? {
        return "id"
    }
}
 
class KidGroup: Object {
    dynamic var group: Group?
    dynamic var kid: Kid?
    let ownerKid = LinkingObjects(fromType: Kid.self, property: "kidGroups")
    let ownerGroup = LinkingObjects(fromType: Group.self, property: "kidGroups")
}
 
class Group: Object {
    dynamic var id = 0
    dynamic var name = ""
    var kidGroups = List<KidGroup>()
     
    override static func primaryKey() -> String? {
        return "id"
    }
}

Realmのモデルに実際にデータを入れてみる

モデルクラスを用意したら、データを入れてみます。 テストなので、viewDidLoadに入れてログを出して確認しています。

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
     
    // テストのため、毎回realmのDBを消して再度新規生成されるようにしています
    let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
    let path = paths[0] + "/test.realm"
    let url = URL(fileURLWithPath: path)
    let fileManager = FileManager.default
    if fileManager.fileExists(atPath: path) {
        try! FileManager.default.removeItem(atPath: path)
        try! FileManager.default.removeItem(atPath: path + ".lock")
        try! FileManager.default.removeItem(atPath: path + ".management")
    }
     
    let realm = try! Realm(fileURL: url)
     
    // たろう君は、たぬき組に、所属、
    // じろう君は、たぬき組、きつね組どちらにも所属させてみる
     
    let kid = Kid()
    kid.id = 1
    kid.name = "たろう"
     
    let kid2 = Kid()
    kid2.id = 2
    kid2.name = "じろう"
     
    let group = Group()
    group.id = 101
    group.name = "たぬき組"
     
    let group2 = Group()
    group2.id = 102
    group2.name = "きつね組"
     
    let kidGroup = KidGroup()
    kidGroup.group = group
    kidGroup.kid = kid
     
    kid.kidGroups.append(kidGroup)
    group.kidGroups.append(kidGroup)
     
    let kidGroup2 = KidGroup()
    kidGroup2.group = group2
    kidGroup2.kid = kid2
     
    kid2.kidGroups.append(kidGroup2)
    group2.kidGroups.append(kidGroup2)
     
    let kidGroup3 = KidGroup()
    kidGroup3.group = group
    kidGroup3.kid = kid2
     
    kid2.kidGroups.append(kidGroup3)
    group.kidGroups.append(kidGroup3)
     
    try! realm.write() {
        realm.add(kid)
        realm.add(kid2)
    }

}

正しく保存されているか確認してみる

それでは、正しく参照できるのか、また中間テーブルが削除されれば きちんと関連がきれるのか、テストしてみようと思います。下記のコードをviewDidLoadの下側に追記してみます。

    // クエリ
    print("Kidのオブジェクトをすべて取得する")
    let kids = realm.objects(Kid.self)
    print(kids.count) // => 2
    print(kids[0].kidGroups.count) // => 1
    print(kids[1].kidGroups.count) // => 2 
     
    print("Kidのidが2のオブジェクトを取得する")
    let kidsId_2 = realm.objects(Kid.self).filter("id = 2")
    print(kidsId_2[0].kidGroups.count) // => 2
    print(kidsId_2[0].kidGroups[0].group?.name ?? "") // => たぬき組
    print(kidsId_2[0].kidGroups[1].group?.name ?? "") // => きつね組
     
    print("Groupがきつね組に所属しているkidを取得する")
    let groupsId_102 = realm.objects(Group.self).filter("id = 102")
    print(groupsId_102[0].kidGroups.count) // => 1
    print(groupsId_102[0].kidGroups[0].kid?.name ?? "") // => じろう
     
    print("Kidじろうのgroupきつね組との関連を切ってみる")
    try! realm.write {
        realm.delete(groupsId_102[0].kidGroups[0])
    }
     
    print("きちんと関連が切れているか確認する groupから参照した場合")
    let updateGroupsId_102 = realm.objects(Group.self).filter("id = 102")
    print(updateGroupsId_102[0].kidGroups.count) // =>0
     
    print("きちんと関連が切れているか確認する kidから参照した場合")
    let updateKidsId_2 = realm.objects(Kid.self).filter("id = 2")
    print(updateKidsId_2[0].kidGroups.count) // => 1
    print(updateKidsId_2[0].kidGroups[0].group?.name ?? "") // => たぬき組
 

正しく参照できているようです。中間テーブルを削除して関連を切れば、kidから参照しても、groupから参照しても正しい結果と なっているのがわかります。多対多を実現させることができました。
まだまだRealm初心者なので、今後もいろいろと試してみようと思います。
もしここがおかしいとか、こういう書き方もあるよなどありましたら、ご指摘頂けると幸いです。