Room에서 포함 관계의 객체 사용하기

권민주·2025년 11월 9일

안드로이드

목록 보기
22/23
post-thumbnail

1. @Embedded

1)개념

@Target(allowedTargets=[AnnotationTarget.FIELD, AnnotationTarget.FUNCTION])
@Retention(value = AnnotationRetention.BINARY)
public annotation Embedded
public Embedded(@NonNull String prefix)
  • 객체의 필드를 테이블에 평탄화(flatten)하는 역할
  • 엔티티나 POJO의 필드에 사용하면, 해당 필드 클래스의 하위 필드(중첩된 필드)를 SQL 쿼리에서 직접 참조 가능
  • 컨테이너가 엔티티일 경우, 하위 필드들은 엔티티의 데이터베이스 테이블 내 컬럼으로 포함

2)이름 충돌

  • 하위 객체의 필드 이름과 상위 객체의 필드 이름이 충돌할 경우, 하위 객체 필드에 접두어(prefix) 지정 가능
  • 접두어는 하위 필드에 항상 적용되며, 하위 필드가 @ColumnInfo 로 이름이 지정되어 있더라도 접두어가 우선 적용
  • 하위 객체의 필드가 @PrimaryKey로 표시되어 있더라도, 상위 엔티티의 기본 키로 간주되지 않음

3)null 처리

  • 필드 및 그 하위 필드의 모든 컬럼 값이 android.database.Cursor에서 null이라면, Room은 그 필드를 null로 설정
  • 하나라도 null이 아닌 값이 있으면 Room은 하위 객체를 생성
  • TypeConverter가 null 컬럼을 non-null 값으로 변환하도록 정의되어도, 하위 필드의 모든 컬럼이 null이면 TypeConverter는 호출X. 해당 @Embedded 필드는 생성되지 않음.
  • null 컬럼을 non-null 값으로 변환하려면, @Embedded 필드에 androidx.annotation.NonNull 애너테이션 추가

4)예제

data class Coordinates (
  val latitude: Double,
  val longitude: Double
)

data class Address (
  val street: String,
  @Embedded
  val coordinates: Coordinates
)
  • Room은 latitudelongitudeAddress 클래스의 필드처럼 취급
  • 쿼리 결과가 street, latitude, longitude 컬럼을 반환하면, Room은 Address 객체 구성
  • Address 클래스가 @Entity로 선언되어 있으면, 데이터베이스 테이블은 street, latitude, longitude 세 컬럼을 보유

2. @Relation

1)개념

@Target(allowedTargets=[AnnotationTarget.FIELD, AnnotationTarget.FUNCTION])
@Retention(value = AnnotationRetention.BINARY)
public annotation Relation
public Relation(
    @NonNull KClass<@NonNull ?> entity,
    @NonNull String parentColumn,
    @NonNull String entityColumn,
    @NonNull Junction associateBy,
    @NonNull String[] projection
)	
  • POJO 안에서 관계가 있는 엔티티들을 자동으로 가져오기 위해 사용하는 편의 애너테이션
  • 해당 애너테이션이 붙은 POJO가 쿼리 결과로 반환되면, Room은 해당 객체의 모든 관계 데이터도 함께 조회
  • 1:N 또는 N:M 관계에서는, @Relation이 붙은 필드의 타입이 java.util.Listjava.util.Set 이어야 함.

2)entity 속성

  • Room은 반환 타입에서 엔티티 유형을 추론
  • 다른 객체 타입을 반환하고 싶다면, @Relationentity 속성으로 명시 가능
@Entity
data class Song(
    @PrimaryKey
    val songId: Int,
    val albumId: Int,
    val name: String
    // 기타 필드
)

data class AlbumNameAndAllSongs(
    val id: Int,
    val name: String,
    @Relation(parentColumn = "id", entityColumn = "albumId")
    val songs: List<Song>
)

@Dao
interface MusicDao {
    @Query("SELECT id, name FROM Album")
    fun loadAlbumAndSongs(): List<AlbumNameAndAllSongs>
}
  • SongNameAndId는 POJO이지만, 그 안의 모든 필드는 @Relation 애너테이션의 entity로 지정된 Song 엔티티에서 가져옴
  • SongNameAndId 안에 다른 관계가 정의되어 있더라도, Room은 그것들도 자동으로 함께 가져옴
@Entity
data class Song(
    @PrimaryKey
    val songId: Int,
    val albumId: Int,
    val name: String
    // 기타 필드
)

data class Album(
    val id: Int
    // 기타 필드
)

data class SongNameAndId(
    val songId: Int,
    val name: String
)

data class AlbumAllSongs(
    @Embedded
    val album: Album,
    @Relation(
        parentColumn = "id", 
        entityColumn = "albumId", 
        entity = Song::class
    )
    val songs: List<SongNameAndId>
)

@Dao
interface MusicDao {
    @Query("SELECT * FROM Album")
    fun loadAlbumAndSongs(): List<AlbumAllSongs>
}

3)projection

  • projection 속성을 통해 자식 엔티티에서 어떤 컬럼만 조회할지 지정 가능
data class AlbumAndAllSongs(
    @Embedded
    val album: Album,
    @Relation(
        parentColumn = "id",
        entityColumn = "albumId",
        entity = Song::class,
        projection = ["name"]
    )
    val songNames: List<String>
)

4)associateBy

  • 관계가 조인 테이블을 통해 정의되어 있다면, associateBy 속성을 통해 해당 테이블 지정 가능
  • N:M 관계를 조회할 때 유용

5)POJO 전용

  • @Relation POJO 클래스에서만 사용 가능
  • 데이터베이스에서 각 객체 모델로 관계를 매핑하는 것은 일반적인 관행
  • 클라이언트 측에서의 지연 로드는 일반적으로 UI 스레드에서 발생되기에 실행 불가능. UI 스레드에서 디스크에 관한 정보를 쿼리하면 상당한 성능 문제가 발생.
  • UI 스레드는 activity의 업데이트된 레이아웃을 계산하고 그리는 데 약 16ms 소요. 쿼리가 5ms밖에 걸리지 않아도 앱에서 프레임을 그리는 데 시간 부족 발생. 이에 따라 시각적 결함이 존재
  • 병렬로 실행 중인 별도의 트랜잭션이 있거나 기기가 다른 디스크 집약적인 작업을 실행 중이면, 쿼리가 완료되는 데 훨씬 많은 시간 소요
  • 지연 로드를 사용하지 않으면 앱이 필요한 것보다 더 많은 데이터를 가져옴. 이에 따라 메모리 소비 문제가 발생.
  • Room에서 데이터를 로드할 때, 엔티티를 상속하는 POJO 클래스를 만들어 제한 우회 가능

3. @Transaction

1)개념

@Target(allowedTargets = [AnnotationTarget.FUNCTION])
@Retention(value = AnnotationRetention.BINARY)
public annotation Transaction
  • DAO 클래스의 메서드가 하나의 데이터베이스 트랜잭션으로 실행되도록 표시하는 애너테이션
  • 추상 DAO 클래스의 비추상 메서드에 사용될 경우, 해당 메서드의 파생 구현은 데이터베이스 트랜잭션 안에서 상위(super) 메서드를 실행. 모든 매개변수와 반환 타입은 그대로 유지
  • 메서드 본문에서 예외가 발생하지 않는 한, 트랜잭션은 성공
  • Room은 한 번에 최대 하나의 트랜잭션만 수행. 추가 트랜잭션은 대기열에 들어가며, 먼저 요청된 순서대로 실행
@Dao
abstract class SongDao {
    @Insert
    abstract fun insert(song: Song)
    @Delete
    abstract fun delete(song: Song)
    @Transaction
    fun insertAndDeleteInTransaction(newSong: Song, oldSong: Song) {
        // 이 메서드 안의 모든 동작은 하나의 트랜잭션 안에서 실행
        insert(newSong)
        delete(oldSong)
    }
}

2)SELECT

  • SELECT 문을 가진 Query 메서드에 사용될 때, 생성된 Query 코드는 트랜잭션 안에서 실행. 이를 사용하는 주요 이유는 두 가지.
    • 결과가 상당히 큰 쿼리의 경우: 트랜잭션 내에서의 실행이 일관된 결과 수신에 용이. 사용하지 않으면 쿼리 결과가 단일 android.database.CursorWindow에 맞지 않을 때, CursorWindow 교체 사이에 데이터베이스가 변경되어 결과가 손상
    • 쿼리 결과가 @Relation 필드를 가진 POJO일 경우: @Relation 필드들은 별도의 쿼리로 조회. 쿼리 간의 일관된 결과를 보장하려면 하나의 트랜잭션으로 실행해야 함.
data class AlbumWithSongs : Album (
    @Relation(parentColumn = "albumId", entityColumn = "songId")
    val songs: List<Song>
)
@Dao
public interface AlbumDao {
    @Transaction
    @Query("SELECT * FROM album")
    fun loadAll(): List<AlbumWithSongs>
}

3)비동기 쿼리

  • androidx.lifecycle.LiveData 또는 RxJavaFlowable를 반환하여 쿼리가 비동기적일 경우, 트랜잭션은 메서드가 호출될 때 처리X. 쿼리가 실제로 실행될 때 처리.
  • @Insert, @Update, @Delete 메서드에는 영향X. 이러한 메서드는 항상 트랜잭션 안에서 실행되기 때문.
  • @Query 메서드가 INSERT, UPDATE, DELETE 문을 실행하는 경우에도 자동으로 트랜잭션이 감싸지므로 효과X

4. 중첩된 관계

1)개념

  • 서로 관련이 있는 세 개 이상의 테이블 집합을 쿼리할 때, 테이블 간에 중첩된 관계를 정의
  • 중첩된 관계가 있는 데이터를 쿼리하려면 Room에서 많은 양의 데이터를 조작되기에 성능에 영향. 그러므로 최소한의 사용 권장.

2)예제

  • 음악 스트리밍 앱의 예에서 모든 사용자, 각 사용자의 모든 재생목록, 각 사용자의 각 재생목록에 있는 모든 노래를 쿼리
  • 사용자는 재생목록과 일대다 관계, 재생목록은 노래와 다대다 관계
  • UserWithPlaylistsAndSongs 클래스는 세 가지의 모든 항목 클래스(User, Playlist, Song) 간의 관계를 간접적으로 모델링
  • 쿼리하려는 모든 테이블 간의 중첩된 관계 체인이 생성
@Entity
data class User(
    @PrimaryKey val userId: Long,
    val name: String,
    val age: Int
)

@Entity
data class Playlist(
    @PrimaryKey val playlistId: Long,
    val userCreatorId: Long,
    val playlistName: String
)

@Entity
data class Song(
    @PrimaryKey val songId: Long,
    val songName: String,
    val artist: String
)

@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
    val playlistId: Long,
    val songId: Long
)

// Playlist 항목 클래스와 Song 항목 클래스 간의 다대다 관계
data class PlaylistWithSongs(
    @Embedded val playlist: Playlist,
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val songs: List<Song>
)

//User 항목 클래스와 PlaylistWithSongs 관계 클래스 간의 일대다 관계를 모델링
//새 관계 내부에 기존 관계를 중첩
data class UserWithPlaylistsAndSongs(
    @Embedded val user: User
    @Relation(
        entity = Playlist::class,
        parentColumn = "userId",
        entityColumn = "userCreatorId"
    )
    val playlists: List<PlaylistWithSongs>
)

// Room에서 여러 쿼리를 실행해야 함
// 전체 작업이 원자적으로 실행되도록 @Transaction 주석 추가
@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>

출처: https://developer.android.com/

profile
안드로이드 개발자:D

0개의 댓글