App 개발 일지/Nutri Capture

Nutri Capture 백엔드 - 새 ERD와 Room의 @Entity 정의

interfacer_han 2024. 10. 23. 11:58

#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 라이브러리

 

[Android] Room - 기초, INSERT와 DELETE 연습

#1 Room 소개 Room을 사용하여 로컬 데이터베이스에 데이터 저장 | Android DevelopersRoom 라이브러리를 사용하여 더 쉽게 데이터를 유지하는 방법 알아보기developer.android.comSQLite는 모바일 기기를 위한 SQL

kenel.tistory.com

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

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com

 

#5-2 본 프로젝트의 가장 최신 Commit

 

GitHub - Kanmanemone/nutri-capture-new

Contribute to Kanmanemone/nutri-capture-new development by creating an account on GitHub.

github.com