본문 바로가기
Android 개발/Room, Realm, Databases

RoomDB 에서 One to Many 관계 구현하기 # 채팅 DB 구현

by Developer88 2023. 4. 7.
반응형

오늘은 RoomDB에서 One to Many 관계를 구현하는 방법에 대해서 정리해 보도록 하겠습니다.

이 글에서는 채팅의 예를 볼 텐데요.

채팅의 경우, 하나의 채팅룸에 다량의 메시지가 들어가는 one to many 구조를 가지고 있기 때문입니다.

 

참고로 이 글에서는 RoomDB구현에 대해서는 다루지 않으므로,

이에 관해서는 아래 글을 참조해 주세요.

>> Room DB 사용방법 총정리 # Android SQLite

 

1. 구현 전 필요한 채팅과 채팅룸 Entity

채팅과 채팅룸의 one to many 관계를 구현하기 전에 이들의 Entity가 있어야 하는데요.

먼저 채팅룸 Entity는 다음과 같습니다.

 

@Entity(tableName = "chat_rooms")
data class ChatRoom(
    @PrimaryKey(autoGenerate = true)
    val roomId: Long,
    val roomName: String,
    val createAt: Long = System.currentTimeMillis(),
    val updateAt: Long?,
    val isPinned: Boolean = false,
)

 

 

그리고 이와 연결된 채팅메세지 Entity는 다음과 같습니다.

 

@Entity(tableName = "chat_messages")
data class ChatMessage(
    @PrimaryKey(autoGenerate = true)
    val chatId: Long = 0,
    val chatRoomId: Long = 0,
    val messageText: String?,
    val senderId: String,
    val receiverId: String,
    val createdAt: Long = System.currentTimeMillis(),
    val updatedAt: Long?
)

 

이 둘을 연결해야 하는데요.

RoomDB에서는 어떻게 one to many 를 구현하는지 보도록 하겠습니다.

 

2. One to Many 구현하기

관계형 데이터에 대해서 배우신 분들이라면 ForeignKey를 정의하는 방법에 대해서 보려고 하실텐데요.

실제로 ForeignKey를 정의할 수도 있지만,

공식문서에서는 Intermediate data class를 사용하는 방법을 안내하고 있습니다.

이 글에서는 이 방법에 대해서 정리해 보도록 하겠습니다.

 

먼저 아래와 같은 data class를 생성해 줍니다.

@Embedded 애노테이션이 사용되었는데요.

이것은 ChatRoomWithMessages에서 다른 클래스를 포함시킬 때 사용합니다. (정확히는 해당 클래스의 필드들을 포함시킬 때를 의미)

여기서는 부모가 되는 Chatroom클래스의 필드를 사용하였습니다.

(하나의 챗룸에 여러개의 메시지가 붙는 구조이므로, 이것을 부모로 봅니다.)

 

 

 

그리고 @Relation 애노테이션이 사용된 두가지는 다음과 같은 의미를 가집니다.

  • parentColumn: 부모 Entity의 특정 컬럼을 지정합니다. 유니크한 컬럼이어야 하겠지요. 
  • entityColumn: 부모 Entity의 primary키를 참조하게 되는 자식 Entity의 특정 컬럼을 지정합니다. 부모 Entity의 PrimaryKey를 가지고 있는 컬럼이면 됩니다.

이런 Relation을 형성시켜주면, 부모자식 관계가 생겨서 부모인 Chatroom삭제시, 자식인 ChatMessage도 사라지게 됩니다.

 

이제 이러한 Messeages클래스타입의 함수를 Dao에 넣어주기만 하면 됩니다.

아래에서는 2가지 함수를 넣었는데요.

하나는 roomId에 따라서, 해당 룸과 그에 포함된 메시지를 리턴하는 것이구요.

다른 하나는 모든 챗룸을 메시지와 함께 하는 것 입니다.

 

참고로 Flow<> 타입으로 감싸서 데이터변경에 대해서 observe 할 수 있도록 하였는데요.

필요하지 않은 분들은 굳이 래핑할 필요는 없습니다.

 

@Dao
interface ChatRoomDao {
    ...

    @Transaction
    @Query("SELECT * FROM chat_rooms WHERE roomId = :roomId")
    fun getChatRoomWithMessages(roomId: Long): Flow<ChatRoomWithMessages>
    
    @Transaction
    @Query("SELECT * FROM chat_rooms")
    fun getChatRoomsWithMessages(): Flow<List<ChatRoomWithMessages>>
}

 

 

3. 팁

3-1. limit 쿼리

실제 프로덕트에서 쿼리를 사용할 때는 limit를 걸어서 많이 사용하게 되는데요.

limit은 offset과 같이 사용하게 되는 경우가 많습니다.

왜냐하면, 한번만 호출하기보다는 읽을 수 있는부분까지 제한해서 받고, 그다음에 유저가 읽기 위해 올라가면 거기서부터 다시 로딩을 해 주는 것 이지요.

 

아래와 같은 쿼리가 있다고 가정해 보겠습니다.

 

SELECT * FROM chat_messages LIMIT :offset, :limit

 

offset과 limit은 다음과 같은 의미를 가집니다.

 

  • limit: 리턴되는 최대 row입니다. 8이라면 8개의 row데이터만 나오게 됩니다.
  • offset: 쿼리의 스타팅 포인트입니다. 0부터 시작하게 되는데요. 만약, 0~8까지 다읽게 된다면, 다음은 9부터 읽어주어야 하겠지요. 그럴 경우는 offset을 8로 해주면 됩니다

 

3-2. custom 쿼리 사용시 rawString 사용하기

참고로 custom한 쿼리를 사용하게 될 경우는,

Kotlin의 rawString을 사용하면 편리한데요.

개행이 필요할 때, \n할필요없이 쿼리를 작성할 수 있습니다.

 

val rawString = """
    TEST
    TEST
    TEST
"""

 

728x90

댓글