본문 바로가기

TIL

[TIL] Kotlin SQLite

-1- SQLite

 

SQLite란??

  • SQLite는 관계형 데이터베이스 관리 시스템(Relationnal Database Management System)으로 표준 SQL을 지원하는 DB파일 하나로 구현한 경량화된 RDBMS이며 Android Studio, IOS 등에서 DB구현을 위한 기본 라이브러리로 사용되어지고 있다.
  • 추가적인 특징은 다음과 같다.
    • 별도의 서버가 존재하지 않는다
      • SQLite는 클라이언트 - 서버 아키텍처를 사용하지 않으므로 별도의 데이터베이스 서버가 필요하지 않다.
    • ACID 지원
      • Atomicity( 원자성 ), Consistency( 일관성 ), Isolation( 고립,격리성 ), Durability( 지속성 )와 같은 ACID 특성을 제공하여 데이터 무결성을 보장한다.
    • 표준 SQL지원
      • SQLite는 대부분의 표준 SQL 문법과 기능을 지원한다.

  • Ranking of the most popular Databse 

 

 

관계형 데이터 베이스( Relational Database ) ??

  • 여러 건의 주문목록 데이터는 다음과 같이 테이블 구조로 관리할 수 있다. 이런식으로 통일된 규칙의 데이터를 관리할 수 있는 테이블을 데이터베이스 라고 한다.
ID ( 순서 ) 이름 가격       ( $ ) 주문번호 상태
0 불닭볶음면 9.9 qnfekfr-0231414 배송완료
1 생수 15.0 todtn-123905897 배송완료
2 비타민C 19.8 vitaminc-354980 배송중
  • 데이터는 Key와 Value의 형태로 저장되며 이름, 가격과 같이 테이블 구조의 열 상단을 차지하고 있는 것이 Key, 해당 Key에 포함되는 값이 Value다. 이때 ID는 각 데이터에 겹치지 않는 고유한 값이기 때문에 Primary Key라고 한다.

 

SQL 과 CRUD

  • 위에 작성된 DB에서 주문번호에 해당 하는 불닭 볶음면의 배송상태를 찾는 건 쉽겠지만 주문 목록이 더 늘어나고 또 다른 불닭 볶음면 주문 건이 추가되게 되면 해당 주문건의 불닭 볶음면의 배송상태를 확인하는 것이 어려워질 것이다. 이러한 복잡하게 된 DB는 SELECT, INSERT, UPDATE, DELETE 등의 SQL 문을 사용하여 데이터를 조회, 삽입, 수정 및 삭제할 수 있다.WHERE ~ 을 사용하여 조건에 따라 필터링 된 결과를 가져올 수 있으며, JOIN ~ 을 사용하여 여러 테이블 간의 관계를 처리할 수도 있다.
  • 자세한 문법 확인 https://en.wikipedia.org/wiki/SQL_syntax 

 

SQLite 사용

  • Android Studio는 데이터베이스 작업의 기본 Library로 SQLite를 지원하기 때문에, 각 안드로이드 API에는 SQLite가 기본적으로 내장되어져  있다.

  • Android Developer 에서는 SQLite를 더 안전하게 사용하기 위해서는 Room Database Library를 이용하라고 안내를 하고 있다. 간단한 작업을 하려고 하기 때문에 Room을 사용하지 않고 SQLiteOpenHelper를 사용해서 간단한 CRUD작업을 하는 예제를 만들어보자.

 

activity_main.xml

  • Insert, Update, Delete, View 각각의 버튼을 통해 ID, Name, Price, State EditText의 값을 추가하거나, 업데이트하거나 삭제 및 데이터 목록을 볼 수 있도록 xml을 만들어준다.
<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"
    tools:context=".MainActivity">


    <EditText
        android:id="@+id/editText_id"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:ems="10"
        android:inputType="text"
        android:hint="ID"
        android:paddingStart="16dp"
        app:layout_constraintBottom_toBottomOf="@+id/textView_id"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/textView_id"
        app:layout_constraintTop_toTopOf="parent" />
    <EditText
        android:id="@+id/editText_name"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:ems="10"
        android:inputType="text"
        android:hint="Name"
        android:paddingStart="16dp"
        app:layout_constraintBottom_toBottomOf="@+id/textView_name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/textView_name"
        app:layout_constraintTop_toBottomOf="@+id/editText_id" />

    <EditText
        android:id="@+id/editText_price"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:ems="10"
        android:inputType="text"
        android:hint="Price"
        android:paddingStart="16dp"
        app:layout_constraintBottom_toTopOf="@+id/textView_state"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/textView_price"
        app:layout_constraintTop_toBottomOf="@+id/editText_name" />

    <EditText
        android:id="@+id/editText_state"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:ems="10"
        android:inputType="text"
        android:hint="State"
        android:paddingStart="16dp"
        app:layout_constraintBottom_toBottomOf="@+id/textView_state"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/textView_state"
        app:layout_constraintTop_toBottomOf="@+id/editText_price" />

    <TextView
        android:id="@+id/textView_id"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:gravity="center"
        android:text="ID"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/textView_name"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:gravity="center"
        android:text="Name"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView_id" />

    <TextView
        android:id="@+id/textView_price"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:gravity="center"
        android:text="Price"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView_name" />
    <TextView
        android:id="@+id/textView_state"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:gravity="center"
        android:text="State"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView_price" />

    <Button
        android:id="@+id/button_insert"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="4dp"
        android:text="INSERT"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView_state" />

    <Button
        android:id="@+id/button_update"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:text="UPDATE"
        app:layout_constraintStart_toEndOf="@+id/button_insert"
        app:layout_constraintTop_toBottomOf="@+id/editText_state" />

    <Button
        android:id="@+id/button_delete"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:text="DELETE"
        app:layout_constraintStart_toEndOf="@+id/button_update"
        app:layout_constraintTop_toBottomOf="@+id/editText_state" />

    <Button
        android:id="@+id/button_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="4dp"
        android:text="VIEW"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/button_delete"
        app:layout_constraintTop_toBottomOf="@+id/editText_state" />

    <ScrollView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:padding="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button_insert">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

            <TextView
                android:id="@+id/textView_result"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="20sp"/>
        </LinearLayout>
    </ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

 

DatabaseHelper.kt [ 1 ] SQLiteOpenHelper 생성, 스키마 정의

class DatabaseHelper private constructor(context: Context):
    SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION){

    // SQLite 데이터 베이스의 스키마를 정의
    companion object {
        const val DATABASE_NAME = "OrderDetail.db"
        const val DATABASE_VERSION = 1
        const val TABLE_NAME = "order_table"
        const val COL1_ID = "_id"
        const val COL2_NAME = "name"
        const val COL3_PRICE = "price"
        const val COL4_STATE = "state"

        // DatabaseHelper가 여러개면 충돌을 일으킬 수 있기 때문에 SingleTon으로 생성.
        @Volatile
        private var instance: DatabaseHelper? = null

        @OptIn(InternalCoroutinesApi::class)
        fun getInstance(context: Context) =
            instance ?: synchronized(DatabaseHelper::class.java){
                instance ?: DatabaseHelper(context).also {
                    instance = it
                }
            }
    }
  • SQLiteopenHelper 클래스를 상속하는 DatabaseHelper 클래스를 만들어주고 데이터베이스의 스키마를 정의한다.
  • 데이터베이스의 스키마란, 데이터 베이스 내에 저장될 테이블과 해당 테이블의 열(column) 들을 구조적으로 정의하는 것을 의미한다. 스키마를 정의함으로 데이터베이스에서 데이터를 구성, 조직하는 방식을 결정한다.

 

 

DatabaseHelper.kt [ 2 ] onCreate, onUpgrade 테이블 생성하기

// 테이블 생성 쿼리
private val createQuery = "CREATE TABLE $TABLE_NAME (" +
        "$COL1_ID INTEGER PRIMARY KEY AUTOINCREMENT, " +
        "$COL2_NAME TEXT, " +
        "$COL3_PRICE TEXT, " +
        "$COL4_STATE TEXT " +
        ")"
// 테이블 삭제 쿼리
private val deleteQuery = "DROP TABLE IF EXISTS $TABLE_NAME"

override fun onCreate(db: SQLiteDatabase?) {
    db?.execSQL(createQuery)
}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
    if (oldVersion != newVersion) {
        db?.execSQL(deleteQuery)
        onCreate(db)
    }
}
  • 정의 해놓은 스키마를 기반으로 테이블을 생성한다.
  • SQLiteopenHelper는 onCreate()와 onUpgrade 메소드를 필수로 구현해야 한다.
  • execSQL()
    • SQLiteDatabase에서 SQL 문을 실행하는 역할을 하며 결과 반환값이 없어 주로 데이터 베이스 스키마 생성,수정,데이터 삽입 및 갱신 등에 사용 된다.
  • onCreate()
    • createQuery 에서 정의해둔 테이블 생성 쿼리를 적용해 execSQL() 메서드를 통해 SQL문을 실행한다.
  • onUpgrade()
    • newVision과 oldversion 버전 차이를 확인하여서 기존의 테이블을 삭제하고 다시 생성하는 것
    • Database의 스키마를 수정한 다음에 적용할 때 유용하다.

 

DatabaseHelper.kt [ 3 ] CRUD 추가하기

    // 데이터 추가
    fun insertData(name: String, price: String, state: String){
        val db = this.writableDatabase
        val contentValues = ContentValues().apply {
            put(COL2_NAME, name)
            put(COL3_PRICE, price)
            put(COL4_STATE, state)
        }
        db.insert(TABLE_NAME, null,contentValues)
    }

    // 데이터 수정
    fun updateData(id: String, name: String, price: String, state: String){
        val db = this.writableDatabase
        val contentValues = ContentValues().apply {
            put(COL2_NAME, name)
            put(COL3_PRICE, price)
            put(COL4_STATE, state)
        }
        db.update(TABLE_NAME, contentValues, "$COL1_ID = ?", arrayOf(id))
    }

    // 데이터 삭제
    fun deleteData(id: String){
        val db = this.writableDatabase
        db.delete(TABLE_NAME, "$COL1_ID = ?", arrayOf(id))
    }

    // 저장된 데이터 확인
    fun getAllData(): String {
        var result = "NO data in DB"

        val db = this.readableDatabase
        val cursor = db.rawQuery("SELECT * FROM $TABLE_NAME", null)

        try {
            if (cursor.count != 0){
                val stringBuffer = StringBuffer()
                while (cursor.moveToNext()){
                    stringBuffer.append("ID :" + cursor.getInt(0).toString() + "\n")
                    stringBuffer.append("NAME :" + cursor.getString(1) + "\n")
                    stringBuffer.append("PRICE :" + cursor.getString(2) + "\n")
                    stringBuffer.append("STATE :" + cursor.getString(3) + "\n")
                }
                result = stringBuffer.toString()
            }
        } catch (e: Exception){
            e.printStackTrace()
        } finally {
            if (cursor != null && !cursor.isClosed) {
                cursor.close()
            }
        }
        return result
    }
}
  • insertData()
    • name, price, state의 값을 파라미터로 받아 해당 값을 writableDatabase상태인 Database 에 contentValues로 묶어서 insert(Table, null, contextValues) 를 통해 데이터 베이스에 값을 추가한다 
  • update()
    • update는 id와 name 등의 파라미터를 받아 update()를 통해 파라미터로 받은 id에 해당하는 값의 name, price, state 값을 수정할 수 있다. 이때 "$COL1_ID = ? " 로 쿼리를 사용해 갱신할 데이터를 특정해줘야 한다.
  • delete()
    • delete는 파라미터로 받은 id를 primary key로 가진 데이터를 delete()를 통해 삭제한다
  • read()
    • 마지막으로 읽기를 구현한다. readableDatabase로 데이터를 읽을 수 있는 상태로 만들고 rawQuery에 " SELECT * FROM $TABLE_NAME " 을 전달하여 Cursor 객체를 받아온다. 객체가 정상적으로 얻어졌으면 try-catch 안에서 moveToNext로 데이터를 순회하ㅓ면서 값을 확인하고 커서를 다 사용했으면 close로 리소스를 반환하도록 한다
  • cursor?
    • 커서는 SQLite 데이터베이스에서 쿼리문의 결과 집합을 순회하고 읽을 수 있는 인터페이스를 제공하는 객체이다. 커서는 쿼리 결과에 대한 Row단위로 이동하며, 각 행에 저장된 열 값을 읽어올 수 있다.
    • 커서의 초기 위치는 첫번째 행 이전인 "before first" 상태이다. moveToFirst(), moveToLast(), moveToNext(). moveToPrevious(), moveToPosition(position: Int) 메서드로 커서의 위치를 다음행으로 이동하거나 이전행 또는 원하는 위치로 직접이동할 수 있다.

 

 

 

MainActivity.kt 에서 dbHelper 인스턴스 생성

class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding

    private val dbHelper: DatabaseHelper by lazy {
        DatabaseHelper.getInstance(applicationContext)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        insertDb()
        updateDb()
        deleteDb()
        getAllDb()
    }

    override fun onDestroy() {
        dbHelper.close()
        super.onDestroy()
    }
    private fun showTxt(text: String){
        binding.textViewResult.append(text + "\n")
    }

    private fun clearEditTexts() {
        with(binding){
            editTextId.setText("")
            editTextName.setText("")
            editTextPrice.setText("")
            editTextState.setText("")
        }
    }

    private fun insertDb() {
        binding.buttonInsert.setOnClickListener {
            try {
                dbHelper.insertData(
                    binding.editTextName.text.toString().trim(),
                    binding.editTextPrice.text.toString().trim(),
                    binding.editTextState.text.toString().trim(),
                )
                clearEditTexts()
                showTxt("Data inserted")
            }catch (e:Exception){
                e.printStackTrace()
            }
        }
    }

    private fun updateDb() {
        binding.buttonUpdate.setOnClickListener {
            try {
                dbHelper.updateData(
                    binding.editTextId.text.toString().trim(),
                    binding.editTextName.text.toString().trim(),
                    binding.editTextPrice.text.toString().trim(),
                    binding.editTextState.text.toString().trim(),
                )
                showTxt("Data updated")
            }catch (e:Exception){
                e.printStackTrace()
            }
        }
    }

    private fun deleteDb() {
        binding.buttonDelete.setOnClickListener {
            try {
                dbHelper.deleteData(binding.editTextId.text.toString().trim())
                clearEditTexts()
                showTxt("Data deleted")
            }catch (e:Exception){
                e.printStackTrace()
            }
        }
    }

    private fun getAllDb(){
        binding.buttonView.setOnClickListener {
            try {
                val selectResult = dbHelper.getAllData()
                showTxt(selectResult)
            }catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
}
  • MainActivity에서의 dhHelper 인스턴스 사용이 끝나게 되면 onDestory에서 close()를 통해 리소스를 반환하도록 한다.
  • 각 버튼 기능을 지정해준 뒤 onCreate()에서 실행

 

 

 


 

 

[오류,에러 등등]

1. 특이점은 없었다

 

 


[느낀 점]

1. 어렵다

 

2. Room을 공부하기전에 SQLite를 알아야 하듯이 조바심 갖지 말고 차근차근 하나씩 많이 배워야 겠다

 

 


[Reference]

 

// Database

https://velog.io/@dldmswo1209/Android-SQLite-%EC%BD%94%ED%8B%80%EB%A6%B0%EC%97%90%EC%84%9C-SQLite%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

https://hello-bryan.tistory.com/530

https://developer.android.com/training/data-storage/sqlite?hl=ko

https://cliearl.github.io/posts/android/implement-crud-with-sqliteopenhelper/