iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 29
0
自我挑戰組

Kotlin Everyday:新手寫程式踩的坑系列 第 29

Day 29 ─用 Kotlin 做書籍檢索 SQLite 資料庫(下)

  • 分享至 

  • xImage
  •  

首先,我們要設定好用來顯示資料畫面的 Adapter,之前做 RecycleView 時是自定義 itemView,這次偷懶一下直接用內建的 Layout 樣式、也不需要開新的 .kt 檔,直接寫在 MainActivity 即可:

  • 定義 item 為 ArrayList() 列表
  • 讓 adapter 繼承 ArrayAdapter,並定義樣式 simple_list_1
  • 將這個 adapter 指給 listView
private var items : ArrayList<String> = ArrayList()  //定義資料清單
private lateinit var adapter: ArrayAdapter<String>   //定義 adapter 繼承 ArrayAdapter

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    title = "書籍管理系統"
    adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items)
    listView.adapter = adapter          //simple_list_1 為最基本的樣式
}

畫面做完了可以來寫功能,昨天寫好資料庫、表格類別後,今天將會實例化資料庫物件,並調用 SQL 裡的指令來設定按鈕功能:

一、建立資料庫物件

先在外面宣告性質為 SQLiteDatabase 的 db,讓其後面才賦值

private lateinit var db : SQLiteDatabase

getWritableDatabase

 db : SQLiteDatabase = MyDBHelper(this).writableDatabase

實例化資料庫
打開或創建一個可以用來讀取和寫入的資料庫物件,該方法會在 onCreate、onUpgrade 或 onOpen() 被調用,類似於 getSharedPreferences 取得 SharedPreferences 的方法,兩者不同之處在於當不需要資料庫時, database 要另外調用 close() 方法來關閉

二、運用 execSQL 與 rawQuery 方法

SQLiteDatabase
打開 SQLite 資料庫後,接下來可以利用 SQLiteDatabase 類別來操作資料庫,這個類別裡提供非常多方法可以拿來操縱資料,像是新增、查詢、更新或和刪除等等功能,其中常用的是 execSQL 和 rawQuery 方法:

  • execSQL 執行方法
    可執行新增 Insert、刪除 Delete、修正更新 Update 等指令
  • rawQuery 查詢語法
    可執行 Select 指令

參考1:Android之採用execSQL與rawQuery方法完成資料的添刪改查操作詳解
參考2:Android - kotlin 資料庫的使用——技術積累
參考3:MySQL 超新手入門(3)SELECT 基礎查詢

三、實作 execSQL 方法

execSQL 方法可以用來執行新增 Insert、刪除 Delete、修正更新 Update 等等指令,於是我們就來練習設定這三個按鈕:

I. 新增資料: INSERT INTO

fun insert(table: String!, nullColumnHack: String!, values: ContentValues!): Long
  • 參數1 表格名稱
  • 參數2 如為空列,預設值
  • 參數3 以 ContentValues 型別封裝的欄位名稱和欄位值的Map
   val db : SQLiteDatabase = helper.getWritableDatabase()
   db.execSQL(“INSERT INTO 表格名稱(欄位1名稱, 欄位2名稱) VALUES('欄位1值', '欄位2值')”)
   db.close()

插入資料指令,在編寫 SQL 執行時就可以依照上面寫法,先打開資料庫,寫進資料時先放入表格欄位名稱、再放其對應的資料值,最後以提示 Toast 及輸入欄位為空 來暗示使用者儲存步驟已完成

  1. 打開資料庫 db
  2. INSERT INTO 指令,寫入欄位對應的值,這樣就把資料存進 SQLite 裡面囉
  3. 切記!不然會報錯!VALUES 裡面的東西各自要用' '包好
  4. 關閉資料庫 db.close() 先略過
img_add.setOnClickListener {
    if( (ed_book.text.isBlank()) || (ed_price.text.isBlank()) )
    Toast.makeText(this, "欄位請勿空白", Toast.LENGTH_SHORT).show()
    else{
        db.execSQL("INSERT INTO myTable(book,price) VALUES('${ed_book.text}', '${ed_price.text}')")        
        Toast.makeText(this, "新增書名「${ed_book.text}」價格「${ed_price.text}」", Toast.LENGTH_SHORT ).show()
        ed_book.setText("")
        ed_price.setText("")
    }
}

II. 刪除資料:DELETE FROM

fun delete(table: String!, whereClause: String!, whereArgs: Array<String!>!): Int
  • 參數1 表格名稱
  • 參數2 刪除的條件
  • 參數3 刪除條件值的陣列
   val db : SQLiteDatabase = helper.getWritableDatabase()
   db.execSQL(“DELETE FROM 表格名稱 WHERE 刪除條件”)
   db.close()

刪除資料指令,一樣先打開資料庫,這次要放入表格名稱和以 WHERE 書寫的條件句,最後以提示 Toast 及輸入欄位為空 來暗示使用者刪除步驟已完成

  1. 打開資料庫 db
  2. DELETE FROM 指令,寫入表格名稱及設條件句 WHERE
  3. 條件為使用者輸入的書名 WHERE book = '${ed_book.text}'
  4. WHERE 條件式一定要加哦!只寫表格名稱的話,全部資料會被刪除
  5. 參考 DELETE FROM 敘述句
img_delete.setOnClickListener {
    if (ed_book.text.isNullOrEmpty())
        Toast.makeText(this, "書名請勿空白", Toast.LENGTH_SHORT).show()
        
    else{
        db.execSQL("DELETE FROM myTable WHERE book = '${ed_book.text}'")
        Toast.makeText(this, "刪除書名為「${ed_book.text}」", Toast.LENGTH_SHORT).show()
        ed_book.setText("")
        ed_price.setText("")
    }
}

III. 編輯功能: UPDATE SET

fun update(table: String!, values: ContentValues!, whereClause: String!, whereArgs: Array<String!>!): Int
  • 參數1 表格名稱
  • 參數2 更新的值對 Key-Value
  • 參數3 判斷條件(WHERE 字句)
  • 參數4 更新條件陣列
   val db : SQLiteDatabase = helper.getWritableDatabase()
   db.execSQL(“UPDATE 表格名稱 SET Key-Value WHERE 更新條件”)
   db.close()

目前練習只先做修改價格,輸入要修改價格的書名(WHERE 條件)和新的價格(值對 Key-Value),寫法和刪除資料大同小異,就是

  • UPDATE 表格名稱
  • SET 修改欄位及值
  • WHERE 更新條件
img_edit.setOnClickListener {
    if( (ed_book.length()<1) || ed_price.length()<1)
        Toast.makeText(this, "欄位請勿空白", Toast.LENGTH_SHORT).show()
    else{
        db.execSQL("UPDATE myTable SET price = ${ed_price.text} WHERE book = '${ed_book.text}'")
        Toast.makeText(this, "更新書名'${ed_book.text}' 價格'${ed_price.text}'", Toast.LENGTH_SHORT).show()
        ed_price.setText("")
        ed_book.setText("")
    }
}

四、實作 rawQuery 方法

輸入書名,點擊按鈕可以查出該筆資料,並且有一個 Toast 提醒使用者一共有幾筆符合條件的資料,會一一顯示在下面的 ListView

rawQuery
SQLite 的查詢語法,參考超新手入門,查詢敘述通常是 SELECT 句搭配 FROM 來使用,而「*」表示表格的所有欄位:

指定表格

SELECT * FROM myTABLE   //表格名稱

指定表格及其中欄位

SELECT book FROM myTABLE   //指定表格某欄位

指定表格及查詢條件

SELECT * FROM myTABLE WHERE 條件句   //查詢條件

使用 rawQuery 會傳回 Cursor 型別的物件,執行結果或進一步查詢資料,而 Cursor 類是一個游標介面,提供許多查詢方法,例如獲得總資料項數 getCount()、移動指標方法 move()、獲得列值方法 getString()等等,這幾個等會都能用上:

  1. 取得 Cursor 物件(rawQuery 語法--- SELECT * FROM
    按照上面方法指定表格及設定查詢條件
  fun rawQuery(sql: String!, selectionArgs: Array<String!>!): Cursor!

這邊值得一提的是模糊搜尋,之前刪除和修改資料都是以「欄位 = 值」方式指定條件,但是在搜尋時可能不太確定資料名稱,這時候可以以 LIKE 來執行搜尋動作,以「%」或「_ 」添加在搜尋字眼兩邊

  db.rawQuery("SELECT * FROM myTABLE WHERE book Like '%${ed_book.text}'%", null)
  1. 清空 items 資料列表原本的資料,要讓 ListView 顯示搜尋到的結果
  items.clear()
  1. 以迴圈去跑,添加每一筆查詢到的資料,放入 items 清單

這裡應用到兩個 Cursor 方法--- cursor.getString 搭配 cursor.moveToNext()
簡單來說,設一迴圈從 i = 0 開始跑,去取得索引值為 i 的資料值,這時候 cursor.moveToNext() 很有用,它代表「移動到下一筆資料」,當這筆資料值取完、添加到 items 列表後,使用它會繼續跑下一筆,跑到 cursor.count (資料總筆數)停止。

Cursor.getString

  fun getString(columnIndex: Int): String!

而資料怎麼取,從文件說明可以知道是以 columnIndex 欄位索引值來取資料,我需要的是書名和價格兩欄位,於是用 cursor.getString(1) 和 cursor.getString(2) 來取,沒想到一整個系統報錯,又回到 Helper 看一下資料庫表格創建的寫法:

確認了欄位是以「id、book、price」創建,我想不到問題出在哪裡,於是用了 Day21 ─Stetho 超級好用的工具 去查看 SQLite 內資料長相,咦?!

rowid 是什麼??我明明寫的是 id 啊!

ROWID

  1. SQLite 中的每張表格裡的每一列都會有一個「ROWID」欄位,同一張表格裡的每列 ROWID 值都是唯一的
  2. 如果表格內有一個「INTEGER PRIMARY KEY」類型的欄位,那這個欄位就會變成 ROWID 的別稱
  3. 如果 INSERT 指令裡未指定 ROWID 的值、或 ROWID 的值是 NULL,那 SQLite 便會自動地產生一個合適的 ROWID 。作法通常是取表格內最大的 ROWID 值再加 1,即為新的 ROWID

因為我們在創建 id 時加上了特別指令 PRIMARY KEY AUTOINCREMENT,所以 id 自動變成 ROWID 的別稱,總算知道是怎麼一回事,但還是沒有解決到欄位指定錯誤的問題,繼續找資料,終於在 SQLite 學習筆記之四 - Query Planning 看到問題:

ROWID 不能夠算在內容,而索引是取內容欄位,因此書名 book 才是欄位第 0 列、索引值為(0)

for (i in 0 until cursor.count){
    items.add("書名:${cursor.getString(0)}\t\t\t價格:${cursor.getString(1)}")
    cursor.moveToNext()
}
  1. Toast提醒,顯示總共查詢到幾筆符合條件的資料

  2. 關閉資料庫

  3. 更新畫面

全部寫完之後來 biu 看看,結果報錯了,趕緊點開來看問題在哪,顯示出

android.database.CursorIndexOutOfBoundsException: Index -1 requested, with a size of 1

這個問題其實與 Cursor 初始位置有關,在cursor的遍历时moveToFirst和moveToNext的区别有提到 Cursor 初始位置是在-1,而我們要跑的索引值 i 從 0 開始,所以要讓 Cursor 位置移動到 0,利用 movetofirst() 就可以囉~

img_query.setOnClickListener {
    val cursor = db.rawQuery("SELECT * FROM myTABLE WHERE book Like '${ed_book.text}'", null)
    
    cursor.moveToFirst()  //✦這步很重要!✦
      
    items.clear()
    Toast.makeText(this, "符合條件共${cursor.count}筆資料", Toast.LENGTH_SHORT).show()
    for (i in 0 until cursor.count){
        items.add("書名:${cursor.getString(0)}\t\t\t價格:${cursor.getString(1)}")
        cursor.moveToNext()
    }
    cursor.close()
    adapter.notifyDataSetChanged()
}

最後,要來補個東西,我希望在剛跳入 APP 或是每次執行完任何動作,都可以即時更新畫面
另外寫一個函式 show(),類似於查詢功能的寫法,只是不指定條件,要求查詢目前全部的表格資料,然後顯示在 ListView上,把這個 show() 放到

  1. 初始畫面
  2. 各個功能結尾

注意!裡面有 cursor.close(),這也是為什麼前面在寫各功能時沒有放入它,因為要在最後更新畫面時寫入啊~

fun show(){
    val cursor = db.rawQuery("SELECT * FROM myTABLE", null)
    cursor.moveToFirst()
    items.clear()
    for (i in 0 until cursor.count) {
        items.add("書名:${cursor.getString(0)}\t\t\t價格:${cursor.getString(1)}")
        cursor.moveToNext()
    }
    cursor.close()
    adapter.notifyDataSetChanged()
}

上一篇
Day 28 ─用 Kotlin 做書籍檢索 SQLite 資料庫(上)
下一篇
Day 30 ─完賽
系列文
Kotlin Everyday:新手寫程式踩的坑30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言