はじめに
前回は、アーキテクチャ選定(MVVM + Repository + Hilt + Flow)について紹介しました。今回は、アプリの中核となる データベース構成(Room) と エンティティ設計 のポイントを整理します。
この記事の目的
- データモデルをどう整理したか
- Entity 、View 、 Repository の責務分け
- 今後の拡張を見据えたテーブル設計の考え方
全体構成イメージ
アプリでは「習慣(Habit)」を中心にカテゴリ構造を持たせて整理しています。
CategoryMaster(大分類) ─┬─ SubCategory(小分類)
│
└─ Habit(ユーザーごとの習慣データ)
たとえば:
- CategoryMaster:運動・メンタル・食事など
- SubCategory:「ランニング」「瞑想」「朝食をとる」など
- Habit:ユーザーが登録した実際の習慣(ON/OFFや曜日情報を保持)
Habitエンティティ
アプリの中核データです。以下は1件の「習慣」を表すEntityです。
@Entity(tableName = "habit")
data class Habit(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val subCategoryId: Long,
val description: String?,
val enable: Boolean = false,
val dayOfWeek: String, // 例: "1,3,5" → 月・水・金
val updatedAt: Long
)
ここでは以下を意識しています:
- 曜日情報を文字列で持たせる(柔軟性重視)
- UIでのON/OFF状態を
enableで管理 updatedAtで最近更新された習慣を抽出可能に
また、このテーブルが「日常的に触るデータの中心」になるため、シンプルでありつつ拡張しやすい構造を意識しました。
カテゴリマスターとサブカテゴリ
カテゴリ構成は、マスター情報とユーザーが追加できるカスタムカテゴリを分けて管理しています。
さらに、統合して扱うためのSubCategoryViewを用意しています。
まとめると以下になります。
- 「アプリが持つ固定カテゴリ」
- 「ユーザーが作成したカテゴリ」
- 「それらをまとめてUIで扱うビュー」
これにより、アプリ側で定義されたカテゴリ構成を崩さずに、追加、編集行うことができます。
// カテゴリマスター
@Entity(tableName = "category_master")
data class CategoryMaster(
@PrimaryKey val id: Long,
val name: String,
val iconName: String,
)
// サブカテゴリマスター
@Entity(tableName = "sub_categories")
data class SubCategory(
@PrimaryKey(autoGenerate = true)
val subCategoryId: Int = 0,
val userId: String,
val categoryMasterId: Int,
val name: String,
val createdAt: Long,
val updatedAt: Long
)
// サブカテゴリビュー
@DatabaseView("SELECT ... UNION ALL SELECT ...", viewName = "sub_category_view")
data class SubCategoryView(...)
テーブルの考え方
CategoryMaster:アプリにあらかじめ定義された固定マスター(ユーザー操作では変更しない前提のデータ)SubCategoryMaster:アプリにあらかじめ定義されたサブカテゴリ固定マスター(ユーザー操作では変更しない前提のデータ)SubCategoryView: マスター側のサブカテゴリ(固定定義)とユーザーが追加したサブカテゴリの両方をまとめた中間ビューです。
ポイント
- マスターは固定データ(アプリ側で定義)
- サブカテゴリはユーザー追加可能
Viewで両者を統合し、UIやRepository層から一元的に扱えるようにした
Repository層の設計
UI側では単純に getAllSubCategories() を呼ぶだけで両方が取れるようになるためシンプルです。
class SubCategoryRepository @Inject constructor(
private val subCategoryDao: SubCategoryDao
) {
fun getAllSubCategories(userId: String): Flow<List<SubCategoryView>> =
subCategoryDao.getSubCategoriesByUser(userId)
}
これにより、ViewModel からはカテゴリ構造を意識せずに済みます。
Repository層での結合例
Roomではテーブル間結合をDAOでも書けますが、アプリ内のビジネスロジックを考慮し、Repositoryでまとめています。
class SubCategoryRepository @Inject constructor(
private val subCategoryDao: SubCategoryDao
) {
fun getAllCategoryMasters(): List<CategoryMaster> =
subCategoryDao.getAllCategoryMasters()
fun getAllSubCategories(userId: String): Flow<List<SubCategoryView>> =
subCategoryDao.getSubCategoriesByUser(userId)
}
DAOは「DBアクセス専用」と割り切り、Repositoryで「カテゴリマスターとユーザー定義サブカテゴリを統合する」などのアプリ独自の集約処理を行っています。
これにより、ViewModel 側では「構造化済みのカテゴリデータ」をシンプルに扱えます。
Flowでリアクティブに更新検知
Roomのクエリに Flow を使うと、DBの変更をUIに自動で反映できます。
@Dao
interface HabitDao {
@Query("SELECT * FROM habit ORDER BY updatedAt DESC")
fun getAllHabits(): Flow<List<Habit>>
@Update
suspend fun update(habit: Habit)
@Delete
suspend fun delete(habit: Habit)
}
また、ViewModelではcollectAsState()と組み合わせることで、「DB更新 → ViewModel → UI再描画」のリアクティブな流れが完成します。
そして、今後は以下のような機能(仮)を予定しています。
| 機能 | 対応予定テーブル |
|---|---|
| 習慣達成ログ(1日ごと) | habit_log |
| 統計グラフ(週・月単位) | habit_log + 集計処理 |
| 継続率表示 | habit_log |
そのため、Habit テーブル自体には「現在の状態」だけを保持し、履歴や集計は別テーブルで管理します。
今後は「習慣の達成ログ」や「統計グラフ」も実装予定です。現在の状態はHabit テーブルに持たせ、履歴は別テーブルに分離しました。
// ログテーブル
@Entity(tableName = "habit_log")
data class HabitLog(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val habitId: Long,
val date: String,
val isCompleted: Boolean
)
分離のメリット
- ログ機能を追加しても
Habitテーブルを壊さない - 日単位・週単位の集計も簡単に行える
- 「状態の履歴」と「定義情報」を独立して扱える
このように分離しておくことで、ログ機能を追加しても既存テーブルを壊さずに拡張できます。
まとめ
| 観点 | 方針 |
|---|---|
| Entity設計 | 責務を分け、拡張性を確保 |
| Repository構成 | ビジネスロジックを一元管理 |
| Flow活用 | UI更新をリアクティブ化 |
| 将来の拡張 | ログ・統計などを別テーブルで実装可能に |
次回は、このデータ構造を使って Room × Flow × ViewModel の連携実装 を進めます。
「DB更新 → ViewModel → Compose再描画」までのリアクティブな流れを、実際のコードとUIを交えて執筆する予定です。

