[Android] room

안드로이드 DB

안드로이드 테이블 생성 과정

User라는 테이블을 만든다고 가정한다.

User 엔티티 만들기

@Entity
class User {
  @PrimaryKey val userId: Long ,
  val userName: String
}

// 또는 이런 식으로 키를 자동생성할 수 있다.
@Entity
class User {
  @PrimaryKey(autoGenerate = true)
  @ColumnInfo(name = "user_id")
  val userId: Long = 0,
  val userName: String
}

UserDao 만들기

대략 이런 식으로 만들 수 있다.

abstract class UserDao: EntityDao<User>() {
  @Insert
  abstract suspend fun insert(entity: User): Long

  @Insert
  abstract suspend fun insertAll(entities: List<User>): Long

  @Update
  abstract suspend fun update(entity: User)

  @Delete
  abstract suspend fun deleteEntity(entity:E): Int

  @Transaction
  open suspend fun withTransaction(tx: suspend () -> Unit) = tx()

  suspend fun insertOrUpdate(entity: E): Long {
    return if (entity.id == 0L) {
        insert(entity)
    } else {
        update(entity)
        entity.id
    }
  }

  @Transaction
  open suspend fun insertOrUpdate(entities: List<E>) {
      entities.forEach {
          insertOrUpdate(it)
      }
  }
}

DB 인터페이스 정의

interface AppDb {
  fun userDao(): UserDao
}

Room DB 정의

@Database(
    version = 1,
    entities = [ User::class ]
)
@TypeConverters(AppDbTypeConverters::class)
abstract class AppRoomDb : RoomDatabase(), AppDb

TypeConverters 정의

object AppDbTypeConverters {
    private val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME

    @TypeConverter
    @JvmStatic
    fun toOffsetDateTime(value: String?) = value?.let { formatter.parse(value, OffsetDateTime::from) }


    @TypeConverter
    @JvmStatic
    fun fromOffsetDateTime(date: OffsetDateTime?): String? = date?.format(formatter)


    @TypeConverter
    @JvmStatic
    fun fromUserPlaceType(value: UserPlaceType?) = value?.name

    @TypeConverter
    @JvmStatic
    fun toUserPlaceType(value: String?) = UserPlaceType.values.firstOrNull { it.name == value }

    // ...
}


// 위의 타입컨버터는 UserPlaceType을 문자열로 변환하고 있다
enum class UserPlaceType(val desc: String) {
    HOME("집"),
    OFFICE("회사"),
    ETC("기타");

    companion object {
        val values by lazy { values() }
    }
}

DB의 TransactionRunner 정의

dao의 메소드들을 호출하는 쪽에서 트랜잭션 단위의 실행을 정의하기 위해 TransactionRunner를 정의한다.

interface DbTransactionRunner {
    suspend operator fun <T> invoke(block: suspend () -> T): T
}

위의 인터페이스를 Room DB에서 다음과 같이 구현한다.

class AppRoomTransactionRunner @Inject constructor(
    private val db: AppRoomDb
) : DbTransactionRunner {
    override suspend operator fun <T> invoke(block: suspend () -> T): T {
        return db.withTransaction {
            block()
        }
    }
}

중간 정리

이제 Dagger에서 이들을 잘 엮어줘야 할 차례다.

Dagger

크게 세가지 파트다.

AppRoomDb의 인스턴스를 컴포넌트로 만들기
@Module
class RoomDbModule {
    @Singleton
    @Provides
    fun provideDatabase(context: Context): AppRoomDb {
        val builder = Room.databaseBuilder(context, AppRoomDb::class.java, "app.db")
            //.addMigrations(*AppRoomDb_Migxxxxx.build())
            .fallbackToDestructiveMigration()
        if (Debug.isDebuggerConnected()) {
            builder.allowMainThreadQueries()
        }
        return builder.build()
    }
}

AppRoomDb의 각 Dao를 Dagger 컴포넌트로 만들기

@Module
class DbDaoModule {
    @Provides
    fun provideUserDao(db: AppDb) = db.userDao()

    @Provides
    fun provideXXXDao(db: AppDb) = db.xxxDao()

    // ...
}

인터페이스와 인스턴스를 바인드
@Module
abstract class DbModuleBinds {
    @Binds
    abstract fun bindAppDb(db: AppRoomDb): AppDb

    @Singleton
    @Binds
    abstract fun provideDbTransactionRunner(runner: AppRoomTransactionRunner): DbTransactionRunner
}

모듈 include
@Module(
    includes = [
        RoomDbModule::class,
        DbModuleBinds::class,
        DbDaoModule::class
    ]
)
class DbModule

이제 DaggerComponent에서 아래와 같이 include하면 된다.

@Singleton
@Component(
    modules = [
        DbModule::class,
        // ...
        RetrofitModule::class
    ]
)
interface AppComponent {
    @Component.Factory
    interface Factory {
        fun create(
            @BindsInstance application: Application,
            @BindsInstance applicationContext: Context
        ): AppComponent
    }
    // ....
}

여기까지 진행하면, DB 컴포넌트들을 필요한 곳에 Injection해서 사용할 수 있다. 끝.

안드로이드 Room DB의 쿼리

Room을 사용하면서 대단하다고 느낀 점


안드로이드 Room을 사용하면서

Room, LiveData, Flow를 함께 사용한다.

Room에서 데이터를 Observe하는 방식은 매우 훌륭하다. 리사이클러뷰에서 주의해야 할 점이 있다.

애플리케이션의 라이프 사이클