Edited at
RealmDay 10

Realmでの1対多の関係の記述について

More than 3 years have passed since last update.

みなさん!Realm使っていますか?

ここではRealmオブジェクト間の1対多の関係の記述について考察したいと思います。

Realmオブジェクトで1対多の関係を表現するには2つの方法があります。

- シンプルな1対多関係

- RealmList


シンプルな1対多関係

例えば、あるオブジェクトが必ず複数のオブジェクトを内包するような、シンプルな一対多の関係の場合、SQLのテーブルと同じように親オブジェクトへの参照を持つ、という形で表現する事ができます。

クラス図1.png


BookShelfクラス


public class BookShelf extends RealmObject {

@PrimaryKey
private String id;

@Ignore
private List<Book> books;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public List<Book> getBooks() {
return realm.where(Book.class).equalTo("bookShelf.id", getId()).findAll();
}
}



Bookクラス

public class Book  extends RealmObject{

@PrimaryKey
private String id;

private BookShelf1 bookShelf;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public BookShelf1 getBookShelf() {
return bookShelf;
}

public void setBookShelf(BookShelf1 bookShelf) {
this.bookShelf = bookShelf;
}

}


ここではBookShelfクラスではBookのリストをgetterで取得できるように、@Ignoreプロパティを使い、getter内でQueryを書いています。

この時、新しくBookクラスをBookshelfクラスに関連づけて保存する場合は、BookクラスにBookShelfクラスを関連付けて上げるだけで、特にBookShelfクラスについて考える必要はないです。


Bookオブジェクトを保存する

        val realm = Realm.getInstance(this)

realm.executeTransaction {
val bookShelf = it.where(BookShelf::class.java).equalTo("id", bookShelf1Id).findFirst()
val book = it.createObject(Book::class.java).apply {
this.id = UUID.randomUUID().toString()
this.bookShelf = bookShelf
}
it.copyToRealmOrUpdate(book)
}


この方法のいいところは、BookからBookShelfへの逆参照がもてる。また、正規化されているため、あるBookが複数のBookShelfと関連付けられる、という間違いが起こりえない、というところにあります。

ただ、次に記述するRealmListの方法とくらべてのデメリットは、ある順番で並び替えたい場合は、必ず取得時にソースする必要があるため、パフォーマンスの点で不利である可能性があります。


RealmList

また、Realmでは1対多のリレーションを表現するためにRealmListというListの形式がよく使用されます。これは実は多対多の関係になり、SQLの実装では中間テーブルを使って表現するような形になります。

クラス図0.png


BookShelfクラス

public class BookShelf extends RealmObject{

@PrimaryKey
private String id;

private RealmList<Book> books;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public RealmList<Book> getBooks() {
return books;
}

public void setBooks(RealmList<Book> books) {
this.books = books;
}
}



Bookクラス

public class Book2 extends RealmObject {

@PrimaryKey
private String id;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}
}


ここで注意が留意しておくべきは、RealmListはListなので、順番をもつ、ということです。

保存時にListの順番が保存順でよい場合はそのままlistにaddすればよいですが、例えば名前順に保存したい、などの場合には追加時に再ソートしてあげる必要があります。


Bookクラスを関連付けて保存

        val realm = Realm.getInstance(this)

realm.executeTransaction {
val bookShelf = it.where(BookShelf2::class.java).equalTo("id", bookShelf2Id).findFirst()
val book = it.createObject(Book2::class.java).apply {
this.id = UUID.randomUUID().toString()
}
bookShelf.books.add(book)
bookShelf.books.sortBy { it.id } // id順でソートする
it.copyToRealmOrUpdate(book)
it.copyToRealmOrUpdate(bookShelf)
}


RealmListを使った場合の良い所は、並び順を含めて関係を保存することができるため、並び順が決まっている場合はより高速に取得することが期待できるという点です。

逆に、並び順が一意に定まっていない場合、例えば、並び替えの表示にバリエーションがある場合には、保存時に並び替えるメリットは少ないといえるでしょう。

また、ただの1対多の関係を記述したい場合には冗長な情報となり、管理が煩雑になる可能性があります。


速度を比較する

いままでは定性的な議論を行ってきましたが、実際にどうなのか、速度を比較してみましょう。

ここではシンプルな1対多の関係のオブジェクトを保存する場合と、RealmListで保存時に並び替えを行う場合、同じくRealmListで保存時には特に並べ替えを考慮せず、取得時に並び替えを行う場合を比較してみます。

データの件数は10000件で、保存時には1件毎にトランザクションをコミットするような形で実装しました。

そのためRealmListで保存時にソートする場合は、1件保存する毎にソートを行います。

--
シンプルな1対多(取得時にソート)
RealmListで保存時にソート
RealmListで取得時ソート

データの保存にかかる時間
96233ms
500000ms程度※
105629ms

データの取得にかかる時間
149ms
1ms
55ms

※途中GCが走ってしまい、全体的に計測が難しいが最大で1件の保存に約60msほどかかっている

※計測はNexus5x上で行いました。

RealmListちょっ早!

データの取得時にソートする場合でもRealmListを使ったほうが速度的に有利であるということがわかりました。


まとめ

1対多の関係を表現する場合、データ量が少ない場合や、オブジェクト間の関係を正規化しておきたい場合はシンプルな1対多の実装を行うのがよいが、高速に動作させたい場合はRealmListを使うのがよいですね。