Android Roomリレーション(初学者向け)


はじめに

チーム開発でRoomに触れる機会があったため、初学からリレーションを組めるまでの導入を備忘録ついでに書こうと思います。
より良い書き方を見つけ次第随時更新かけていきます!
説明から書いているので、実装だけ見たい方は実装に飛ぶことをお勧めします!

導入

まず、Roomとはどのようなものかをざっと説明すると
Room 永続ライブラリは SQLite 全体に抽象化レイヤを提供することで、データベースへのより安定したアクセスを可能にし、SQLite を最大限に活用できるようにします。

(AndroidDevelopersより引用)
RoomはAndroidJetpackというボイラープレートコードを省いてくれる良い感じのライブラリ等の一部で、
SQLiteというデータベースをより触りやすくしてくれる神ライブラリです。
アクセスするたびに、製作者様に感謝しながらプログレスバーを回し、レスポンスを待ちましょう。()

下準備

build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' <- ココ追加!

dependencies {

    implementation "androidx.room:room-runtime:2.2.3" <- ココ追加!
    implementation "androidx.room:room-ktx:2.2.3" <-Kotlinコルーチン使うなら追加!
    kapt "androidx.room:room-compiler:2.2.3" <- ココ追加!
    //以下のように書くと綺麗(Object作ってDep.room.versionみたいに書くともっと綺麗)
    //上の書き方か下の書き方どっちかを追加してね!
    def room_version = "2.2.3"
    implementation "androidx.room:room-runtime:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
}

以上でgradle側の下準備ができたのでSync Now!!

Room入門

今回使う知識を初学者向けにざっと書いていくので、詳しい部分はリファレンスをご参照あれ!

リレーションとは

ここでは1:1や1:nといったテーブル同士の関係性のことだよ!
日本で言うと夫婦が一般的に夫と妻で1:1の関係
学校で言うと教師と生徒が1:nの関係性みたいなイメージだね!

今回使うアノテーション群

@Dao データアクセスオブジェクト(データを取ってくるためのinterfaceにつける)
@Insert データを挿入する関数の上に書く、色々オプションがある
@Transaction トランザクションメソッドとしてマークするよ!
@Query SQLのクエリ文をかけるよ
@Entity テーブルとして定義するよ!
@PrimaryKey 対象の変数を主キーとして扱うよ!(そのオブジェクトを指す一意の値)
@ColumnInfo カラムの情報を追加できるよ!
@Database データベースとして定義するよ!

 トランザクションに関する参考資料
- 「トランザクション」とは何か?を超わかりやすく語ってみた!

実装

1対多

関係性思い浮かばなかっただけなので深く考えないでください
イメージ↓

WomenEntity.kt
@Entity(tableName = "women",
    foreignKeys = arrayOf(
        ForeignKey(
            entity = ManEntity::class,
            parentColumns = arrayOf("man_id"),
            childColumns = arrayOf("husband_id"),
            onDelete = ForeignKey.CASCADE
        )
    )
)
data class WomenEntity (
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "women_id")
    val id: Int,
    @ColumnInfo(name = "husband_id")
    val husbandId:Int,
    @ColumnInfo(name = "women_name")
    val name: String
)
ManEntity.kt
@Entity(tableName = "man")
data class ManEntity (
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "man_id")
    val id: Int,
    @ColumnInfo(name = "man_name")
    val name: String
)
Family.kt
data class Family {
    @Embedded
    val man: ManEntity
    @Relation(parentColumn = "man_id", entityColumn = "husband_id")
    val womens: List<WomenEntity>
}

ForeignKeysは、外部キーといい、外部のテーブルと結びつける役割をしています!
parentColumnsが親となるカラムを指定していて、お互いにidを参照していますね(Womenならmanのidを)
childColumnsは子となるカラムを指定しています!これは親のカラムと同じ値をもちます!
これを知ることで何ができるかと言うと、自分は相手のキーを知っているから、アクセスができると言うことなのですね!
onDeleteは親が削除された時に自分はどうするかというオプションなのですが、今回は削除をつけています。特に深い意味はないです!
なお、データの取得は、

Dao.kt
    @Transaction
    @Query("SELECT * FROM man")
    suspend fun getFamilyList(): List<Family>

をDaoに記述してあげることで、Familyのリストが取得できます。

新しくfamilyと言うデータクラスが増えています、これはManEntityと複数のWomenEntityを結びつけている中間テーブルになります。

ManEntity | WomenEntity |  Family
man_id    | women_id    |  man_idを持つManEntity
man_name  | husband_id  |  man_idがhusband_idと等価なWomenEntityのリスト
          | women_name  |

このような関係性を持っているので、中間テーブルを持っておくことでお互いの関係性を表せています!
Familyの中身をよりわかりやすく説明すると

man_id |  man_idがhusband_idと等価なWomenEntityのwomen_id
  1    |     2
  1    |     3
  1    |     4
  1    |     5

このような形で、man_idに対してどんなwomen_idが結びついているかを表していると言う認識で良さそうです!
この辺りを見るとわかりやすいかも?
DBのリレーション

多対多

WomenEntity.kt
@Entity(tableName = "women")
data class WomenEntity (
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "women_id")
    val id: Int,
    @ColumnInfo(name = "women_name")
    val name: String
)
ManEntity.kt
@Entity(tableName = "man")
data class ManEntity (
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "man_id")
    val id: Int,
    @ColumnInfo(name = "man_name")
    val name: String
)
Family.kt
@Entity(
    tableName = "family",
    primaryKeys = ["family_man_id", "family_women_id"],
    foreignKeys = [
        ForeignKey(
            entity = ManEntity::class,
            parentColumns = ["man_id"],
            childColumns = ["family_man_id"],
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = WomenEntity::class,
            parentColumns = ["women_id"],
            childColumns = ["family_women_id"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class Family(
    @ColumnInfo(name = "family_man_id")
    val mansId: Int,
    @ColumnInfo(name = "family_women_id")
    val womensId: Int
)

WomenEntityからhusband_idとforeignKeysがなくなり、
Familyに二つのforeignKeysが追加されています!
なぜそのように実装したかと言うと、Entityが持てるデータは一つのカラムにつき1データまでなので、
自由に作成し増やすことのできる中間テーブルからManEntityとWomenEntityのidを取得して、その結びつきを登録してあげることで、様々な結びつきを表現できると言うことです!

前回と中間テーブルの中身がどう違うかなのですが、

family_man_id  |  family_women_id
      1        |        1
      1        |        2
      2        |        1
      2        |        3

のように別々の関係性を持っています
男性1 は 女性1,2とのリレーションを持っており、
女性1 は 男性1,2とのリレーションを持つ
といった、複数対複数の関係性を持っていることが表現できています。
多対多のデータ取得はとても理解が難しいため、説明します。

Dao.kt
@Transaction
@Query(
    """
       SELECT * FROM man
       INNER JOIN family
       ON man.man_id=family.family_man_id
       WHERE family.family_women_id=:womenId
    """
)
suspend fun getBindManList(vararg womenId: Int): List<ManEntity>

上記のSQL文を簡略化して日本語にすると、
「全てのmanテーブルから、familyテーブルも参考にしながら、familyに登録されていて、
 そのfamilyのfamily_women_idが引数で渡されたwomenIdと等しいManEntityを取ってきてね」
と言う意味になります。
その理解で、womenを取ってくるSQLを書くと以下のようになります。

Dao.kt
@Transaction
@Query(
    """
       SELECT * FROM women
       INNER JOIN family
       ON women.women_id=family.family_women_id
       WHERE family.family_man_id=:manId
    """
)
suspend fun getBindWomenList(vararg manId: Int): List<WomenEntity>

となります。

編集後記

私自身リレーションにとても苦しんでいたので、同じ苦しみを初心者の方が出来るだけ味合わないように記事を書かせていただきました。
なかなか言語化するのも難しいですが、もし読んでくださった方の理解の助けになれたのであれば幸いです。

そもそも普段から記事を書いたりしないので、日本語がおかしかったりする部分も多々あるとは思いますが、生暖かい目でも良いので見守ってくれたら幸いです。

また、ここはもっと詳しく説明して!だとか、こんな風に表現した方がわかりやすいよ!とか言ってくださると今後の記事の質が上がると思いますので、ぜひコメントください....!

あとなぜ女性と男性で表したんだ!と言う方もいらっしゃると思いますが、エネルギーの残っていない脳ではこれしか思いつきませんでした...許してください....
(ブログでは悟●とドラゴ●ボールとドラゴ●ボールの中の星の関係性を1:n:nとか言ってたのでまだわかりやすい表現かな...?)

いらすとやさんより画像を使わせていただきました。
https://www.irasutoya.com/p/health.html)