본문 바로가기

Android

ROOM

1. 정의

- SQLite를 쉽게 사용할 수 있는 데이터 베이스 객체 매핑 라이브러리
- 쉽게 Query를 사용할 수 있는 API 제공
- Query 결과를 LiveData로 해 데이터베이스 변경될 때마다 쉽게 UI 변경

- SQLite 보다 룸 권장

 


2. 주요 3요소


1) @Database

- 클래스를 데이터베이스로 지정하는 annotation, RoomDatabase를 상속받은 클래스
- Room.databaseBuilder 사용해 인스턴스 생성

 


2) @Entity 


- 클래스를 테이블 스키마로 지정하는 annotation

 


3) @Dao 


- 클래스를 DAO로 지정하는 annotation

  • 기본적인 insert, delete, update SQL 자동
  • 복잡한 SQL 직접

 

💡 annotation

: 코드에 메타데이터(일반적으로 데이터에 관한 구조화된 데이터)를 추가하는 방법 중 하나

: 컴파일러와 런타임에 정보를 전달하거나 처리

// 2개의 argument 가짐, 메타데이터 제공
// 이 어노테이션에 전달된 인자들은 클래스 또는 어노테이션 프로세서에서 활용
@MyAnnotationWithArguments(argument1 = "value1", argument2 = 42)
class MyClass {
    // 클래스 내용
}




3. gradle 파일 설정


- 'kotlin-kapt' 플러그인이 추가
- dependencies 추가

plugins {
....
    id 'kotlin-kapt'
}
.....

dependencies {

    ......

    def room_version = "2.5.1"
    implementation "androidx.room:room-runtime:$room_version"
    annotationProcessor "androidx.room:room-compiler:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    // optional - Kotlin Extensions and Coroutines support for Room
    implementation "androidx.room:room-ktx:$room_version"
    // optional - Test helpers
    testImplementation "androidx.room:room-testing:$room_version"
}



1) Entity 생성

- Entity : 테이블 스키마 정의
- CREATE TABLE student_table (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);

@Entity(tableName = "student_table")    // 테이블 이름을 student_table로 지정함
data class Student (
    @PrimaryKey 
@ColumnInfo(name = "student_id") 
    val id: Int,
    val name: String
)



2) DAO 생성

- interface나 abstract class로 정의
- annotation에 SQL 쿼리 정의, 그 쿼리를 위한 메소드 선언

@Query("SELECT * from table") fun getAllData() : List<Data>


- @Insert, @Update, @Delete는 SQL 쿼리를 작성하지 않아도 컴파일러가 자동으로 생성
- @Insert, @Update는 key가 중복되는 경우 onConflict 지정 가능
    - OnConflictStrategy.ABORT: key 충돌 시 종료
    - OnConflictStrategy.IGNORE: key 충돌 무시
    - OnConflictStrategy.REPLACE: key 충돌 시 새로운 데이터로 변경
    


- Query로 리턴 되는 데이터 타입을 LiveData<> : 데이터가 업데이트 될 때 Observer 가능

@Query("SELECT * from table") fun getAllData() : LiveData<List<Data>>


- Query에 SQL 정의할 때 메소드의 인자 사용 가능

//:sname 가능
//suspend : coroutine 사용, 이 메소드 호출할 땐 runBlocking{} 내에서 호출
//LiveData : coroutine 사용 X (비동기적 동작)
@Query("SELECT * FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List<Student>
@Dao
interface MyDAO {
    @Insert(onConflict = OnConflictStrategy.REPLACE)  // INSERT, key 충돌이 나면 새 데이터로 교체
    suspend fun insertStudent(student: Student)

    @Query("SELECT * FROM student_table")
    fun getAllStudents(): LiveData<List<Student>>        // LiveData<> 사용

    @Query("SELECT * FROM student_table WHERE name = :sname")   
    suspend fun getStudentByName(sname: String): List<Student>

    @Delete
    suspend fun deleteStudent(student: Student); // primary key is used to find the student

    // ...
}

 



4. Database 생성


- RoomBatabase를 상속하여 Room 클래스 생성
- 포함된 Entity들과 데이터베이스 버전을 @Database annotation에 지정함

  • 버전이 기존에 저장되어 있는 데이터베이스 버전보다 높으면 open 할 때 높은 버전으로 이동


- DAO를 가져올 수 잇는 getter 메소드 만듦

  • 실제 메소드 정의 자동으로 생성


- Room 클래스

  • 인스턴스 : 하나만 있으면 되므로 Singleton 패턴 사용
  • 객체 생성 : Room.databaseBuidler()
// annotation : 포함된 entity, 데이터 버전
@Database(entities = [Student::class, ClassInfo::class, Enrollment::class, Teacher::class], version = 1)
//RoomDatabase 상속
abstract class MyDatabase : RoomDatabase() {
    abstract fun getMyDao() : MyDAO

    companion object {
        private var INSTANCE: MyDatabase? = null
        private val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) { 생략 }
        }

        private val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) { 생략 }
        }
        fun getDatabase(context: Context) : MyDatabase {
            if (INSTANCE == null) {
                INSTANCE = Room.databaseBuilder(
                    context, MyDatabase::class.java, "school_database")
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                    .build()
            }
            return INSTANCE as MyDatabase
        }
    }
}



5. Migration


- MyRoomDatabase 객체 생성 후 addMigrations() 메소드를 호출하여 Migration 방법 지정

Room.databaseBuilder(...).addMigrations(MIGRATION_1_2, MIGRATION_2_3)

//버전 1 -> 2
private val MIGRATION_1_2 = object : Migration(1, 2) {   // version 1 -> 2
    override fun migrate(database: SupportSQLiteDatabase) {
        //테이블 변경 : Column 하나 추가
database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
    }
}

private val MIGRATION_2_3 = object : Migration(2, 3) {   // version 2 -> 3
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE class_table ADD COLUMN last_update INTEGER")
    }
}

 


6. UI와 연결


- RoomDatabase객체에서 DAO 객체를 받아오고, 이 DAO객체의 메소드를 호출하여 데이터베이스를 접근함

myDao = MyDatabase.getDatabase(this).getMyDao()
runBlocking { // (주의) UI를 블록할 수 있는 DAO 메소드를 UI 스레드에서 바로 호출하면 안됨
    myDao.insertStudent(Student(1, "james"))  // suspend 지정되어 있음
}
val allStudents = myDao.getAllStudents() // LiveData는 Observer를 통해 비동기적으로 데이터를 가져옴



7. UI와 연결 - Live Data

- LiveDAta<> 타입으로 리턴되는 DAO 메소드 경우
    - observe() 메소드를 이용해 Observer를 지정
    - 데이터가 변경될 때마다 자동을 Observer의 onChanged() 호출

val allStudents = myDao.getAllStudents()
allStudents.observe(this) {   // Observer::onChanged() 는 SAM 이기 때문에 lambda로 대체
    val str = StringBuilder().apply {
            for ((id, name) in it) {
                append(id)
                append("-")
                append(name)
                append("\n")
            }
        }.toString()
    binding.textStudentList.text = str
}



8. 예제

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="5dp"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edit_student_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="ID"
        android:inputType="number"
        app:layout_constraintEnd_toStartOf="@+id/query_student"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintHorizontal_chainStyle="spread_inside"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/edit_student_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="student name"
        android:inputType="textPersonName"
        app:layout_constraintEnd_toStartOf="@+id/add_student"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintHorizontal_chainStyle="spread_inside"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/edit_student_id" />

    <Button
        android:id="@+id/add_student"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Add Student"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/edit_student_name"
        app:layout_constraintTop_toBottomOf="@+id/query_student" />

    <Button
        android:id="@+id/query_student"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Query Student"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/edit_student_id"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Result of Query Student"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/edit_student_name" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="Student List"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text_query_student" />

    <TextView
        android:id="@+id/text_query_student"
        android:layout_width="0dp"
        android:layout_height="100sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <TextView
        android:id="@+id/text_student_list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2" />

</androidx.constraintlayout.widget.ConstraintLayout>



package com.example.room

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.room.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.StringBuilder

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    lateinit var myDao: MyDAO

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        myDao = MyDatabase.getDatabase(this).getMyDao()

        val allStudent = myDao.getAllStudents()
        allStudent.observe(this){
            val str = StringBuilder().apply {
                for((id, name) in it){
                    append(id)
                    append("-")
                    append(name)
                    append("\n")
                }
            }.toString()

            binding.textStudentList.text = str
        }

        binding.addStudent.setOnClickListener {
            val id = binding.editStudentId.text.toString().toInt()
            val name = binding.editStudentName.text.toString()
            if(id > 0 && name.isNotBlank()){
                CoroutineScope(Dispatchers.IO).launch {
                    myDao.insertStudent(Student(id, name))
                }
            }
        }

        binding.queryStudent.setOnClickListener {

            val name = binding.editStudentName.text.toString()
            CoroutineScope(Dispatchers.IO).launch {
                val results = myDao.getStudentByName(name)

                if(results.isNotEmpty()){
                    val str = StringBuilder().apply {
                        results.forEach {
                            student -> append(student.id)
                            append("-")
                            append(student.name)
                        }
                    }
                    withContext(Dispatchers.Main){
                        binding.textQueryStudent.text = str
                    }
                }else{
                    withContext(Dispatchers.Main){
                        binding.textQueryStudent.text = ""
                    }
                }
            }

        }

    }
}

 

package com.example.room

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

@Database(entities = [Student::class], exportSchema = false, version = 1)
abstract class MyDatabase : RoomDatabase(){
    //데이터베이스와 관련된 작업을 수행하기 위한 DAO를 얻기 위한 메서드, 데이터베이스와 상호작용
    abstract fun getMyDao() : MyDAO

    companion object{
        //데이터베이스는 싱글톤 패턴을 따르므로 앱에서 하나의 인스턴스만 생성
        private var INSTANCE: MyDatabase? = null

        //데이터베이스 버전이 변경될 때 데이터베이스 스키마를 업데이트 하기 위해 마이그레이션 정의
        private val MIGRATION_1_2 = object: Migration(1,2){
            override fun migrate(database: SupportSQLiteDatabase) {
            }
        }

        private val MIGRATION_2_3 = object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) {
                //student_table 테이블에 last_update 열을 추가
                database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
            }
        }

        //데이터베이스 인스턴스 생성하고 반환
        fun getDatabase(context: Context) : MyDatabase{
            if(INSTANCE == null){
                // Room 라이브러리를 사용하여 데이터베이스를 빌드, 데이터베이스 생성하고 설정
                INSTANCE = Room.databaseBuilder(
                    //"school_database" : 데이터베이스 이름 지정
                    context, MyDatabase::class.java, "school_database")
                    //데이터베이스의 마이그레이션 추가 : 스키마가 변경될 때 이전 데이터 유지하면 스키마 업데이트
                    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                    //데이터베이스 실제로 생성 후 반환
                    .build()
            }
            //데이터베이스 반환
            return INSTANCE as MyDatabase
        }

    }

}

 

package com.example.room

import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

@Dao
interface MyDAO {

    //삽입, 충돌이 나면 새 데이터로 교체
    //suspend fun : 비동기적 실행
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertStudent(student: Student)

    //모든 학생 데이터 가져옴
    //LiveData : 비동기적 실행 (다른 작업과 독립적, 결과를 기다리지 않고 계속 진행)
    @Query("SELECT * FROM student_table")
    fun getAllStudents() : LiveData<List<Student>>

    //이름이 :sanme으로 받아온 이름과 같은 것만 가져오기
    @Query("SELECT * FROM student_table WHERE name = :sname")
    suspend fun getStudentByName(sname: String): List<Student>

    //데이터베이스에서 데이터를 삭제 (데이터베이스에서 해당 학생 데이터를 삭제, 단일 학생)
    @Delete
    suspend fun deleteStudnet(student: Student)

}

 

package com.example.room

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

//Student : 데이터베이스 테이블
//student_table : 테이블 이름
@Entity(tableName = "student_table")
data class Student(
    //id 기본키 지정
    @PrimaryKey
    //id 열 이름 지정 : student_id
    @ColumnInfo(name = "student_id")
    val id: Int,
    val name: String
)

'Android' 카테고리의 다른 글

Fragment 생명주기  (0) 2023.11.22
사용자 위치 얻기  (2) 2023.09.15
Parcelize  (0) 2023.08.30
어댑터뷰  (0) 2023.08.29
Dialog  (0) 2023.08.25