ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] 데이터 저장 - Room
    안드로이드 2024. 4. 30. 17:08

    ✏️ TIL(Today I Learned)

    Room 대해 알아보았다.

    Room은 안드로이드 플랫폼을 위한 객체 관계 매핑 (ORM) 라이브러리로, SQLite 데이터베이스를 보다 쉽게 사용할 수 있도록 도와준다.

    EdtiText에 입력된 ID와 이름을 Datebase에 ADD, QUERY, DELETE 할 수 있는 버튼을 만들었다.

     이름을 통해 QUERY 하고, ID를 통해 DELETE 되도록 했다.

     

    App Inspection에서 Database를 확인하면 잘 저장된 것을 볼 수 있다.

     

     

    Room을 사용하려면, gradle 파일 설정을 해야 한다.

    build.gradle(:app)에 아래 내용을 추가하면 된다.

    plugins {
    
        ...
        
        id ("kotlin-kapt")
    }
    
    dependencies {
    
    	...
    
        // Room dependencies
        val room_version = "2.6.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")
    }

     

     

    그다음, Entity를 만들어서 테이블 스키마 정의한다.

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

     

     

    그리고 DAO를 생성한다. Dao는 interface나 abstract class로 정의되어야 한다.

    Annotation에 SQL 쿼리를 정의하고 그 쿼리를 위한 메소드를 선언하면 된다. 가능한 annotation으로 @Insert, @Update, @Delete, @Query가 있다.

    @Query로 리턴되는 데이터의 타입을 LiveData<>로 하여, 나중에 이 데이터가 업데이트될 때 Observer를 통해 할 수 있게 했다.

    @Query에 SQL을 정의할 때, 메소드의 인자를 사용했다.

    @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") // 메소드 인자를 SQL문에서 :을 붙여 사용
        suspend fun getStudentByName(sname: String): List<Student>
    
        @Delete
        suspend fun deleteStudent(student: Student)
    }

     

     

    마지막으로, Database 생성한다.

    RoomDatabase를 상속하여 자신의 Room 클래스를 만들어야 한다. 

    포함되는 Entity들과 데이터베이스 버전(version)을 @Database annotation에 지정한다.

    DAO를 가져올 수 있는 getter 메소드도 만들어 준다. (실제 메소드 정의는 자동으로 생성된다.)

    Room 클래스의 인스턴스는 하나만 있으면 되므로 Singleton 패턴을 사용한다.

    그리고 Room 클래스의 객체 생성은 Room.databaseBuilder()를 이용한다.

    @Database(entities = [Student::class],
        exportSchema = false, version = 1)
    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) {
                }
            }
    
            fun getDatabase(context: Context) : MyDatabase {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(
                        context, MyDatabase::class.java, "school_database")
                        .addMigrations(MIGRATION_1_2)
                        .build()
                    // for in-memory database
                    /*INSTANCE = Room.inMemoryDatabaseBuilder(
                        context, MyDatabase::class.java
                    ).build()*/
                }
                return INSTANCE as MyDatabase
            }
        }
    }

     

     

    이 과정이 끝나면 MainActivity에서 RoomDatabase객체에서 DAO 객체를 받아오고,

    이 DAO객체의 메소드를 호출하여 데이터베이스에 접근할 수 있다.

    class MainActivity : AppCompatActivity() {
    
        private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
        private lateinit var myDao: MyDAO
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
    
            myDao = MyDatabase.getDatabase(this).getMyDao()
    
            observeStudents()
            setListeners()
        }
    
        private fun observeStudents() {
            myDao.getAllStudents().observe(this) { students ->
                val str = buildString {
                    students.forEach { student ->
                        append("${student.id}-${student.name}\n")
                    }
                }
                binding.textStudentList.text = str
            }
        }
    
        private fun setListeners() {
            with(binding) {
                addStudent.setOnClickListener {
                    val id = editStudentId.text.toString().toIntOrNull() ?: 0
                    val name = editStudentName.text.toString()
                    if (id > 0 && name.isNotEmpty()) {
                        CoroutineScope(Dispatchers.IO).launch {
                            myDao.insertStudent(Student(id, name))
                        }
                    }
                    editStudentId.text = null
                    editStudentName.text = null
                }
    
                queryStudent.setOnClickListener {
                    val name = editStudentName.text.toString()
                    CoroutineScope(Dispatchers.IO).launch {
                        val results = myDao.getStudentByName(name)
                        withContext(Dispatchers.Main) {
                            textQueryStudent.text = buildString {
                                results.forEach { student ->
                                    append("${student.id}-${student.name}\n")
                                }
                            }
                        }
                    }
                }
    
                deleteStudent.setOnClickListener {
                    val id = editStudentId.text.toString().toIntOrNull() ?: 0
                    val name = editStudentName.text.toString()
                    if (id > 0) {
                        CoroutineScope(Dispatchers.IO).launch {
                            myDao.deleteStudent(Student(id, name))
                        }
                    }
                    editStudentId.text = null
                    editStudentName.text = null
                }
            }
        }
    }

     

     


     

    📝 공부한 Kotlin 정리

    1) Room

    • SQLite를 쉽게 사용할 수 있는 데이터베이스 객체 매핑 라이브러리
    • 쉽게 Query를 사용할 수 있는 API를 제공
    • Query를 컴파일 시간에 검증
    • Query결과를 LiveData로하여 데이터베이스가 변경될 때마다 쉽게 UI를 변경 가능
    • SQLite 보다 Room을 사용할 것을 권장

     

    2) Room 주요 3요소

    • @Database: 클래스를 데이터베이스로 지정하는 annotation, RoomDatabase를 상속받은 클래스여야 한다.
      (Room.databaseBuilder를 이용하여 인스턴스를 생성)
    • @Entity: 클래스를 테이블 스키마로 지정하는 annotation
    • @Dao: 클래스를 DAO(Data Access Object)로 지정하는 annotation
      (기본적인 insert, delete, update SQL은 자동으로 만들어주며, 복잡한 SQL은 직접 만들 수 있다.)

     

    3) DAO

    • DAO는 interface나 abstract class로 정의되어야 한다.
    • Annotation에 SQL 쿼리를 정의하고 그 쿼리를 위한 메소드를 선언한다.
    • 가능한 annotation으로 @Insert, @Update, @Delete, @Query가 있다.
    • @Insert, @Update, @Delete는 SQL 쿼리를 작성하지 않아도 컴파일러가 자동으로 생성한다.
    • @Insert나 @Update는 key가 중복되는 경우 처리를 위해 onConflict를 지정할 수 있다.
      • OnConflictStrategy.ABORT: key 충돌시 종료
      • OnConflictStrategy.IGNORE: key 충돌 무시
      • OnConflictStrategy.REPLACE: key 충돌시 새로운 데이터로 변경
    • @Update나 @Delete는 primary key에 해당되는 튜플을 찾아서 변경/삭제한다.

     

    4) Room Database의 주요 어노테이션(Annotations)

    1. @Database
      • 데이터베이스 클래스를 정의할 때 사용한다.
      • 데이터베이스에 포함될 엔티티와 버전을 명시한다.
    2. @Entity
      • 데이터베이스 내의 테이블을 정의할 때 사용한다.
      • 클래스 이름이 테이블 이름으로 사용되며, 필드는 컬럼으로 매핑된다.
    3. @PrimaryKey
      • 엔티티의 기본 키(primary key)를 정의할 때 사용한다.
      • 유니크한 값이어야 하며, 데이터베이스 내에서 각 엔티티를 구분하는 데 사용된다.
    4. @ColumnInfo
      • 테이블의 컬럼 정보를 세부적으로 정의할 때 사용한다.
      • 컬럼의 이름, 타입, 인덱스 등을 설정할 수 있다.
    5. @Dao
      • 데이터 접근 객체(Data Access Object)를 정의할 때 사용한다.
      • 데이터베이스의 CRUD(Create, Read, Update, Delete) 연산을 위한 메소드를 포함한다.
    6. @Insert
      • 데이터를 삽입하는 메소드에 사용한다.
      • 해당 메소드는 엔티티를 인자로 받아 데이터베이스에 추가한다.
    7. @Query
      • 복잡한 SQL 쿼리를 실행하는 메소드에 사용한다.
      • 메소드에 주어진 SQL 쿼리를 실행하여 결과를 반환한다.
    8. @Update
      • 데이터를 업데이트하는 메소드에 사용한다.
      • 인자로 받은 엔티티의 데이터로 기존 레코드를 갱신한다.
    9. @Delete
      • 데이터를 삭제하는 메소드에 사용한다.
      • 인자로 받은 엔티티를 데이터베이스에서 제거한다.
    10. @Transaction
      • 메소드가 하나의 트랜잭션으로 실행되어야 함을 나타낸다.
      • 여러 연산을 하나의 작업으로 묶어 실행할 때 사용한다.
    11. @ForeignKey
      • 엔티티 간의 외래 키 관계를 정의할 때 사용한다.
      • 참조 무결성을 유지하는 데 도움을 준다.
    12. @Index
      • 특정 컬럼에 인덱스를 생성할 때 사용한다.
      • 쿼리 성능을 향상하는 데 유용하다.
    1.  
Designed by Tistory.