首先,我們要設定好用來顯示資料畫面的 Adapter,之前做 RecycleView 時是自定義 itemView,這次偷懶一下直接用內建的 Layout 樣式、也不需要開新的 .kt 檔,直接寫在 MainActivity 即可:
simple_list_1
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
db : SQLiteDatabase = MyDBHelper(this).writableDatabase
實例化資料庫
打開或創建一個可以用來讀取和寫入的資料庫物件,該方法會在 onCreate、onUpgrade 或 onOpen() 被調用,類似於 getSharedPreferences
取得 SharedPreferences 的方法,兩者不同之處在於當不需要資料庫時, database 要另外調用 close() 方法來關閉
SQLiteDatabase
打開 SQLite 資料庫後,接下來可以利用 SQLiteDatabase 類別來操作資料庫,這個類別裡提供非常多方法可以拿來操縱資料,像是新增、查詢、更新或和刪除等等功能,其中常用的是 execSQL 和 rawQuery 方法:
參考1:Android之採用execSQL與rawQuery方法完成資料的添刪改查操作詳解
參考2:Android - kotlin 資料庫的使用——技術積累
參考3:MySQL 超新手入門(3)SELECT 基礎查詢
execSQL 方法可以用來執行新增 Insert、刪除 Delete、修正更新 Update 等等指令,於是我們就來練習設定這三個按鈕:
I. 新增資料: INSERT INTO
fun insert(table: String!, nullColumnHack: String!, values: ContentValues!): Long
val db : SQLiteDatabase = helper.getWritableDatabase()
db.execSQL(“INSERT INTO 表格名稱(欄位1名稱, 欄位2名稱) VALUES('欄位1值', '欄位2值')”)
db.close()
插入資料指令,在編寫 SQL 執行時就可以依照上面寫法,先打開資料庫,寫進資料時先放入表格欄位名稱、再放其對應的資料值,最後以提示 Toast 及輸入欄位為空 來暗示使用者儲存步驟已完成
db
INSERT INTO
指令,寫入欄位對應的值,這樣就把資料存進 SQLite 裡面囉' '
包好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
val db : SQLiteDatabase = helper.getWritableDatabase()
db.execSQL(“DELETE FROM 表格名稱 WHERE 刪除條件”)
db.close()
刪除資料指令,一樣先打開資料庫,這次要放入表格名稱和以 WHERE 書寫的條件句,最後以提示 Toast 及輸入欄位為空 來暗示使用者刪除步驟已完成
db
DELETE FROM
指令,寫入表格名稱及設條件句 WHERE
WHERE book = '${ed_book.text}'
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
val db : SQLiteDatabase = helper.getWritableDatabase()
db.execSQL(“UPDATE 表格名稱 SET Key-Value WHERE 更新條件”)
db.close()
目前練習只先做修改價格,輸入要修改價格的書名(WHERE 條件)和新的價格(值對 Key-Value),寫法和刪除資料大同小異,就是
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("")
}
}
輸入書名,點擊按鈕可以查出該筆資料,並且有一個 Toast 提醒使用者一共有幾筆符合條件的資料,會一一顯示在下面的 ListView
rawQuery
SQLite 的查詢語法,參考超新手入門,查詢敘述通常是 SELECT 句搭配 FROM 來使用,而「*」表示表格的所有欄位:
指定表格
SELECT * FROM myTABLE //表格名稱
指定表格及其中欄位
SELECT book FROM myTABLE //指定表格某欄位
指定表格及查詢條件
SELECT * FROM myTABLE WHERE 條件句 //查詢條件
使用 rawQuery 會傳回 Cursor 型別的物件,執行結果或進一步查詢資料,而 Cursor 類是一個游標介面,提供許多查詢方法,例如獲得總資料項數 getCount()、移動指標方法 move()、獲得列值方法 getString()等等,這幾個等會都能用上:
SELECT * FROM
) fun rawQuery(sql: String!, selectionArgs: Array<String!>!): Cursor!
這邊值得一提的是模糊搜尋,之前刪除和修改資料都是以「欄位 = 值」方式指定條件,但是在搜尋時可能不太確定資料名稱,這時候可以以 LIKE 來執行搜尋動作,以「%」或「_ 」添加在搜尋字眼兩邊
db.rawQuery("SELECT * FROM myTABLE WHERE book Like '%${ed_book.text}'%", null)
items.clear()
這裡應用到兩個 Cursor 方法--- cursor.getString
搭配 cursor.moveToNext()
簡單來說,設一迴圈從 i = 0 開始跑,去取得索引值為 i 的資料值,這時候 cursor.moveToNext()
很有用,它代表「移動到下一筆資料」,當這筆資料值取完、添加到 items 列表後,使用它會繼續跑下一筆,跑到 cursor.count (資料總筆數)停止。
fun getString(columnIndex: Int): String!
而資料怎麼取,從文件說明可以知道是以 columnIndex 欄位索引值來取資料,我需要的是書名和價格兩欄位,於是用 cursor.getString(1) 和 cursor.getString(2) 來取,沒想到一整個系統報錯,又回到 Helper 看一下資料庫表格創建的寫法:
確認了欄位是以「id、book、price」創建,我想不到問題出在哪裡,於是用了 Day21 ─Stetho 超級好用的工具 去查看 SQLite 內資料長相,咦?!
rowid 是什麼??我明明寫的是 id 啊!
- SQLite 中的每張表格裡的每一列都會有一個「ROWID」欄位,同一張表格裡的每列 ROWID 值都是唯一的
- 如果表格內有一個「INTEGER PRIMARY KEY」類型的欄位,那這個欄位就會變成 ROWID 的別稱
- 如果 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()
}
Toast提醒,顯示總共查詢到幾筆符合條件的資料
關閉資料庫
更新畫面
全部寫完之後來 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() 放到
注意!裡面有 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()
}