개발일지

Android in A..Z - Room (기본) 본문

Android (안드로이드)/Room

Android in A..Z - Room (기본)

강태종 2021. 1. 17. 02:28

Room

Room이란 SQLite를 쉽게 사용할 수 있도록 제공하는 추상레이어 라이브러리이다. Room을 사용하여 Android에 Database구현를 쉽고 빠르게 할 수 있다.

Room은 기본적으로 LiveData, Optional, Cursor 등의 강력한 기능도 지원한다.


dependency

dependencies {
  def room_version = "2.2.5"

  implementation "androidx.room:room-runtime:$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"
}

기본 구조

Room

  • Room Database : DAO를 얻을 수 있다.
  • DAO : 실제 데이터베이스에서 Entity에 대한 Select, Update, Delete 등의 작업을 수행한다.
  • Entities : 데이터베이스에 저장된 데이터를 저장하는 클래스이다.

RoomDatabase

@Database 선언과 필요한 내용을 기제하고, abstract class로 선언후에 RoomDatabase를 상속받아 abstract fun으로 DAO를 획득하는 함수를 선언한다.

* SingleTon Pattern을 사용하면 관리하기 편하다.

 

AppDatabase

@Database(entities = [Drawer::class, ToDo::class], version = 2, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    companion object {
        private var instance: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
                    .addCallback(object : Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            CoroutineScope(Dispatchers.IO).launch {
                                getInstance(context).drawer().insert(
                                    Drawer(name = "ToDo")
                                )
                            }
                        }
                    })
                    .addMigrations(MIGRATION_1_2)
                    .build()
            }.also {
                instance = it
            }
        }

        private val MIGRATION_1_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("ALTER TABLE ToDo ADD COLUMN isFinished INTEGER DEFAULT 0 NOT NULL")
            }
        }
    }

    abstract fun drawer(): DrawerDao
    abstract fun todo(): ToDoDao
}
  • @Database : Room에 Database를 알리는 어노테이션이다.
    • entities : Database에서 사용하는 Entity들을 나열한다.
    • version : Database의 버전을 적는다. (Database Scheme가 변경될 경우 버전을 바꾸고 Migration을 해야한다.
  • abstract fun을 선언하여 DAO를 얻을 수 있다.

DAO

기본적으로 interface로 선언하여 사용하고 insert, update, delete는 room에서 제공하는 에노테이션을 사용하고, select쿼리는 @Query 에노테이션을 사용하여 SQL을 작성한다.

* BaseDao를 만들어서 insert, delete, update에 대한 중복 코드를 줄일 수 있다.

* suspend fun으로 선언하여 사용하면 실수로 main thread에서 호출하는 것을 막을 수 있다.

 

BaseDao

@Dao
interface BaseDao<E: Any> {
    @Transaction
    @Insert
    suspend fun insert(vararg elements: E)

    @Transaction
    @Insert
    suspend fun insert(elements: List<E>)

    @Transaction
    @Delete
    suspend fun delete(vararg elements: E)

    @Transaction
    @Delete
    suspend fun delete(elements: List<E>)

    @Transaction
    @Update
    suspend fun update(vararg elements: E)

    @Transaction
    @Update
    suspend fun update(elements: List<E>)
}
  • @Transaction : 데이터베이스에 Transaction과 같은 기능(SQL을 수행중에 Exception이 발생하면 롤백한다.)
  • @Insert : Insert문이다.
  • @Delete : Delete문이다. (매개변수의 PrimaryKey를 통해서 일치하는 데이터를 delete한다.)
  • @Update : Update이다. (매개변수의 PrimaryKey를 통해서 일치하는 데이터를 update한다.)

DrawerDao

@Dao
interface DrawerDao : BaseDao<Drawer> {
    @Query("SELECT * FROM Drawer")
    fun findLiveData(): LiveData<MutableList<Drawer>>

    @Query("SELECT * FROM Drawer")
    fun findLiveDataWithToDo(): LiveData<MutableList<DrawerWithToDo>>
}

 

  • @Query : SQL문을 작성하여 결과 값을 받을 수 있다. (결과 값으로 List, LiveData, Object, Optional, Cursor등 다양한 형태로 받을 수 있다.)

ToDoDao

@Dao
interface ToDoDao : BaseDao<ToDo> {
    @Transaction
    @Query("SELECT * FROM ToDo WHERE drawerName = :drawerName")
    fun findLiveDataByDrawerName(drawerName: String): LiveData<MutableList<ToDo>>
}

Entity

@Entity 선언과 @PrimaryKey 선언을 사용하여 Entity로 설정할 수 있다.

 

Drawer

@Entity(
        indices = [
            Index(value = ["name"], unique = true)
        ]
)
data class Drawer(
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0L,
    var name: String = "",
)
  • @Entity : Room에 Entity 선언을 알린다.
    • indices : Database에 Index를 설정할 수 있다. (unique로 고유한 값을 가지는지 설정할 수 있다.)
  • @PrimaryKey : Entity의 PK를 설정한다. (여러개의 PK를 설정할 때는 @Entity 에노테이션 안에서 설정한다.)
    • autoGenerate : AUTO INCREAMENT와 같은 기능

ToDo

@Entity(
        foreignKeys = [
            ForeignKey(entity = Drawer::class, parentColumns = ["name"], childColumns = ["drawerName"], onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE)
        ]
)
data class ToDo(
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0L,
    var text: String = "",
    var drawerName: String = "",
    var isFinished: Boolean = false
)
  • foreignKeys : Entity의 FK를 설정한다.
    • entity : 관계를 설정할 Entity
    • parentColumns : FK를 받을 Column들을 나열한다.
    • childColumns : FK로 받는 Column들을 나열한다.
    • onDelete, onUpdate : FK의 제약사항을 설정한다. (CASCADE -> 같이 변경된다.)

사용

Room을 Main Thread에서 접근하면 Error가 발생하기 때문에 새로운 Thread나 Coroutine을 활용한다.

AppDatabase의 instance를 얻고 DAO를 사용하여 데이터베이스에 접근한다.

ToDo

 

DrawerFragment

class DrawerFragment : BaseFragment<FragmentDrawerBinding>(R.layout.fragment_drawer) {
    init {
        setHasOptionsMenu(true)
    }

    companion object {
        private const val MENU_DELETE = 1000
    }

    private lateinit var menu: Menu
    private val drawerWithToDoAdapter by lazy { DrawerWithToDoAdapter() }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        AppDatabase.getInstance(requireContext()).drawer().findLiveDataWithToDo().observe(viewLifecycleOwner) {
            drawerWithToDoAdapter.submitList(it)
        }
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        this.menu = menu
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            MENU_DELETE -> {
                drawerWithToDoAdapter.tracker?.let {
                    CoroutineScope(Dispatchers.IO).launch {
                        val deleteList = ArrayList<Drawer>(it.selection.size())
                        it.selection.forEach { id ->
                            deleteList.add(Drawer(id = id))
                        }

                        AppDatabase.getInstance(requireContext()).drawer().delete(deleteList)
                        withContext(Dispatchers.Main) {
                            it.clearSelection()
                        }
                    }
                }
            }
        }
        return super.onOptionsItemSelected(item)
    }

    override fun init() {
        super.init()
        initRecyclerView()
        initSelectionTracker()
        initOnAddButton()
    }

    private fun initRecyclerView() {
        with(binding.recyclerView) {
            adapter = drawerWithToDoAdapter
            addItemDecoration(GridSpacingItemDecoration(2, 10.toDp()))
        }
    }

    private fun initSelectionTracker() {
        with(binding.recyclerView) {
            drawerWithToDoAdapter.tracker = SelectionTracker.Builder(
                "Selection",
                this,
                DrawerWithToDoAdapter.DrawerWithToDoKeyProvider(this),
                DrawerWithToDoAdapter.DrawerWithToDoDetailsLookup(this),
                StorageStrategy.createLongStorage()
            ).withSelectionPredicate(
                SelectionPredicates.createSelectAnything()
            ).build().apply {
                addObserver(object : SelectionTracker.SelectionObserver<Long>() {
                    override fun onSelectionChanged() {
                        super.onSelectionChanged()
                        val tracker = this@apply
                        if (tracker.hasSelection() && menu.findItem(MENU_DELETE) == null) {
                            menu.add(Menu.NONE, MENU_DELETE, Menu.NONE, "Delete")
                                .setIcon(R.drawable.ic_delete)
                                .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
                        } else if (!tracker.hasSelection() && menu.findItem(MENU_DELETE) != null) {
                            menu.removeItem(MENU_DELETE)
                        }
                    }
                })
            }
        }

    }

    private fun initOnAddButton() {
        binding.setOnAdd {
            with(binding.text) {
                val name = text.trim()
                if (name.isNotEmpty()) {
                    CoroutineScope(Dispatchers.IO).launch {
                        AppDatabase.getInstance(requireContext()).drawer().insert(Drawer(name = name.toString()))
                        withContext(Dispatchers.Main) {
                            binding.text.text.clear()
                        }
                    }
                }
            }
        }
    }
}

 

ToDoFragment

class ToDoFragment : BaseFragment<FragmentTodoBinding>(R.layout.fragment_todo) {
    init {
        setHasOptionsMenu(true)
    }

    private val args by navArgs<ToDoFragmentArgs>()
    private val drawerName by lazy { args.drawerName }
    private val todoAdapter by lazy { ToDoAdapter() }

    private var filter by Delegates.observable(false) { _, _, _ ->
        submitList()
    }
    private var currentList = emptyList<ToDo>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        AppDatabase.getInstance(requireContext()).todo().findLiveDataByDrawerName(drawerName).observe(viewLifecycleOwner) {
            currentList = it
            submitList()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater.inflate(R.menu.menu_todo_fragment, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            R.id.finished -> {
                filter = true
            }
            R.id.notFinished -> {
                filter = false
            }
        }
        return super.onOptionsItemSelected(item)
    }

    private fun submitList() {
        todoAdapter.submitList(currentList.filter { it.isFinished == filter })
    }

    override fun init() {
        super.init()
        initToolbar()
        initRecyclerView()
        initOnAddButton()
    }

    private fun initToolbar() {
        (activity as AppCompatActivity).supportActionBar?.title = "$drawerName Drawer"
    }

    private fun initRecyclerView() {
        with(binding.recyclerView) {
            adapter = todoAdapter
            addItemDecoration(GridSpacingItemDecoration(1, 10.toDp()))

            ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
                override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
                    return viewHolder is ToDoAdapter.ToDoHolder
                }

                override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                    if (viewHolder is ToDoAdapter.ToDoHolder) {
                        if (!viewHolder.element.isFinished) {
                            CoroutineScope(Dispatchers.IO).launch {
                                viewHolder.element.isFinished = true
                                AppDatabase.getInstance(requireContext()).todo().update(viewHolder.element)
                            }
                        } else {
                            CoroutineScope(Dispatchers.IO).launch {
                                when(direction) {
                                    ItemTouchHelper.RIGHT -> {
                                        AppDatabase.getInstance(requireContext()).todo().delete(viewHolder.element)
                                    }
                                    ItemTouchHelper.LEFT -> {
                                        viewHolder.element.isFinished = false
                                        AppDatabase.getInstance(requireContext()).todo().update(viewHolder.element)
                                    }
                                }
                            }
                        }
                    }
                }
            }).attachToRecyclerView(this)
        }
    }

    private fun initOnAddButton() {
        binding.setOnAdd {
            with(binding.text) {
                val name = text.trim()
                if (name.isNotEmpty()) {
                    CoroutineScope(Dispatchers.IO).launch {
                        AppDatabase.getInstance(requireContext()).todo().insert(ToDo(text = name.toString(), drawerName = drawerName))
                        withContext(Dispatchers.Main) {
                            binding.text.text.clear()
                        }
                    }
                }
            }
        }
    }
}

Git (예제코드)

github.com/KangTaeJong98/Example/tree/main/Android/Room

 

KangTaeJong98/Example

My Example Code. Contribute to KangTaeJong98/Example development by creating an account on GitHub.

github.com

 

Comments