[android/kotlin] DataBase로 저장하기

남윤희·2023년 9월 15일
0

kotlin

목록 보기
10/25
post-thumbnail

Room

1) Room 개요

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

2) Room 주요 3요소

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

3) gradle 파일 설정

  • Room은 안드로이드 아키텍처에 포함되어 있음
  • 사용하기위해 build.gradle 파일의 dependencies에 아래 내용을 추가해야 함
    • Androidx 사용하는 경우를 가정함, Android Studio와 SDK는 최신 버전으로 사용

    • '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"
      }

4) Entity 생성

  • Entity는 테이블 스키마 정의
  • CREATE TABLE student_table (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
  • @Entity data class Student
    @Entity(tableName = "student_table")    // 테이블 이름을 student_table로 지정함
    data class Student (
        @PrimaryKey 
    		@ColumnInfo(name = "student_id") 
        val id: Int,
        val name: String
    )

5) DAO 생성

  • DAO는 interface나 abstract class로 정의되어야 함
  • Annotation에 SQL 쿼리를 정의하고 그 쿼리를 위한 메소드를 선언
  • 가능한 annotation으로 @Insert, @Update, @Delete, @Query가 있음
    @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 충돌시 새로운 데이터로 변경
    • @Update나 @Delete는 primary key에 해당되는 튜플을 찾아서 변경/삭제 함
  • @Query로 리턴되는 데이터의 타입을 LiveData<>로 하면, 나중에 이 데이터가 업데이트될 때 Observer를 통해 할 수 있음
    @Query("SELECT * from table") fun getAllData() : LiveData<List<Data>>
  • @Query에 SQL을 정의할 때 메소드의 인자를 사용할 수 있음
    @Query("SELECT * FROM student_table WHERE name = :sname")
    suspend fun getStudentByName(sname: String): List<Student>
    인자 sname을 SQL에서 :sname으로 사용
  • fun 앞에 suspend는 Kotlin coroutine을 사용하는 것임, 나중에 이 메소드를 부를 때는 runBlocking {} 내에서 호출해야 함
  • LiveData는 비동기적으로 동작하기 때문에 coroutine으로 할 필요가 없음
@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

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

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

  • 인자 sname을 SQL에서 :sname으로 사용

6) Database 생성

  • RoomDatabase를 상속하여 자신의 Room 클래스를 만들어야 함
  • 포함되는 Entity들과 데이터베이스 버전(version)을 @Database annotation에 지정함
    • version이 기존에 저장되어 있는 데이터베이스보다 높으면, 데이터베이스를 open할 때 migration을 수행하게 됨
    • Migration 수행 방법은 RoomDatabase 객체의 addMigration() 메소드를 통해 알려줌
  • DAO를 가져올 수 있는 getter 메소드를 만듬
    • 실제 메소드 정의는 자동으로 생성됨
  • Room 클래스의 인스턴스는 하나만 있으면 되므로 Singleton 패턴을 사용
  • Room 클래스의 객체 생성은 Room.databaseBuilder()를 이용함
@Database(entities = [Student::class, ClassInfo::class, Enrollment::class, Teacher::class], 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) { 생략 }
        }

        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
        }
    }
}

7) Migration

  • 앞에서 MyRoomDatabase객체 생성 후 addMigrations() 메소드를 호출하여 Migration 방법을 지정했음
    • 여러개의 Migration 지정 가능

      Room.databaseBuilder(...).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
      
      private val MIGRATION_1_2 = object : Migration(1, 2) {   // version 1 -> 2
          override fun migrate(database: SupportSQLiteDatabase) {
              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")
          }
      }

8) UI와 연결

  • 안드로이드 아키텍처에 따라 Repository와 ViewModel을 사용하길 권장하지만
  • RoomDatabase객체에서 DAO 객체를 받아오고, 이 DAO객체의 메소드를 호출하여 데이터베이스를 접근함
    myDao = MyDatabase.getDatabase(this).getMyDao()
    runBlocking { // (주의) UI를 블록할 수 있는 DAO 메소드를 UI 스레드에서 바로 호출하면 안됨
        myDao.insertStudent(Student(1, "james"))  // suspend 지정되어 있음
    }
    val allStudents = myDao.getAllStudents() // LiveData는 Observer를 통해 비동기적으로 데이터를 가져옴

9) UI와 연결 - LiveData

  • LiveData<> 타입으로 리턴되는 DAO 메소드 경우
    • observe() 메소드를 이용하여 Observer를 지정
    • 데이터가 변경될 때마다 자동으로 Observer의 onChanged()가 호출됨
  • LiveData<>를 리턴하는 DAO 메소드는 Observer를 통해 비동기적으로 데이터를 받기 때문에, UI 스레드에서 직접 호출해도 문제 없음
    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
    }

10) Room 예제

build.gradle(:app)

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}

android {
    namespace 'com.android.roomexample'
    compileSdk 33

    defaultConfig {
        applicationId "com.android.roomexample"
        minSdk 31
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    viewBinding {
        enabled = true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.10.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

    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"
}

activity_main.xml

<?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>

MyDatabase

package com.android.roomexample

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() {
    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) {
                database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
            }
        }

        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()
                // for in-memory database
                /*INSTANCE = Room.inMemoryDatabaseBuilder(
                    context, MyDatabase::class.java
                ).build()*/
            }
            return INSTANCE as MyDatabase
        }
    }
}

MyDao

package com.android.roomexample

import androidx.lifecycle.LiveData
import androidx.room.*

@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); // primary key is used to find the student

}

MyEntity

package com.android.roomexample

import androidx.room.*

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

MainActivity.kt

package com.android.roomexample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.android.roomexample.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    lateinit var myDao: MyDAO

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

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

        val allStudents = myDao.getAllStudents()
        allStudents.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.isNotEmpty()) {
                CoroutineScope(Dispatchers.IO).launch {
                    myDao.insertStudent(Student(id, name))
                }
            }

            binding.editStudentId.text = null
            binding.editStudentName.text = null
        }

        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 = ""
                    }
                }
            }
        }
    }
}

profile
안드로이드 주니어 개발자

0개의 댓글