App 개발 일지/Nutri Capture

Nutri Capture 백엔드 - Room의 @DAO, @Database 구현

interfacer_han 2024. 10. 23. 12:26

#1 개요

이전 게시글에 이어, Room의 남은 부분을 구현한다.

 

#2 코드

#2-1 @DAO

// package com.example.nutri_capture_new.db

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import java.time.LocalDate

@Dao
interface MainDAO {
    @Query("SELECT day_id FROM day_table WHERE day_date = :date LIMIT 1")
    suspend fun getDayId(date: LocalDate): Long?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertDay(day: Day): Long

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertMeal(meal: Meal): Long

    @Delete
    suspend fun deleteMeal(meal: Meal): Int

    @Query("""
    SELECT meal_table.* 
    FROM meal_table 
    INNER JOIN day_table ON meal_table.day_id = day_table.day_id 
    WHERE day_table.day_date = :targetDate 
    ORDER BY meal_table.meal_time ASC
    """)
    suspend fun getMealsOrderedByTime(targetDate: LocalDate): List<Meal>

    @Query("SELECT COUNT(*) FROM meal_table WHERE day_id = :dayId")
    suspend fun getMealCountForDay(dayId: Long): Int

    @Query("DELETE FROM day_table WHERE day_id = :dayId")
    suspend fun deleteDay(dayId: Long)
}

처음부터 이 모든 함수들을 생각해낸 것은 아니다. 본 게시글 이후 게시글에서 INSERT, SELECT 등을 View에서부터 Trigger하는 코드를 만들었는데 해당 코드를 만들며 이 DAO의 함수들과 아래에 있는 Repository의 함수들을 하나하나 만들어냈다. 즉, 지금은 @Dao 어노테이션이 붙은 인터페이스 하나만 정의하고 넘어가도 상관이 없다는 말이다.

 

#2-2 Repository

// package com.example.nutri_capture_new.db

import java.time.LocalDate

class MainRepository(private val dao: MainDAO) {
    suspend fun insertMeal(meal: Meal, date: LocalDate): Long {
        var dayId = dao.getDayId(date)
        if (dayId == null) {
            dayId = dao.insertDay(Day(date = date))
        }
        return dao.insertMeal(meal.copy(dayId = dayId))
    }

    suspend fun deleteMeal(meal: Meal): Int {
        val deletedRowCount = dao.deleteMeal(meal)

        if (deletedRowCount == 0) {
            return 0

        } else {
            val mealCount = dao.getMealCountForDay(meal.dayId)
            if (mealCount == 0) {
                dao.deleteDay(meal.dayId)
            }
            return deletedRowCount
        }
    }

    suspend fun getMealsOrderedByTime(targetDate: LocalDate): List<Meal> {
        return dao.getMealsOrderedByTime(targetDate)
    }
}

insertMeal( ... )

Meal을 INSERT하려고 시도할 때 부모 테이블 즉, Day 테이블이 존재하지 않으면 에러가 날 것이다. 따라서, Day 테이블이 없다면 먼저 Day 테이블부터 생성하는 코드를 집어넣는다.

 

deleteMeal( ... )

Meal이 하나도 없는 Day는 존재 이유가 없다. 따라서, Meal이 하나도 없다면 Day 테이블을 삭제한다.

 

getMealsOrderedByTime( ... )

어떤 날(Day)의 먹은 것들(List<Meal>)을 오름차순(#2-1의 DAO 코드 참조)으로 가져온다.

 

#2-3 TypeConverter

 

Room을 사용하여 복잡한 데이터 참조  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Room을 사용하여 복잡한 데이터 참조 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Room은 기본 유형과

developer.android.com

Room은 LocalDate형 및 LocalTime형 변수를 내부적으로 저장하지 못한다. 따라서 위 공식문서의 가이드를 따라 아래 있는 TypeConverter 클래스를 만들었다.

 

// package com.example.nutri_capture_new.db

import androidx.room.TypeConverter
import java.time.LocalDate
import java.time.LocalTime
import java.time.format.DateTimeFormatter

class Converters {
    private val dateFormatter = DateTimeFormatter.ISO_LOCAL_DATE
    private val timeFormatter = DateTimeFormatter.ISO_LOCAL_TIME

    // LocalDate -> String
    @TypeConverter
    fun fromLocalDate(date: LocalDate?): String? {
        return date?.format(dateFormatter)
    }

    // String -> LocalDate
    @TypeConverter
    fun toLocalDate(dateString: String?): LocalDate? {
        return dateString?.let { LocalDate.parse(it, dateFormatter) }
    }

    // LocalTime -> String
    @TypeConverter
    fun fromLocalTime(time: LocalTime?): String? {
        return time?.format(timeFormatter)
    }

    // String -> LocalTime
    @TypeConverter
    fun toLocalTime(timeString: String?): LocalTime? {
        return timeString?.let { LocalTime.parse(it, timeFormatter) }
    }
}

 

#2-4 @Database

// package com.example.nutri_capture_new.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

@Database(
    entities = [
        Day::class,
        Meal::class
    ],
    version = 1
)
@TypeConverters(Converters::class)
abstract class MainDatabase : RoomDatabase() {

    abstract val mainDAO: MainDAO

    companion object {
        @Volatile
        private var INSTANCE: MainDatabase? = null
        fun getInstance(context: Context): MainDatabase {
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Room.databaseBuilder(
                        context.applicationContext,
                        MainDatabase::class.java,
                        "main_database"
                    ).build()
                    INSTANCE = instance
                }
                return instance
            }
        }
    }
}

이 게시글에 있는 Database 상용구 코드를 그대로 사용했다. #2-3에서 만든 TypeConverter를 추가하는 구문(@TypeConverters(Converters::class))도 넣어준다.

 

#3 요약

Room의 Entity 부분을 제외한 나머지(Dao와 Database 그리고 TypeConverter와 Repository)를 구현했다.

 

#4 완성된 앱

#4-1 이 게시글 시점의 Commit

 

GitHub - Kanmanemone/nutri-capture-new

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

github.com

 

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

 

GitHub - Kanmanemone/nutri-capture-new

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

github.com