#1 개요
#1-1 이전 게시글 폐기
ERD에 대해 다뤘던 이전 게시글은 앱의 방향성을 재고한 이 게시글과 상충된다. 따라서 이전에 만들었던 ERD대로 데이터베이스의 스키마를 형성하지 않을 것이다. 새 ERD는 아래와 같다.
#1-2 새 ERD
meal_table과 nutrition_info_table이 1 : n 관계인 것처럼 되어있는데 실제로는 그렇지 않다. nutrition_info_table은 meal_table의 기본키를 외래키이자 기본키로 쓰는, 식별 관계의 자식 테이블이기 때문에 1 : 1 관계다. 위 ERD 이미지는 DBeaver를 통해 뽑아낸 것인데 아마 오류가 난 것 같다. 아니면 1..n라는 표기가 1 : 1과 1 : n을 모두 아우르는 표현식일까? 아무튼 그렇다.
-- Day 테이블
CREATE TABLE "day_table" (
"day_id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"date" TEXT NOT NULL
);
-- Meal 테이블
CREATE TABLE "meal_table" (
"meal_id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"day_id" INTEGER NOT NULL,
"time" TEXT NOT NULL,
"name" TEXT NOT NULL,
-- Foreign key 설정
FOREIGN KEY ("day_id") REFERENCES "day_table" ("day_id") ON DELETE CASCADE
);
-- NutritionInfo 테이블
CREATE TABLE "nutrition_info_table" (
"meal_id" INTEGER PRIMARY KEY NOT NULL,
"overeating_excess" INTEGER NOT NULL, -- 과식 정도값
"refined_sugar_excess" INTEGER NOT NULL, -- 정제당 섭취 정도값
"refined_grain_excess" INTEGER NOT NULL, -- 정제 곡물 섭취 정도값
"flour_excess" INTEGER NOT NULL, -- 밀가루 섭취 정도값
"fiber_quality" INTEGER NOT NULL, -- 섬유질 섭취 정도값
"protein_quality" INTEGER NOT NULL, -- 단백질 섭취 정도값
"sodium_excess" INTEGER NOT NULL, -- 나트륨 섭취 정도값
-- Foreign key 설정 (Meal과 식별 관계)
FOREIGN KEY ("meal_id") REFERENCES "meal_table" ("meal_id") ON DELETE CASCADE
);
데이터베이스 스키마를 SQLite의 SQL문으로 보면 위와 같다. NutritionInfo 테이블의 Column들은 여기에서 도출했던 걸 넣었으며 앞으로 계속 업데이트해 나갈 것이다. 많을수록 나쁜거에는 Excess, 많을수록 좋은거에는 Quality를 붙이는 규칙을 두었다. 쓸데없이 변수이름의 길이를 늘리는 규칙이 될 수도 있겠으나, 더 직관적인 의미를 알려주는 장점도 있다고 생각해서 만들었다. 이 규칙이 나중에 불필요하다고 판단되면 없애겠다.
#1-3 Room 라이브러리
Room에 관한 내용은 위 게시글에 기반하여 작성했다.
#2 코드 - Entity용 data class 만들기
#2-1 개요
다음 게시글에서 Room의 @Entity 어노테이션 등을 이용해 데이터베이스 Entity로 만들 클래스의 내용을 미리 작성한다. #2-2 ~ #2-4에 있는 data class들은 모두 "db"라는 새로운 패키지를 만들어 위치시킨다.
#2-2 Day
// package com.example.nutri_capture_new.db
import java.time.LocalDate
data class Day(
val dayId: Long = 0,
var date: LocalDate,
)
#2-3 Meal
// package com.example.nutri_capture_new.db
import java.time.LocalTime
data class Meal(
val mealId: Long = 0,
// 외래키
val dayId: Long = 0,
var time: LocalTime,
var name: String,
val nutritionInfo: NutritionInfo,
)
#2-4 NutritionInfo
// package com.example.nutri_capture_new.db
import androidx.room.ColumnInfo
data class NutritionInfo(
// 과식 정도값
@ColumnInfo(name = "overeating_excess")
var overeatingExcess: Int = 0,
// 정제당 섭취 정도값
@ColumnInfo(name = "refined_sugar_excess")
var refinedSugarExcess: Int = 0,
// 정제 곡물 섭취 정도값
@ColumnInfo(name = "refined_grain_excess")
var refinedGrainExcess: Int = 0,
// 밀가루 섭취 정도값
@ColumnInfo(name = "flour_excess")
var flourExcess: Int = 0,
// 섬유질 섭취 정도값
@ColumnInfo(name = "fiber_quality")
var fiberQuality: Int = 0,
// 단백질 섭취 정도값
@ColumnInfo(name = "protein_quality")
var proteinQuality: Int = 0,
// 나트륨 섭취 정도값
@ColumnInfo(name = "sodium_excess")
var sodiumExcess: Int = 0
)
원래 "nutrient" 패키지에 있던 클래스다. "db" 패키지로 옮기고, 생성자 프로퍼티들도 위와 같이 업데이트한다.
#3 코드 - Entity 만들기
#3-1 Room 라이브러리 다운로드
프로젝트 수준 build.gradle
plugins {
...
// KSP (어노테이션 읽기용)
id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false
}
#1-3에 있는 Room 게시글과는 달리, 어노테이션 읽기를 위해 kapt 대신 KSP를 사용할 것이다.
모듈 수준 build.gradle
plugins {
...
// KSP (어노테이션 읽기용)
id("com.google.devtools.ksp")
}
android {
...
}
dependencies {
...
// Room
implementation (libs.androidx.room.runtime)
implementation (libs.androidx.room.ktx)
// KSP (어노테이션 읽기용)
ksp(libs.androidx.room.compiler)
}
libs.versions.toml
[versions]
...
roomVersion = "2.6.1"
[libraries]
...
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
[plugins]
...
#3-2 Day
// package com.example.nutri_capture_new.db
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import java.time.LocalDate
@Entity(
tableName = "day_table",
indices = [Index(value = ["day_date"], unique = true)]
)
data class Day(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "day_id")
val dayId: Long = 0,
@ColumnInfo(name = "day_date")
var date: LocalDate,
)
indices를 통해 date를 인덱싱한 이유는, date를 unique = true 속성을 부여하기 위한 선행 조건이기 때문이다. 당연하지만, 이 프로젝트에서 date에 중복이 발생해선 안 된다.
#3-3 Meal
// package com.example.nutri_capture_new.db
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import java.time.LocalTime
@Entity(
tableName = "meal_table",
foreignKeys = [
ForeignKey(
entity = Day::class,
parentColumns = ["day_id"],
childColumns = ["day_id"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Meal(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "meal_id")
val mealId: Long = 0,
// 외래키
@ColumnInfo(name = "day_id")
val dayId: Long = 0,
@ColumnInfo(name = "meal_time")
var time: LocalTime,
@ColumnInfo(name = "meal_name")
var name: String,
@Embedded
val nutritionInfo: NutritionInfo,
)
@Embedded는 어떤 객체를 통째로 가져와 Column에 넣는 기능을 한다. 즉, 사실상 Meal에 Column을 추가하는 것과 마찬가지다. NutritionInfo는 Meal의 식별 관계 자식 테이블(#1-2에 있는 도식도 참조)이다. 식별 관계 자식 테이블은 부모 테이블의 Column을 추가하는 것과 같은 역할을 수행한다. 따라서, 위 코드와 같은 방식으로 Room의 스키마를 짜면 식별 관계를 표현할 수 있는 것이다.
#3-4 NutritionInfo
// package com.example.nutri_capture_new.db
import androidx.room.ColumnInfo
data class NutritionInfo(
// 과식 정도값
@ColumnInfo(name = "overeating_excess")
val overeatingExcess: Int = 0,
// 정제당 섭취 정도값
@ColumnInfo(name = "refined_sugar_excess")
val refinedSugarExcess: Int = 0,
// 정제 곡물 섭취 정도값
@ColumnInfo(name = "refined_grain_excess")
val refinedGrainExcess: Int = 0,
// 밀가루 섭취 정도값
@ColumnInfo(name = "flour_excess")
val flourExcess: Int = 0,
// 섬유질 섭취 정도값
@ColumnInfo(name = "fiber_quality")
val fiberQuality: Int = 0,
// 단백질 섭취 정도값
@ColumnInfo(name = "protein_quality")
val proteinQuality: Int = 0,
// 나트륨 섭취 정도값
@ColumnInfo(name = "sodium_excess")
val sodiumExcess: Int = 0
)
@Entity로서 자립하지 않고, @Embedded를 통해, Column만 Meal에 전달된다. 따라서, @Entity 어노테이션은 없고, @ColumnInfo 어노테이션만 존재한다.
#4 요약
새 ERD와 해당 ERD를 구현하는 Room Entity를 만들었다.
#5 완성된 앱
#5-1 이 게시글 시점의 Commit
#5-2 본 프로젝트의 가장 최신 Commit
'App 개발 일지 > Nutri Capture' 카테고리의 다른 글
Nutri Capture 백엔드 - Model을 ViewModel에 생성자 주입 (0) | 2024.10.23 |
---|---|
Nutri Capture 백엔드 - Room의 @DAO, @Database 구현 (1) | 2024.10.23 |
Nutri Capture 프론트엔드 - Card (0) | 2024.10.17 |
Nutri Capture 프론트엔드 - 스크롤 로직 View에 일임 (0) | 2024.10.16 |
Nutri Capture 프론트엔드 - requestScrollToItem()을 이용한 깔끔한 역방향 무한 스크롤 (0) | 2024.10.16 |