[Android] Room - Entity(table) 간의 관계(relationship) 정의하기

2021. 11. 28. 22:42Android

반응형

지난주에 사이드 프로젝트로 개발하던 개인 앱을 플레이스토어에 출시했다. 사용자가 등록한 RSS 피드를 파싱 해 보여주는 앱으로, 개인적인 필요성에 의해 만들게 된 앱이다. 최근에는 앱 개발 마무리를 하느라 블로그에 글을 쓰지 못했다.
개발하면서 트러블 슈팅한 내용들을 따로 정리만 해놓았어서, 이 내용들을 바탕으로 그동안 밀렸던 블로그 글을 쓰려고 한다 😉

 

이번 글은 room database 에서 relational query를 사용해 원하는 데이터를 얻는 방법에 대해 설명하려고 한다.

데일리피드 앱에서는 서버 없이 로컬 저장소만으로 앱을 구성했기 때문에, room DB에 많이 의존을 했는데, 이때 겪었던 트러블 슈팅을 통해 알게 된 내용을 공유한다. 

 

목차

     


     

    Entity(table) 간의 관계 정의하기

    SQLite 는 관계형 데이터베이스(relational database)이다. 따라서, room에서도 관계형 쿼리(relational query)를 사용할 수 있다.

    Room에서는 2가지 방식을 통해 관계형 쿼리를 할 수 있다.

    • (이번 글에서 설명하려는 방식) Intermediate data class  
      • 포함된 객체가 있는 중간 데이터 클래스를 사용하여 관계를 모델링
    • (권장 방식) Multimap return types
      • multimap 리턴 타입의 관계형 쿼리 (Join 등을 사용하여)
      • 공식문서에서는 이 방식을 권장하고 있다. (이 방식으로 리팩토링 후 별도의 글로 올릴 예정)

     

      장점 단점
    Intermediate data class SQL query 가 간단 추가 data class 를 생성해 코드의 복잡도가 올라간다
    Multimap return types SQL query 복잡 별도의 추가 클래스를 생성할 필요가 없음.

     

    Multimap return types 방식 (권장)

    이 경우에는 별도의 임시 data class 를 생성할 필요가 없다. 대신 multimap 리턴 타입을 정의하면 된다.

    @Query(
        "SELECT * FROM user" +
        "JOIN book ON user.id = book.user_id"
    )
    fun loadUserAndBookNames(): Map<User, List<Book>>
    

     

    room 2.4 이상 버전 부터는 이 방법만을 지원한다고 하니 기억할 것!

    💡 Note: Room only supports multimap return types in version 2.4 and higher.

     

    이 방식에 대한 자세한 내용은 추후 앱에 적용 후 올릴 예정.

     

    Intermediate data class 방식 (deprecate 될 예정)

    room 2.4 이상 버전부터는 지원되지 않을 방식이다. 즉, deprecate 될 예정

    그렇지만.. 이번 글에서는 이 방법에 대해 설명하려고 한다. 이유는, 개인 앱 개발 시 이 방법을 사용했고, deprecated 될 예정은 글을 쓰면서 알게 되었는데... 그래서 글을 올리지 않으려다가, 그래도 이 방법이 필요한 분들이 계실 수도 있을 것 같아 트러블 슈팅하면서 알게 된 내용을 공유하려고 올린다. 

    자세한 내용은 아래 예시를 통해 알아보자. 

     


     

    예시를 통해 Intermediate data class 방식에 대해 알아보자

    데일리피드 개발 시, 로컬 데이터 베이스의 테이블 관계는 아래와 같다.

    데일리피드 ERD

     

    🧤 이 때, 북마크 한 아이템들의 정보(domain 데이터도 같이)를 가져오고 싶다!

     

    이해를 돕기 위해, 위 테이블에 대한 room entity 클래스는 아래와 같다.

    @Entity(tableName = "rssItems")
    data class RssItemEntity(
        @PrimaryKey(autoGenerate = true)
        val itemId: Long? = null,
        // 해당 피드의 domain 정보
        val itemDomainId: Long,
    )
    
    @Entity(tableName = "domains")
    data class RssDomainEntity(
        @PrimaryKey(autoGenerate = true)
        val domainId: Long? = null,
        val rssAddress: String,
    )
    
    @Entity(tableName = "bookmarks")
    data class BookmarkEntity(
        @PrimaryKey(autoGenerate = true)
        val bookmarkId: Long? = null,
        // 해당 피드의 item id
        val bookmarkItemId: Long
    )

     

    1. domains 테이블과 rssItems 테이블의 관계를 정의

    [rssItems] 테이블의 row 데이터를 가져오는데, 이때 [rssItems.itemDomainId]에 해당하는 [domains] 테이블의 정보도 같이 가져오고 싶다!

    그렇다면, domain 데이터도 같이 담을 수 있는 클래스를 추가로 만들어주자. (Intermediate data class를 생성하는 것)

    data class RssItemWithDomain(
        // [rssItems] 테이블의 row 데이터 
        @Embedded 
        val rssItem: RssItemEntity,
    
        /**
        * [rssItems] 테이블과 [domains] 테이블의 relation
        *  
        * [parentColumn] 은 [rssItems] 테이블 내 컬럼명
        * [entityColumn] 은 [domains] 테이블 내 컬럼명
        */
        @Relation(
            parentColumn = "itemDomainId",
            entityColumn = "domainId"
        )
        val domainRoomEntity: RssDomainEntity,
    )

    위의 RssItemWithDomain 에 대한 설명 👇

    • 기준 entity class(rssItems 테이블 데이터인 RssItemEntity)에  @Embedded 어노테이션을 붙인다.
    • 위 entity 에서 관계형 쿼리를 통해 가져오려는 entity class (domains 테이블 데이터인 RssDomainEntity)를 추가하고, @Relation 어노테이션을 붙인다. @Relation 에는 아래 2개의 파라미터를 넣어준다.
      • parentColumn : @Embedded된 클래스의 관계형 컬럼명. 즉, 여기서는 RssItemEntity 의 itemDomainId 가 들어가게 된다.
      • entityColumn : @Relation어노테이션이 붙은 클래스의 관계형 컬럼명. 즉, 여기서는 RssDomainEntity 의 domainId 가 들어간다.

     

    2. rssItems 테이블과 bookmarks 테이블의 관계를 정의

    [bookmarks] 테이블의 row 데이터를 가져오는데, 이때 [bookmarkItemId]에 해당하는 [rssItems] 테이블의 정보도 같이 가져오고 싶다!

    그렇다면, rss item 데이터도 같이 담을 수 있는 클래스를 생성하자. (1번 과정과 동일)

    data class BookmarkWithItem(
        // [bookmarks] 테이블의 row 데이터 
        @Embedded 
        val bookmarkEntity: BookmarkEntity,
    
        @Relation(
            parentColumn = "bookmarkItemId",
            entity = RssItemEntity::class,
            entityColumn = "itemId"
        )
        // 1️번에서 생성한 클래스. rssItem 과 domain 정보가 모두 들어있는 클래스
        val itemWithDomain: RssItemWithDomain
    )

     

    코드 간단 요약

    [bookmarks] 테이블 내 정보에 "추가"로 [rssItems] 테이블의 정보도 가져오고 싶은 것이니!

    • BookmarkEntity 에 @Embedded 을 붙이고,
    • 추가하고 싶은 클래스인 RssItemWithDomain 에 @Relation 을 붙여 관계형 쿼리를 통해 데이터를 가져온다!
      • 이때, RssItemWithDomain 은 room Entity (테이블) 클래스가 아니기 때문에, @Relation 에 별도로 entity 파라미터를 명시해준다.

     

    3. 실제 쿼리를 날려보자.

    위에서 관계형 클래스를 만들어놨기 때문에, 쿼리는 정말 쉽다. (가장 위에서 설명한 Intermediate data class 방식의 장점)

    • 그냥 [bookmarks]에서 모든 데이터를 가져오고,
    • 리턴 타입을 위의 2번에서 생성한 BookmarkWithItem 로 명시하면 끝.
    • 단, 이는 하나의 쿼리만으로 데이터를 가져오는 것이 아니기 때문에 반드시 @Transaction어노테이션을 붙여줘야 한다.
    @Transaction
    @Query("SELECT * FROM bookmarks")
    fun getBookmarkList(): Flow<List<BookmarkWithItem>>

     

     

     

    구현할 땐 복잡했는데.. 다 하고 나서 보니까 별 것 아니네!!! 끝 !

     

    반응형