駆け出しエンジニアぱかぱかの成長記録

引くほど忘れっぽい新卒2年目駆け出しSEぱかぱかの備忘録です。

【Android】Roomで複数テーブルに対して処理する

こんにちは!ぱかぱかです!
今日は現在開発を進めている飲食店記録アプリ「グルメノート」で、店登録画面からデータベースにデータを登録する処理の続きです。
前回はRoomライブラリを使って行きたい店の情報をrestaurantsテーブルに対してのみ登録する処理を実装しました。
(余談ですが、restaurantsっていちいち長いんだよなぁ…)
今回は、行きたい店登録によって飲食店テーブルと訪問記録テーブルの両方に登録がされるというあるべき姿に修正していきたいと思います。

前回の記事

radish-se.hatenablog.com

Roomを使って店の名前をrestaurantsテーブルに登録できるようになった!

ER図の見直し

改めてDB処理を書こうと思い以前作ったER図を見直してみると、やっぱりなんか変では?となりました。


おかしいところ

・行きたい店・行った店・お気に入り店テーブルは多対多になってしまうユーザと飲食店を結ぶ中間テーブルのつもりで作ったけど、なんで中間テーブルがメモとかの情報を持ってるんだ?
・飲食店が場所テーブルやジャンルテーブルを外部キーで持ってるけど、ジャンルとか場所ってユーザ固有のものではないならわざわざDBで持たなくていいのでは?
 場所情報はGoogleマップAPIとかから取得した情報を持っておけばいいし、ジャンルはEnumとかでアプリ側で定義すればいいような。シーンも同じ。
・行きたい店・行った店・お気に入り店の状態が行き来するなら、共通の情報として管理して外部キーで行きたい店・行った店・お気に入り店特有の詳細を持てばいいのでは?

というわけで見直した結果、以下のようになりました。

いや、こんなシンプルでいいのか…なんか考えすぎて逆に気持ち悪い状況になってましたね。
飲食店が親テーブルで、行きたい店詳細・訪問記録・お気に入り店詳細を子テーブルとして持つ構成にしてます。
なんならもう1つの飲食店テーブルで行きたい店・行った店・お気に入り店の情報も持ってしまってもいいのかもしれませんが…
とりあえず一旦これで突き進むとします。

今回行う登録処理

今回実装したい処理の流れは以下のようになります。

①行った店登録画面に以下の情報を入力して「行った店登録」ボタンを押下する。
 店名、場所、ジャンル、評価、感想
②飲食店テーブル(restaurants)に入力した店名、場所、ジャンルが登録される。
 評価、感想は訪問記録テーブル(visit_records)に登録される。

1つの登録処理で2つのテーブルが更新されることになります。

Roomで複数テーブルの更新を行うには

まずRoomではエンティティクラス間のオブジェクト参照を許可していません。
理由は、UIスレッド上で遅延読み込みが発生するとパフォーマンスに問題が出るからとのこと。

Room を使用して複雑なデータを参照する  |  デベロッパー向け Android  |  Android Developers

「Room がオブジェクト参照をサポートしない理由を理解する」の箇所に説明が書かれています。
例えば今回であれば飲食店テーブルのエンティティオブジェクト内で訪問記録テーブルを参照するようにした場合、毎回訪問記録テーブルへの問い合わせが発生しメモリを食うことになります。
そういう構成にしてしまうと、訪問記録テーブルへの問い合わせが不要になった場合にもデータの読み込み方法を変更することが難しいため、無駄なメモリを食い続けることになってしまうそう。
モバイルアプリはとにかくメモリ節約を意識しなきゃいけないんですね。

そうならないように、Android公式では以下の2つのアプローチが提案されています。
オブジェクト間のリレーションを定義する  |  デベロッパー向け Android  |  Android Developers

  1. 埋め込みオブジェクトを持つ中間データクラスを使用
  2. マルチマップの戻り値の型を持つリレーショナルクエリメソッドを使用

それぞれにメリット・デメリットはありますが、公式ではマルチマップの戻り値の型アプローチを推奨しています。
SQLでゴニョゴニョした方が結局楽ということなのでしょう。

というわけで私もマルチマップの戻り値の型アプローチで進めていきたいと思います。

(ただし今回はInsertだけなのでSQLゴニョゴニョはしません。)

エンティティの作成

まずは粛々と、飲食店エンティティ(restaurants)と訪問記録エンティティ(visit_records)を作ります。

・飲食店エンティティ(restaurants)
前回作ったけどER図変更に伴いカラム構成を修正しました。

カラム名 データ型 内容
restaurant_id INTEGER 飲食店のID(主キー)
restaurant_name TEXT 飲食店の名前
place_name TEXT 場所名(Place Searchレスポンスのnameを格納)
genre_id INTEGER ジャンルのID(Enumで定義)
created_time TEXT 作成日時
updated_time TEXT 更新日時
@Entity(tableName = "restaurants")
class RestaurantsEntity(
    @PrimaryKey(autoGenerate = true) val restaurant_id: Int,
    val restaurant_name: String,
    val place: String,
    val genre: String,
    val created_at: String,
    val updated_at: String
    )

・訪問記録エンティティ(visit_records)

カラム名 データ型 内容
visit_record_id INTEGER 訪問記録のID(主キー)
restaurant_id INTEGER 飲食店のID(外部キー)
scene_id INTEGER シーンのID(Enumで定義)
visited_date TEXT 訪問日
evaluation_id INTEGER 評価のID(Enumで定義)
visited_note TEXT 行った感想
created_at TEXT 作成日時
updated_time TEXT 更新日時
@Entity(tableName = "visited_records")
class VisitedRecordsEntity(
    @PrimaryKey(autoGenerate = true) val visit_record_id: Int,
    var restaurant_id: Long,
    val scene: String,
    val visited_date: String,
    val evaluation: String,
    val visited_note: String,
    val created_at: String,
    val updated_time: String
)

各エンティティのDAOを作成

次に各エンティティのDAOを作成します。
それぞれにINSERTするメソッドだけとりあえず作成します。

・飲食店エンティティ(restaurants)

@Dao
interface RestaurantsEntityDAO {
    @Insert
    suspend fun insertRestaurants(restaurantsEntity: RestaurantsEntity): Long
}

@Insertメソッドでは渡すパラメータが1つの時、戻り値をLongで指定すると挿入したテーブルの主キーが返るそうです。
便利ですね。後ほどその主キーをもとに子テーブルの更新などを行います。

・訪問記録エンティティ(visit_records)

@Dao
interface VisitedRecordsDAO {
    @Insert
    suspend fun insertVisitedRecords(visitedRecordsEntity: VisitedRecordsEntity)
}

2つのテーブルのデータ処理を行うレポジトリの作成

前回の記事ではViewModelの中でデータの処理を色々書いてしまっていたのですが、あくまでViewModelはアクティビティに必要なデータを保持するクラスです。
本来はデータ処理をリポジトリクラスに分けておくことが望ましいので、今回はRestaurantVisitedRecordsRepositoryクラスを作成してそこに処理を記述することにします。

今回のINSERTの処理では、トランザクションで親テーブルを更新してから子テーブルを更新する感じで実装します。
@Transactionトランザクションを張ることができます。

class RestaurantVisitedRecordsRepository(application: Application) {
    private val _db: AppDatabase
    init {
        _db = AppDatabase.getDatabase(application)
    }

    @Transaction
    suspend fun insertRestaurantWithVisitedRecord(restaurant: RestaurantsEntity, visitedRecord: VisitedRecordsEntity) {
        val restaurantsEntityDAO = _db.createRestaurantsEntityDAO()
        val visitedRecordsEntityDAO = _db.createVisitedRecordEntityDAO()

        val restaurantId = restaurantsEntityDAO.insertRestaurants(restaurant)
        visitedRecord.restaurant_id = restaurantId
        visitedRecordsEntityDAO.insertVisitedRecords(visitedRecord)
    }
}

いざ実践

行った店登録画面に色々入力します。
ジャンル・シーン・評価はEnumクラスを作ってプルダウンで入力できるようにしました。

やばい…王将のジャンルをカフェにしてしまった…!笑

「行った店登録」ボタンを押下すると、飲食店テーブル(restaurants)と訪問記録テーブル(visit_records)の両方が更新されました。

飲食店テーブル(restaurants)には店名、場所、ジャンルを登録。

ちゃんとカフェで登録されている…笑

訪問記録テーブル(visit_records)には評価、感想を登録。

今日はここら辺にします。
次回は画像の登録も行えるようにしたいと思います。
行った店リストがDBの情報をもとに表示されるようにしたいですね。
ではまた!