【習慣トラッカー開発記 #4】DB設計・エンティティ定義のポイント

はじめに

前回は、アーキテクチャ選定(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を交えて執筆する予定です。

タイトルとURLをコピーしました