あいどんノート

日々疑問に思ったことや得られた知見を書きなぐります!

Android Roomで1対nの表現方法とそれをネストする方法

こんにちは。ふぇりです。

この記事では、Room初学者(同志)を対象としており、1:nのデータ構造の表現方法および、それらをネストする方法について解説(?)共有(?)します。

 

最近Roomを触っていて、その中で、1:nのものをさらに1:nでネストするというデータ構造を作ることになりました。

私が探した範囲内では、日本語でこのようなデータ構造をRoomで表す方法についての記事が見つからなかったので、その実装方法を説明したいと思います。

 

まず前提として、1:nというのは、

一つのものに対して複数のものが相対すること。対応関係にある二項のうち、片方単独単一であり、他方多数であるという構成になっている状況を意味する語。多く場合は「一対一」、「多対多」との対比で用いられる語。

ウェブリオ辞書様より引用

https://www.weblio.jp/content/%E4%B8%80%E5%AF%BE%E5%A4%9A 

 うむ、わかりづらい。

一つのオブジェクトに対して、複数のオブジェクトが紐づいているようなイメージでいいかなと思います。

例えば、悟●が持っているドラ●ンボールなどですかね(謎)

●空という一つのオブジェクトに、所持しているドラゴ●ボールのデータが紐づいているようなイメージです。

           1:n

悟空 → ドラゴンボール

 

Roomですと、その実装は主に中間テーブルを生成するというものになります。

その前に、まず各々のエンティティを実装していきます。

まず、悟空側(ユーザー側)は次のようにします。

 

@Entity(tableName = "user_item")
data class UserEntity(
@PrimaryKey
@ColumnInfo(name = "user_id")
val id:String
)

今回はidのみを持つこととします。

テーブル名はuser_itemでプライマリキーはuser_idとします。

 

次にドラゴンボール側を実装していきます。

 

@Entity(tableName = "dragonball_item",
foreignKeys = arrayOf(
ForeignKey(
entity = UserEntity::class,
parentColumns = arrayOf("user_id"),
childColumns = arrayOf("dragonball_id"),
onDelete = ForeignKey.CASCADE //ユーザー削除と共に子も削除
))
)
data class DragonballEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "project_id")
val id: Int,
@ColumnInfo(name = "author_id")
val author: String
)

少しややこしいのが増えました。

foreignKeysという奴が増えてます。

こいつは外部のエンティティのキーを参照して、自身のエンティティを関連づける役割といったイメージを持っていただけるといいと思いますb

・entityは結びつける対象のクラス

・parentColumnsは結びつける親のカラム名

・childColumnsは結びつけられる側のカラム名

・onDeleteは親が削除された際の扱い

を示しています。parentColumnsとchildColumnsは同じ値を保持します。なので、1:nのn側(ドラゴンボール側)が1側(悟空側)のカラムの値を知っていて、それと紐づけ(内部で結合テーブルを作っている?)を行ってくれています。

 

つぎはこれらの中間テーブルを実装します。

data class UserWithDragonball(
@Embedded
val user: UserEntity,
@Relation(parentColumn = "user_id",entityColumn = "dragonball_id")
val DragonballList: List<DragonballEntity>
)

1側はEmbeddedアノテーションをつけて、n側はRelationアノテーションを使用します。

これらの内部の仕組みはまだ理解していないので、理解し次第記事をあげるか更新します。

 

Daoは次のようになります。

@Dao
interface UserEntityDao {
@Transaction
@Query("SELECT * FROM user_item")
fun getUserWithDragonball():List<UserWithDragonball>
}

一通り、これで1:nの表現ができ、DaoのQueryでuser_itemを全て取ってきた際に、

ユーザーとドラゴンボールが紐づいたリストが返されて、1:nの関係がデータ構造で表現できます。

(ここまでお疲れ様でした....)

 

次に、1:n:nとでもいうべきか1対nのネストを実装していきます。

先ほどの実装には少し不満点がありますね。

みなさんお分かりの通り、ドラゴンボールの星が再現されてません!!!(なんてこった)

ドラゴンボールドラゴンボールの中の星は1:nの関係ですね!!(白目)

これを表現してあげると、

ユーザー : ドラゴンボール = 1 : n

ドラゴンボール : ドラゴンボールの中の星 = 1 : n

となるので、1:nの中にさらに1:nを作る(ネストする)ことになります。

 

さてやっていきましょう。

まずは、ドラゴンボールの星の中のエンティティを実装していきます。

 

 

@Entity(tableName = "star_item",
foreignKeys = arrayOf(
ForeignKey(
entity = DragonballEntity::class,
parentColumns = arrayOf("dragonball_id"),
childColumns = arrayOf("star_id"),
onDelete = ForeignKey.CASCADE
)
)
)
data class StarEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "star_id")
val id: Int
}

これは上の部分で行ったように、今回はユーザーではなくドラボンボールに結びつけています。

上の部分でも行ったように、次はドラゴンボールドラゴンボールの中の星の中間テーブルを作りましょう。

data class DragonballWithStar(
@Embedded
val dragonball: DragonballEntity,
@Relation(parentColumn = "dragonball_id",entityColumn = "star_id")
val todoList: List<StarEntity>
)

困りました、ネストしたいのにこのままではユーザーとドラゴンボールのテーブル、ドラゴンボールとその中の星のテーブルで独立していて、繋がっていることを表現できていません。

ユーザーとドラゴンボールのテーブルを以下のように変更しましょう。

data class UserWithProject(
@Embedded
val user: UserEntity,
@Relation(parentColumn = "user_id",entityColumn = "dragonball_id",
entity = DragonballEntity::class)
val DragonballAndStar: List<DragonballWithStar>
)

ユーザーとドラゴンボールの中間テーブルにドラゴンボールとスターの中間テーブル教えて繋げるのはいいけど、entityってなんやねん!って思った方も多いと思います。

その理由としては、そもそもuserはドラゴンボールとスターの中間テーブルの存在を知らないため、知らんがな!とエラーを吐きます。

それを防ぐため、私は、ドラゴンボールのエンティティです、怪しいものではございませんと伝えることによってエラーを防ぎます。

これによって、1対nをネストすることができました。

 

これによって、  ユーザーを全て取ってきた際に、ドラゴンボールの星までアクセスができるようになります。

 

書き終えて思ったこと。

1対nのネストの例えって難しいですね(白目)

あと記事をかくと自分の理解できていない部分がわかっていい感じです。