본문 바로가기

[Android/Kotlin] AAC #5 - Room

AAC #5 - Room

 

AAC 연관 글

 

ORM

ORM은 Object Relational Mapping의 약어로 DB와 객체지향 프로그래밍 언어간의 호환되지 않는 데이터를 변환하는 프로그래밍 기법을 의미합니다

즉, DB테이블과 매핑되는 객체(인스턴스)를 만들고 그 객체에서 DB를 관리하는 것을 말합니다

 

 

Room

  • Room은 위에서 말한 ORM 기반의 라이브러리
  • SQLite 위에 추상화계층을 제공하여 SQLite에 객체를 매핑하는 역할을 하는 Library
  • 즉, SQLite의 기능을 모두 사용할 수 있고, DB로의 접근을 편하게 도와주는 Library
  • Room의 장점
    • 컴파일 시간에 SQL을 체크
    • 무의미한 boilerplate의 코드 반복 해결

 

 

Room 구성 요소

Room은 3가지의 주요 컴포넌트로 구성요소를 갖고있습니다

1. Database

  • Database Holder를 포함하며, App에 영구적으로 저장되는 data와 기본 연결을 위한 주 엑세스 포인트 역할
  • @Database로 어노테이션된 클래스는 다음의 조건을 만족해야 한다.
    • Roomdatabase를 상속(extends)받는 클래스는 추상 클래스이어야 한다
    • 주석 내에 DB와 연결된 Entity 목록과 버전을 포함(entities=[entity1::class, entity2::class], version=1)
    • 인수가 0개인 추상메서드를 포함하고, return은 @Dao로 주석처리된 DAO인스턴스를 반환 (abstract fun dao() : Dao)
  • 런타임 시 Room.databaseBuilder() 또는 Room.inMemoryDatabaseBuilder()로 Database 인스터스를 얻음

 

2. Entity

  • 데이터베이스 내에서 테이블 구조를 표현하는 @Entity 애노테이션이 설정된 Java/Kotlin 클래스
  • 각 Entity에 대한 항목(Itme)을 보관하기 위해 연결된 Database 객체 내에 테이블이 생성된다
  • 데이터베이스 클래스의 @Database 애노테이션에 설정된 Entity Array를 통해 Entity 클래스 참조
@Entity
data class UserEntity(
    val uid: Long,
    var name: String?,
    var age: Int?
)


@Entity

  • 테이블에 설정하는 애노테이션으로 Column들의 설정값을 한번에 정의할 수 있고,
    테이블에 대한 설정또한 정의할 수 있는 애노테이션
속성 설명
tableName() DB내 테이블 이름 설정, 생략할 경우 클래스이름이 Dafault값으로 설정
indices() DB 쿼리 속도를 높이기 위한 Column들을 Index로 지정할 수 있도록 설정
inheritSuperIndices() true로 설정할 경우 부모클래스에 선언된 모든 인덱스가 현재의 Entity클래스로 옮겨집니다.
primaryKeys() 기본키로 지정하고 싶은 칼럼 값들을 한번에 설정
foreignKeys() 외래키로 지정하고 싶은 칼럼 값들을 한번에 설정
ignoredColumns() DB에 생성되기를 원하지 않는 컬럼을 한번에 설정
Tips) SQLite의 테이블 이름은 대소문자 구분이 없습니다

 

@Entity(
    tableName = "user",		// 테이블명 user로 설정
    indices = arrayOf(Index(value = ["user_name", "user_age"])),
    inheritSuperIndices = true,
    primaryKeys = arrayOf("uid"),
    foreignKeys = arrayOf(
        ForeignKey (
             entity = BookEntity::class
             parentColumns = arrayOf("uId"),
             childColumns = arrayOf("userForeignKey"),
             onDelete = ForeignKey.CASCADE
        )
    ),
    ignoredColumns = arrayOf("image")
)
data class UserEntity(
    var uid: Long,
    var userForeignKey: Int = 0 ,		// 외래키 설정 Field (childColumn)
    @ColumnInfo(name = "user_name") var name: String?,
    @ColumnInfo(name = "user_age") var age: Int?,
    var image: Bitmap? = null		// IgnoredColumns 설정 (DB에 필드 생성X)
)
  • 위 방법처럼 선언해서 사용할 수 있지만, 복잡해 보이므로 선호하지 않습니다
  • 아래 처럼 필드마다 따로따로 설정하는 방법이 더 직관적으로 선호합니다
@Entity(tableName = "user", inheritSuperIndices = true)
data class UserEntity(
    @PrimaryKey var uid: Long,
    @ForeignKey(
        entity = BookEntity::class,		// 외래키 연결대상 Entity 클래스
        parentColmns = ["userId"],		// 외래키 연결대상 Entity 필드명
        childColumns = ["userForeignKey"],
        onDelete = ForeignKey.CASCADE	// 삭제될 경우 같이 삭제 설정
    ) var userForeignKey: Int = 0, 		// 외래키 설정 Field (childColumn)
    @ColumnInfo(name = "user_name", index = true) var name: String?,
    @ColumnInfo(name = "user_age", index = true) var age: Int?,
    @Ignore var image: Bitmap? = null		// IgnoredColumns 설정 (DB에 필드 생성X)
)

 

@PrimaryKey

  • Room Entity는 반드시 1개 이상PrimaryKey를 가지고 있어야 합니다

    Field가 1개뿐일 경우 그 1개의 Field를 PrimaryKey로 설정
  • PrimaryKey가 하나일 경우 위 코드처럼 필드에 @PrimaryKey 애노테이션을 사용
  • PrimaryKey가 복합일(다수) 경우는 @Entity 애노테이션에 primaryKets 속성을 사용할 수 있습니다
속성 설명
autoGenerate true로 설정할 경우 유니크한 ID값을 자동으로 생성

 

// Entity PrimaryKey가 1개일 경우
@Entity
data class UserEntity(
    @PrimaryKey val uid: Long,
    var name: String?,
    var age: Int?
)


// Entity PrimaryKey가 n개일 경우
@Entity(primaryKeys = ["uid", "name"])
data class UserEntity(
    val uid: Long,
    var name: String?,
    var age: Int?
)

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// autoGenerate 설정 (PK를 유니크한 아이디값을 자동으로 생성)
@Entity
data class UserEntity(
    @PrimaryKey(autoGenerate = true) val uid: Long,
    var name: String?,
    var age: Int?
)

 

@ForeignKey

  • 객체(테이블 행)간의 관계를 정의할 때 사용하며 여러 제약조건을 설정할 수 있습니다
  • 예를 들어, 부모객체가 삭제될 때 참조하던 자식객체를 모두 삭제하는 등의 설정이 가능합니다
속성 설명
entity 외래키가 참조할 부모 Entity를 의미 (ParentEntity::class)
parentColumns 참조할 부모 Entity의 Key값들
childColumns 참조한 부모의 Key값을 저장할 현재 Entity의 값들 (ParentColumns 개수와 동일하게 설정)
onDelete

참조하는 부모의 Entity가 삭제될 때 이뤄질 행위

 1. NO_ACTION(default) : 부모 Entity가 삭제 or 변경에도 아무런 행위를 하지 않음

 2. RESTRICT : 참조하는 부모의 Key값을 삭제 or 변경할 수 없게 설정

 3. SET_NULL : 참조하는 부모의 Key값이 삭제 or 변경되면 해당 외래키(FK)를 NULL 초기화

 4. SET_DEFAULT : SET_NULL과 매우 유사, 참조 Key값 삭제 or 변경 시 기본값으로 변경

 5. CASCADE(삭제시) : 부모 Entity가 삭제될 경우 자식 Entity를 삭제

 6. CASCADE(업데이트시) : 부모Entity 업데이트 될 경우 자식 Entity의 FK는 새로운 값 변경

onUpdate 참조하는 부모의 Entity가 Update될 때 이뤄질 행위 (onDelete와 같은 속성)
deferred true 설정할 경우, 트랜잭션이 완료될 때까지 외래키의 제약조건을 연기할 수 있습니다
// BookEntity.kt - 자식 객체 (Child Entity)
@Entity(
    tableName = "book",
    foreignKeys = arrayOf(
        ForeignKey(
            entity = UserEntity::class,
            parentColumns = arrayOf("uid"),
            childColumns = arrayOf("user_id"),
            onDelete = ForeignKey.CASCADE
        )
    )
)
data class BookEntity(
    @PrimaryKey var bookId: Int,
    @ColumnInfo(name = "title") var title: String?,
    @ColumnInfo(name = "user_id") var userId: Int?
)

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// UserEntity.kt - 부모 객체 (Parent Entity)
@Entity(
    tableName = "user",
    indices = arrayOf(
        Index(
            value = ["name_name", "user_age"],
            unique = true
        )
    )
)
data class UserEntity(
    @PrimaryKey(autoGenerate = true) val uid: Long,
    @ColumnInfo(name = "user_name") var name: String?,
    @ColumnInfo(name = "useR_age") var age: Int?
)
  • 자식객체(BookEntity)의 FK(외래키) 설정에 onDelete = CASCADE 옵션을 설정했기 때문에 부모객체(UserEntity)에 연결된 ParentColumn이 삭제될 경우 해당 User를 참조하던 Book 값도 모두 제거됩니다

    ex) 도서관 회원이 탈퇴할 경우, 회원이 빌린 책들을 모두 제거한다고 생각하면 이해가 쉽습니다

 

@ColumnInfo

  • Entity의 Field값에 컬럼 속성을 변경할 수 있게하는 애노테이션
속성 설명
name DB 테이블내 Column이름을 지정하는 속성, 생략 시 Default로 Entity 필드명으로 설정
typeAffinity Column의 Type을 지정할 수 있습니다
index Column들을 Index 설정(인덱싱)
collate Column값들의 정렬과 정렬 캐스트 작업을 정의
// ColumInfo 속성값 설정 예시
@ColumnInfo(
        name = "user_id",
        typeAffinity = ColumnInfo.TEXT,		// 문자열 Type으로 저장
        index = true,
        collate = ColumnInfo.UNSPECIFIED	// 특별한 설정없이 Binary처럼 동작
    ) var : Int
  • typeAffinity 옵션 5가지
옵션 설명
UNDEFINED(default) Type을 지정하지 않으며, 입력되는 값에 따라 자동으로 저장
TEXT String(문자열) 값으로 저장
INTEGER Int 값으로 저장
REAL Double / Float 값으로 저장
BLOB Binary 값으로 저장

 

  • collate 옵션 6가지
옵션 설명
UNSPECIFIED(dafault) 특별히 설정하지 않으며, Binary처럼 동작
BINARY 대소문자 구분하여 정렬
NOCASE 대소문자 구분X 정렬
RTRIM 앞뒤 공백을 제거하고 대소문자 구분하여 정렬
LOCALIZED 시스템의 현재 지역을 기반으로 정렬
UNICODE 유니코드 데이터 정렬 알고리즘을 이용하여 정렬

 

 

3. DAO (Data Access Object)

  • Room에서는 SQL을 이용한 직접적인 쿼리 접근 방식이 아닌 DAO를 이용해서 DB에 접근해야합니다.
  • 즉, DAO는 데이터베이스에 접근할 수 있는 메소드를 포함하며, 데이터 접근 객체
  • DAO는 Interface(인터페이스) / abstract class(추상 클래스)로 구현
  • 기존 SQLite Query의 큰 문제는 컴파일 시 쿼리상의 오류를 발견하지 못한다는 점이였는데,
    Room DAO는 작성된 쿼리의 오류를 컴파일 시점에 발견하여 조기에 오류를 수정할 수 있다는 점이 큰 장점
  • Entity 자동완성을 지원하여 Query 작성시에 오타 확률을 크게 줄여줌
  • Room을 사용해 App의 데이터에 접근하려면 DAO를 사용

 

@Insert

  • DAO 메서드에 @Insert 애노테이션 지정 시, Room은 단일 트랜잭션의 데이터베이스에 모든 매개변수를 삽입하는 구현을 생성
  • 여러 매개변수타입을 가질 수 있으므로, 단일 객체 / 배열 형태의 데이터까지 모두 입력이 가능

    + 입력 시 옵션을 설정하여 충돌이 발생할 경우 처리옵션도 설정 가능
    @Insert
    fun insert(bookEntity: BookEntity)	// 단일 객체

    @Insert
    fun insert(bookEntity01: BookEntity, bookEntity02: BookEntity) // 2개의 객체

    @Insert
    fun insert(bookEntity: List<BookEntity>)	// n개의 객체 (List)

    @Insert(onConflict = OnConflictStrategy.REPLACE)	// 값 중복시 덮어쓰기 REPLACE
    fun insert(vararg userEntity: BookEntity)	// n개의 객체 (Array배열) 
  • onConflict 옵션 (@Update 애노테이션에도 동일한 옵션 적용가능)
옵션 설명
ABORT(default) 충돌 발생 시 트랜잭션을 롤백
REPLACE 충돌 발생 시 기존 Data에 입력 Data를 덮어쓰기
IGNORE 충돌 발생 시 기존 Data를 유지, 입력 Data 무시(버리기)

 

@Update

  • 데이터를 갱신할 때 사용하는 애노테이션
  • 전달받은 매개변수의 PK값에 매칭되는 Entity를 찾아 값을 갱신하는 방식
    @Update
    fun update(bookEntity: BookEntity)	// 단일 객체

    @Update
    fun update(userEntity01: BookEntity, userEntity02: BookEntity)	// 2개의 객체

    @Update(onConflict = OnConflictStrategy.REPLACE)	// 충돌 시 덮어쓰기
    fun update(vararg bookEntity: BookEntity)	// n개의 객체 (배열 형태)

    @Update
    fun update(bookEntity: List<BookEntity>)	// n개의 객체 (List 형태)

 

@Delete

  • 데이터를 삭제할 때 사용하는 애노테이션
  • Inserte / Update / Delete 모두 비슷한 파라미터를 받아서 실행합니다
  • 전달받은 매개변수의 PK값에 매칭되는 Entity를 찾아 삭제합니다
    @Delete
    fun delete(uookEntity: BookEntity)

    @Delete
    fun delete(userEntity01: BookEntity, userEntity02: BookEntity)

    @Delete
    fun delete(vararg uookEntity: BookEntity)

    @Delete
    fun delete(uookEntity: List<BookEntity>)

 

@Query

  • 주로 데이터를 선택할 때 사용하는 애노테이션
  • Query는 DAO 클래스 중 가장 핵심 부분으로 데이터를 읽고 쓸 수 있게 합니다
  • 컴파일 시점에 Query 검사가 이루어 지기 때문에 런타임 오류를 최소화 할 수 있는 장점
  • Query 유형은 크게 Simple Query / Observable Query 2가지로 나누어 집니다
//Simple Query

@Query("SELECT * FROM user")		// user테이블내의 모든 값을 검색
fun getAll(): List<UserEntity>		// List형태로 반환

@Query("SELECT * FROM user where user_name = :name")	// user_name 칼럽값이 입력값 name과 일치하는 값 검색
fun getWithName(name: String): List<UserEntity>			// name이 같은 값이 여러개 일수도 있어 List형 반환

@Query("SELECT * FROM user where user_name in (:names)")	// user_name 칼럼에 입력값 names중에 포함된 값 검색
fun getWithNames(names: ArrayList<String>): List<UserEntity>	// List형 반환

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

//Observable Query

@Query("SELECT * FROM user")	// user 테이블 내 모든 값 검색
fun getAllObservable(): LiveData<List<UserEntity>>	// LiveData로 Wrapping된 List형태 반환

@Query("SELECT * FROM user where user_name = :name")	// user_name 컬럼값과 입력값이 일치하는 값 검색
fun getWithNameObservable(name: String): LiveData<List<UserEntity>>	// LiveData로 Wrapping된 List형태 반환

@Query("SELECT * FROM user where user_name in (:names)")
fun getWithNamesObservable(names: ArrayList<String>): LiveData<List<UserEntity>>


위 예시코드를 보시면 Simpe QueryObservable Query를 작성하였습니다

Simple Query는 List형태의 return 값을 받으며, 변화를 감지할 수 없습니다

Observable Query는 LiveData로 Wrapping된 형태의 return 값을 받으며, 데이터가 변화할 경우 변화를 감지하여 데이터를 재갱신합니다

 

Room 구성 요소

 

 

 

Dependency 추가

build.gradle(Module: app)에 Room Dependency를 추가

[Room 최신버전 보러가기]

2020.05.02 기준 최신버전 - "2.2.3" 

// Kotlin일 경우 kapt 추가
appliy plugin: 'kotlin-kapt'

dependencies {
    def room_version = "2.2.3"
    implementation "android.arch.persistence.room:runtime:$room_version"

    // Kotlin 기반일 경우 annotationProcessor 대신 kapt를 사용
    annotationProcessor "android.arch.persistence.room:compiler:$room_version"
}


Java만 사용하는 경우 생략해도 되지만, Kotlin일 경우엔 아래 두 가지를 지켜야 합니다
 1. appliy plugin: 'kotlin-kapt' 플러그인 추가
 2. annotationProcessor를 kapt로 변경

추가로, RxJava / Guava를 사용한다면 추가로 dependency를 추가해야 합니다

/* optional - RxJava support for Room */
implementation "android.arch.persistence.room:rxjava2:$room_version"

/* optional - Guava support for Room, including Optional and ListenableFuture */
implementation "android.arch.persistence.room:guava:$room_version"