책 검색결과 저장을 위한 Room DB 구현해보기
책 검색 결과를 통해 마음에 드는 책이 있으면 Room 을 통해 SQLite 에 저장하고 불러오는 기능을 구현해보겟습니다.
- Room
-> Room 라이브러리는 엔티티(Entity) , 데이터 접근 객체(DAO) , 데이터베이스(DB) 3가지 구성요소가 있습니다.
출처: Room을 사용하여 로컬 데이터베이스에 데이터 저장 | Android 개발자 | Android Developers
Room을 사용하여 로컬 데이터베이스에 데이터 저장 | Android 개발자 | Android Developers
Room 라이브러리를 사용하여 더 쉽게 데이터를 유지하는 방법 알아보기
developer.android.com
- build.gradle (앱 모듈)
//Room
implementation 'androidx.room:room-runtime:2.5.0'
kapt 'androidx.room:room-compiler:2.5.0'
implementation 'androidx.room:room-ktx:2.5.0'
코틀린에서는 어노테이션 처리기를 위해 kapt를 추가해준다.
plugins {
id 'kotlin-kapt'
- Entity : DB내의 테이블, 즉 DB에 저장할 데이터 형식으로 class의 변수들이 컬럼이 되어 테이블을 구성한다
- Annotation
@Entity(tableName = " ")
테이블 이름을 선언한다.( 기본적으로 entity class 이름을 database table 이름으로 인식한다.)
@Primarykey
각 Entity는 1개의 Primary key를 가져야 한다. 일반적으로 고유한 id 값으로 설정한다.
@ColumInfo
table 내 column을 변수 와 매칭한다.
package com.example.kakaobook.ui.view.data.model
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonClass(generateAdapter = true)
@Entity
data class Book(
@Json(name = "authors")
val authors: List<String>,
@Json(name = "contents")
val contents: String,
@Json(name = "datetime")
val datetime: String,
@PrimaryKey(autoGenerate = false)
@Json(name = "isbn")
val isbn: String,
@Json(name = "price")
val price: Int,
@Json(name = "publisher")
val publisher: String,
@ColumnInfo(name = "sale_price")
@Json(name = "sale_price")
val salePrice: Int,
@Json(name = "status")
val status: String,
@Json(name = "thumbnail")
val thumbnail: String,
@Json(name = "title")
val title: String,
@Json(name = "translators")
val translators: List<String>,
@Json(name = "url")
val url: String
) : Parcelable
기존에 카카오 책 검색에서 가져오던 Book 클래스에 @Entity를 통해 데이터베이스에 사용할 엔티티로 만들어준다. 또한 위에서 설명한 대로 데이터의 고유값인 isbn에 @PrimaryKey(autoGenerate = false)를 통해 PrimaryKey로 만들어주고 salePrice 즉 카멜 케이스 형태로 저장된 책 가격을 @ColumnInfo 어노테이션을 통해 스네이크 케이스로 변환해준다.
- DAO: 데이터베이스에 접근하여 수행할 작업을 메서드 형태로 정의하는 부분(SQL 쿼리도 지정 가능)
@Insert
"onConflict = OnConflictStrategy.REPLACE" 로 만일 동일한 PrimaryKey가 있을 경우 덮어쓸 수 있다.
@Update
Entity 업데이트. Return 값으로 업데이트된 행 수를 받을 수 있다.
@Delete
Entity 삭제. Return 값으로 삭제된 행 수를 받을 수 있다.
@Query
@Query를 사용하여 DB를 조회할 수 있다.
package com.example.kakaobook.ui.view.data.db
import androidx.lifecycle.LiveData
import androidx.room.*
import com.example.kakaobook.ui.view.data.model.Book
@Dao
interface BookSearchDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertBook(book: Book)
@Delete
suspend fun deleteBook(book: Book)
@Query("SELECT * FROM books")
fun getFavoriteBooks(): LiveData<List<Book>>
}
- 데이터를 추가할 때는 기본적으로 @Insert 어노테이션을 사용하고 onConflict = OnConflictStrategy.REPLACE를 통해 동일한 isbn을 가지고 있는 아이템이 존재한다면 덮어쓰도록 구현한다.
- 데이터를 삭제할 때는 @Delete를 통해 간단하게 구현 가능하다.
- 데이터를 조회할 때는 @Query를 써서 sql 쿼리문을 작성하고 위 코드에서는 "SELECT * FROM books"를 써서 books 테이블에 있는 모든 데이터를 가져오도록 했다. 이때 반환받는 데이터를 LiveData로 하엿습니다
- Read 기능을 구현하는 @Query를 제외한 Create, Update, Delete 작업은 시간이 걸리는 작업이기 때문에 코루틴 안에서 비동기적으로 수행하기로 했다. 따라서 suspend 키워드를 함수 앞에 추가해준다.
다음은 DAO와 엔티티(Entity)의 동작을 수행해주는 데이터베이스 클래스를 만들어준다.
- 데이터베이스 : 데이터베이스의 생성 및 버전 관리를 하는 클래스이다. Room DB에서 DAO를 가져와 객체를 통해 데이터를 CRUD 한다.
@Database
Class가 Database임을 알려주는 어노테이션.
- entities
: 이 DB에 어떤 테이블들이 있는지 명시한다. - version
: Scheme가 바뀔 때 이 version도 바뀌어야 한다. - exportSchema
: Room의 Schema 구조를 폴더로 Export 할 수 있다.
package com.example.kakaobook.ui.view.data.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.example.kakaobook.ui.view.data.model.Book
@Database(
entities = [Book::class],
version = 1,
exportSchema = false
)
abstract class BookSearchDatabase : RoomDatabase() {
abstract fun bookSearchDao(): BookSearchDAO
companion object {
@Volatile
private var INSTANCE: BookSearchDatabase? = null
private fun buildDataBase(context: Context): BookSearchDatabase =
Room.databaseBuilder(
context.applicationContext,
BookSearchDatabase::class.java, "favorite-books"
).build()
fun getInstance(context: Context): BookSearchDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDataBase(context).also { INSTANCE = it }
}
}
}
- 클래스에는 데이터베이스와 연결된 데이터 항목을 모두 나열하는 entities 배열이 포함된 @Database 주석이 달려야 합니다.
- 클래스는 RoomDatabase 를 확장하는 추상 클래스여야 합니다.
- 데이터베이스 클래스는 DAO 클래스의 인스턴스를 반환하는 추상 메서드를 정의해야 합니다.
- 데이터베이스의 여러 인스턴스가 동시에 열리는 것을 막기 위해 BookSearchDatabase 를 싱글톤 으로 정의했습니다
- TypeConverter
- 우리가 Entity로 사용할 Book 클래스에는 authors라는 변수가 있다. 이 authors는 List<String>타입이다.
- Room은 기본 유형과 박싱된 유형 간 변환을 위한 기능을 제공하지만 항목 간 객체 참조는 허용하지 않습니다. 본 문서에서는 유형 변환기를 사용하는 방법 및 Room이 객체 참조를 지원하지 않는 이유를 설명합니다.
- 출처: Room을 사용하여 복잡한 데이터 참조 | Android 개발자 | Android Developers
Room을 사용하여 복잡한 데이터 참조 | Android 개발자 | Android Developers
Room을 사용하여 복잡한 데이터 참조 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Room은 기본 유형과 박싱된 유형 간 변환을 위한 기능을 제공하지만 항목
developer.android.com
-> 따라서 우리는 Type Converter를 사용하여 authors의 List<String>을 일반 String 타입으로 변환하여 저장할 것이다.
이를 위해 데이터 직렬화를 해야 하는데 간단하게 사용할 수 있는 Kotlinx Serilaization 을 이용해보겟습니다.
- build.gradle(project)
plugins {
...
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10' apply false
}
- build.gradle(app module)
dependencies {
// Kotlin serialization
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3'
}
- List<String>과 String을 상호 변환하는 @TypeConverter를 추가합니다.
package com.example.kakaobook.ui.view.data.db
import androidx.room.TypeConverter
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class OrmConverter {
@TypeConverter
fun fromList(value: List<String>) = Json.encodeToString(value)
@TypeConverter
fun toList (value: String) = Json.decodeFromString<List<String>>(value)
}
List<String>이 들어오면 encodeToString을 통해 String으로 encode 해주고 String을 받으면 List<String>으로 decodeFromString 해주는 형식이다.
- 데이터베이스에 @TypeConverters(OrmConverter::class)를 추가해주면 컨버터가 필요한 상황에 알아서 TypeConverter를 사용하게 된다.
package com.example.kakaobook.ui.view.data.db
import android.content.Context
import androidx.room.*
import com.example.kakaobook.ui.view.data.model.Book
@Database(
entities = [Book::class],
version = 1,
exportSchema = false
)
@TypeConverters(OrmConverter::class)
abstract class BookSearchDatabase : RoomDatabase() {
abstract fun bookSearchDao(): BookSearchDAO
companion object {
@Volatile
private var INSTANCE: BookSearchDatabase? = null
private fun buildDataBase(context: Context): BookSearchDatabase =
Room.databaseBuilder(
context.applicationContext,
BookSearchDatabase::class.java, "favorite-books"
).build()
fun getInstance(context: Context): BookSearchDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDataBase(context).also { INSTANCE = it }
}
}
}