みなさん!Realm使っていますか?
ここではRealmオブジェクト間の1対多の関係の記述について考察したいと思います。
Realmオブジェクトで1対多の関係を表現するには2つの方法があります。
- シンプルな1対多関係
- RealmList
シンプルな1対多関係
例えば、あるオブジェクトが必ず複数のオブジェクトを内包するような、シンプルな一対多の関係の場合、SQLのテーブルと同じように親オブジェクトへの参照を持つ、という形で表現する事ができます。
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();
}
}
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クラスについて考える必要はないです。
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の実装では中間テーブルを使って表現するような形になります。
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;
}
}
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すればよいですが、例えば名前順に保存したい、などの場合には追加時に再ソートしてあげる必要があります。
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を使うのがよいですね。