마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

Room DB 설계 질문좀 드립니다..

0 추천

 

이러한 화면을 만들고 있고.. 지난번 말씀해주신대로 

 

WorkoutSetInfo

data class WorkoutSetInfo(
    val id: String = UUID.randomUUID().toString(), // UUID 비교를 위한 ID
    val set: Int,
    val weight: String = "",
    val reps: String = ""
)

Workout

@Entity
data class Workout(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val title: String = "",
    val unit: String = "kg",
    val memo: String = "",
    var sets: List<WorkoutSetInfo> = emptyList()
)

이런식으로 Entity를 구성을 했고 

TypeConverter

class Converter {
    @TypeConverter
    fun listToJson(value: List<WorkoutSetInfo>) : String {
        return Gson().toJson(value)
    }

    @TypeConverter
    fun jsonToList(value: String) : List<WorkoutSetInfo> {
        return Gson().fromJson(value, Array<WorkoutSetInfo>::class.java).toList()
    }
}

 

DAO

@Dao
interface WorkoutDao {
    @Query("SELECT sets FROM Workout")
    fun getWorkoutSetInfoList() : LiveData<List<WorkoutSetInfo>>

    @Insert
    fun insert(workout: Workout)
}

Repository

class WorkoutRepository(private val workoutDao : WorkoutDao, title: String) {
    val workout = Workout(0, title)
    var setInfoList : ArrayList<WorkoutSetInfo> = arrayListOf()
    
    val _items: LiveData<List<WorkoutSetInfo>> = workoutDao.getWorkoutSetInfoList()

    fun add(item: WorkoutSetInfo) {
        setInfoList.add(item)
        workout.sets = setInfoList

        workoutDao.insert(workout)
    }
}

 

ViewModel

class DetailViewModel(application: Application, title: String) : ViewModel() {
    private val repository: WorkoutRepository

    private lateinit var _items: LiveData<List<WorkoutSetInfo>>
    val items = _items
    private val list: List<WorkoutSetInfo>
        get() = _items.value ?: emptyList()

    init {
        val workoutDao = DetailDatabase.getDatabase(application)!!.workoutDao()
        repository = WorkoutRepository(workoutDao, title)
        _items = repository._items
    }

    fun addDetail() {

        viewModelScope.launch(Dispatchers.IO){
            val item = WorkoutSetInfo(set = list.size+1)
            repository.add(item)
        }
    }
}

 

이렇게 짰는데..

error: The columns returned by the query does not have the fields [id,title,unit,memo] in com.example.lightweight.data.db.entity.Workout even though they are annotated as non-null or primitive. Columns returned by the query: [sets]
    public abstract androidx.lifecycle.LiveData<com.example.lightweight.data.db.entity.Workout> getWorkoutSetInfoList();

해당 에러를 얻었습니다..

 

뭐가 원인일까요? 코드가 지금 중구난방이라... 저도 뭐가 어떻게 되어가고 있는지 모르겠습니다.

 

추가로 DB 설계와 관련해서 Workout 내에 WokroutSetInfo 를 넣는 형식으로 Entity를 작성하라고 하셨는데,

아래 그림이 제가 최종적으로 만들 제가 생각해본 UI DB 방향 정도입니다..

날짜별로 운동을 작성을 합니다. 작성하는 부분은 초록색 박스와 파란색 박스입니다.,

작성을 완료하면 최종적으로 빨간색 박스에 저장 되겠죠.

즉 빨간 박스 테이블에는 날짜별로 진행한 운동 일지들이 모여있고,

각각 날짜별 튜플?레코드?에는

파란색박스(Work)이 여럿 존재하는 형태가 될텐데요.

 

지금 코드로 작성하고 있는 부분이 파란색 박스(Workout)과 초록색 박스(WorkoutSetInfo)입니다

지금은 위 그림과 달리

WorkoutSetInfo를 Workout 내부에 저장하는 식으로해서 하나의 테이블을 사용하고 있는 형태일텐데요..

궁금한것이 위 그림처럼 두개의 테이블을 사용하는것을 별로인가요?

벤치프레스라는 Workout을 예를들면

지금은 벤치프레스라는 Workout 내부에 WorkoutSetInfo 리스트가 존재합니다.

테이블로 보면 Workout 테이블의 벤치프레스 튜플(레코드)에 WorkoutSetInfo가 속성(필드)로 존재하겠죠..

그런데 이걸 나뉘어서 Workout 테이블과 WorkoutSetInfo 테이블을 나뉘어서

WorkoutSetInfo에서는 Workout의 벤치프레스 ID를 가지고 접근을 하는겁니다..

두개의 테이블을 사용하는 것이죠..

 

이렇게 생각도 해보았는데 이게 더 복잡한 형태가 될까요?

 

에러를 해결하지 못하다보니 자꾸 엉뚱한 방향으로 생각을하게되네요..

에러에 대한 해결방법과 위 DB설계도 괜찮은 방법인지 부탁드립니다 ㅠ

 

 

codeslave (3,940 포인트) 님이 2022년 4월 7일 질문
codeslave님이 2022년 4월 7일 수정

2개의 답변

0 추천
먼저, 에러메시지는 DB 의 sets필드에 JSON string을 저장하려고 하는데, 타입이  List<WorkoutSetInfo>여서 생기는 것 같네요.

몇가지를 짚어보면,

1. UI에서 사용하는 데이터 구조와 DB에 사용하는 데이터 구조 분리
현재 코드는 WorkoutSetInfo를 UI와 DB에 공유해서 사용하고 계시네요. 추후의 변경사항에 잘 대처하려면 분리하는게 좋습니다. UI에 필요한 데이터와 DB에서 사용하는 필드는 달라질 가능성이 매우 많아요.

2. DB 구조의 비일관성
sets필드를 JSON을 변환해서 저장하고 계신데, 제가 보기에는 Workout과 1대 n관계인 WorkoutInfo 테이블을 만드셔서 WorkoutInfo정보는 여기에 저장하시는게 자연스러운 RDB구조일 것 같네요.

님의 경우는 위의 그림에서 보이듯이, 아래의 테이블이 있어야 할 것 같구요.

신체부위(가슴)
운동종목(벤치프레스, 덥벨프레스, 플라이)
사용자정의 운동종목
운동일지
 

각테이블 간의 관계를 설정하는 부분이 중요합니다. 어떤 테이블이 다른 테이블에 대한 참조를 가질 것인지. 예를들면, 운동종목은 신체부위에 대한 포린키가 필요하고, 사용자정의 운동종목은 운동종목들에 대한 키가 필요하고(1:n), 운동일지는 사용자정의 운동종목에 대한 키가 필요하구요 (1:n). 쿼리를 할 때는 관련된 테이블을 조인해서 가져오셔야 하구요. 저장할 때는 트랙잭션을 통해 무결성을 유지하도록 처리해주시구요.
spark (227,510 포인트) 님이 2022년 4월 7일 답변
추가로 코루틴을 IO thread로 호출하는 부분은 뷰모델이 아니라 Repository 더 적합합니다.
음 그럼 현재처럼 Wokrout 클래스내에 WorkoutSetInfo 리스트가 존재하는 형태가아닌 이 두개를 따로 분리하고 마찬가지로 Workout 하나의 테이블에서 Workout과 WorkoutSetInfo 두개의 테이블로 분리하여서 진행하라는 말씀이시죠?
0 추천
제가 에제코드를 Github에 만들어서 올려놨어요.

https://github.com/krpot/WorkoutDiary

package 설명:

db - Room 관련 코드는 여기 다 있습니다.

data - Repository관련

ui - 뷰 레이어

DatatSetRepository를 보시면 데이터를 집어넣은 부분이 있습니다.

HomViewModel에 뷰로직이 다 들어 있어요.

해당 리포틑 몇일만 놔두고 삭제할테니까, 그 사이에 다운받아서 보세요.
spark (227,510 포인트) 님이 2022년 4월 7일 답변
아아 정말 감사합니다ㅠㅠ..하나하나 따라가면서 읽어보고 있습니다..
분명 쉽게 짜주신걸텐데도 저한테는 꽤나 어렵네요.
 ViewModel에서 분명히 Repository 두개를 파라미터로 받는데 Fragment에서 아무리봐도 인자로 전달하는게 없이 by viewmodels()로만 표시를 하셨길래 어떻게 된거지 하고 찾았는데
이게 Hilt Di라는거군요.. 종속성 주입이라는것을 들어는 봤지만 다른것부터 한다고 공부한적이 없었는데 여기서 또 알아가네요 ㅠ 감사합니다. 코드 이해하려고 종속성 개념부터 다시한번 보고있습니다..
감사합니다ㅠ
선생님 코드 보는 와중에 질문있습니다.
HomeViewModel 에서 dataSetRepository와 dailyWorkoutRepository를 둘다 사용하시던데 dataSetRepository 클래스를 보면 이 내부에서 dailyWorkoutRepository까지 다 있던데 dataSetRepository로 접근을하지않고 왜 별도의 dailyWorkoutRepository 인스턴스를 사용하는건가요?

dataSetRepository은 단순 이름그대로 데이터를 설정하기위한 Repository라서 그런건가요?
님이 사용하는 데이터의 구조가 모바일 데이터베이스 구조로는 생각보다 복잡해요. 그 부분을 제외한 나머지는 구글에서 권장하는 가이드와 거의 같구요.
dataSetRepository은 데이터를 초기화하는 부분을 보여주기 위해 집어넣은 코드이고, 딱 그용도로만 사용하는 클래스이기 때문에 그렇습니다. 만약 여기에 다른 기능을 집어넣고 코드를 짜보시면 한개의 repository가 관련되지 않은 일을 하게되는 경우가 많게 됩니다. 물론 클래스이름은 용도에 맞게 더 적합하게 만드는게 아주 중요합니다. 이것도 시간이 걸리는 일이라 베스트 네임은 아닐 겁니다. DataInitialisationRepository정도로 했으면 더 가독성이 있었겠네요.
...