๊ฐœ๋ฐœ ์ผ์ง€ ๐Ÿ’ป/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