AndroidStudioでデータベースを使用する
これまでListViewとEditText、追加ボタン、削除ボタンを配置、さらに 独自ListViewのためのrow.xmlを作ってListViewやBaseAdapter、MutableListの仕組みを覚えました。
しかしMutableListには保存する仕組みはないので、何らかの方法で保存します。
ここではSQLiteを使って保存してみたいと思います。
これまでのソース
class MainActivity : AppCompatActivity() {
val customAdaptor= CustomListView(this@MainActivity)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Show()
}
fun onButtonAdd(view : View){
val et = findViewById<EditText>(R.id.etValue)
val msg = et.text.toString()
customAdaptor.Add(msg)
Show()
}
fun onButtonDel(view : View){
customAdaptor.Del(0)
Show()
}
fun Show(){
var listView = findViewById<ListView>(R.id.lvMain)
listView.adapter = customAdaptor
}
}
class CustomListView(private val context: Activity): BaseAdapter() {
private var mapList:MutableList<MutableMap<String,Any>> = mutableListOf()
private var map = mutableMapOf<String,Any>()
override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
val inflater = context.layoutInflater
val view1 = inflater.inflate(R.layout.row,null)
var fMsg = view1.findViewById<TextView>(R.id.tvMsg) // row.xmlレイアウトのtvMsgを参照
var fDt = view1.findViewById<TextView>(R.id.tvDateTime)
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")
val s = mapList[p0].get("dt").toString()
val dt = LocalDateTime.parse(s)
val dts = dt.format(formatter)
fMsg.setText(mapList[p0].get("msg").toString()) // tvMsgのtextに 引数 p0を返す
fDt.setText(dts) // tvMsgのtextに 引数 p0を返す
return view1
}
override fun getItem(position: Int): Any {
return "" // p0 にあるデータを返す※今回は未使用
}
override fun getItemId(position: Int): Long {
return position.toLong() // 識別するためのidを返す
}
override fun getCount(): Int {
return mapList.count() // 一覧の件数
}
fun Add(msg : String){
val dt = LocalDateTime.now()
map = mutableMapOf("msg" to msg,"dt" to dt)
mapList.add(map)
}
fun Del(idx : Int){
mapList.removeAt(idx)
}
}
SQLiteとは
SQLiteはAndroidStudioの環境で使うことが出来るデータベースです。
他のデータベースを使うことも出来ますが、ここでは以下の理由で採用としています。
1.何もしなくてもAndroidStudioで使うことが出来る
2.SQLの知識を必要とするが、カプセル化してしまえばどれも同じ
3.データベースの中身を見るデバッグ機能がある
1や2も重要ですが特に3を重視しています。
今回作るデータベースのサンプルを学ぶにあたって必要な単語だけ簡単に説明します。
データベースファイル名
データベースのデータが詰まったファイルです。
SQLiteの場合拡張子dbで作るようです。
Androidアプリで使用するファイルは各アプリ毎に異なるフォルダに保存されますので適当な名前にしても上書きされることはありません。
テーブル名
データベースファイルの中にはいくつものテーブルとよばれるデータが入ったものがあります。1つのデータベースファイルに対して複数のテーブルを作る事が出来ます。
データベースバージョン
使用するデータベースのバージョンを指定するのですが今のところ1を指定するようです。
フィールド
サンプルで出てこない用語ですが表計算の(列)横方向、コレクションのキーのようなものと覚えておいて下さい。
レコード
サンプルで出てこない用語ですが表計算の(行)縦方向、MutableListで定義したリスト List[xx] のようなものがレコードに該当します。
SQL
データベースで使用する言語です。SQLは共通の言語ですが各データベースによって拡張されています。
ここではSQLの詳細な説明はしません。
データベース構造
1つのデータベースファイルにはいくつものテーブルがあり、そこにはユーザーが定義したフィールドを持つレコードが存在しています。フィールドとレコードの関係は
レコード\フィールド | _id | dt | msg |
---|---|---|---|
1番目 | 0 | 9:38:04 | Android |
2番目 | 1 | 10:21:42 | Kotlin |
このようになっており、1番目のレコードのmsgフィールドの値はAndroidとなります。
フィールド名は muTableListで使用しているものをそのまま使用しています。
データベースフィールドの型
フィールドを定義するとき型を指定します。ただしJavaでもKotlinでもなくSQLの仕様に基づく型となります。
上記の例では _id、dt、msgの3つのフィールドがありますが、今回は _idが数値型、dtとmsgは文字列型として定義します。
フィールド | 型 |
---|---|
_id | INTEGER PRIMARY KEY |
dt | TEXT |
msg | TEXT |
文字列型はStringじゃなくて TEXTとして指定されているのはまだわかるとして _idの方は複雑に見えます。
そもそも _idがどこから出てきたのか?なぜアンダーバーで始まっているのか?など謎が深まります。
データベースにはインデックス値がない
maplist : MutableListを定義すると maplist[0] というようにインデックス値を利用した参照が出来るのですがデータベースにはインデックス値がありません。
そこで各レコードにはキーと呼ばれるフィールドを用意してその値はレコード毎に異なる値が入る仕組みとするのですが、その指定方法が PRIMARY KEY部分です。
INTEGER つまり数値型のキー id には識別のための値が入りますよということになります。
** PRIMARY KEY**として使うフィールド名には「」で始めるっていう決まりがあるようなので_id としています。
データベースのヘルパークラスを作成する
データベースを扱うにはまずデータベースのヘルパークラスを作成します。Android初心者かつKotlin初心者の筆者は世の中のソースを参考にして自分流に変えています。
class DatabaseHelper(private val context: Context, dbname : String, dbVersion : Int) : SQLiteOpenHelper(context,dbname,null,dbVersion) {
override fun onCreate(db: SQLiteDatabase) {
val sql = "CREATE TABLE tbltest ( _id INTEGER PRIMARY KEY,dt TEXT,msg TEXT);"
db.execSQL(sql)
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
fun fileDelete(){
context.deleteDatabase(databaseName)
}
}
クラスの定義に実装しないといけない抽象クラスonCreateとonUpdateがありますのでそれぞれ実装します。onUpdateは空で構いません。
onCreateイベントは、データベースが使用されるときにデータベースファイルが存在していない場合に発生するようです。
そのためonCreateイベント内でデータベースのテーブルを作成することが一般的のようです。
データベースのテーブルの定義を間違ってしまってもこのonCreateクラスは一度しか呼ばれません。
もし間違ってしまうと後々面倒なのでfileDeleteクラスも実装しました。
CRATE TABLE SQLを説明
CREATE TABLE tbltest ( _id INTEGER PRIMARY KEY,dt TEXT,msg TEXT);の1行の意味を簡単に説明します。
命令 | 説明 |
---|---|
CREATE TABLE | データベースにテーブルを作成 |
tbltest(...) | 作るのはtbltestという名前のテーブル その定義は()内に記述 |
_id INTEGER PRIMARY KEY | レコード識別用に _idというフィールドを使う |
dt TEXT | 文字型の dt というフィールをを使う |
msg TEXT | 文字型のmsgというフィールドを使う |
SQLは命令の区切りとして「;」セミコロンを使うので念のため最後に付けています。
ヘルパークラスを利用する。
CustomListViewクラス内に定義します。
private val _helper = DatabaseHelper(context,"sample.db",1)
一般的なサンプルと異なり、データベースファイル名やバージョンを定義時に指定出来るようにしましたが、テーブル名の指定は出来ないなどまだ不完全ですが後回しとします。
ヘルパーが定義されて実行されるようになりますが、アプリが停止してしまうなどしてデータベースが開きっぱなしになるのは良くないのでCustomListViewクラス内にデータベースを閉じるメソッドを追加します。
fun databaseClose(){
_helper.close()
}
MainActivityのonDestroyを実装して、アプリが終了するときにはデータベースを閉じましょう。
override fun onDestroy() {
customAdaptor.databaseClose()
super.onDestroy()
}
CustomListViewクラスのAddメソッドは
fun Add(msg : String){
val dt = LocalDateTime.now()
map = mutableMapOf("msg" to msg,"dt" to dt)
mapList.add(map)
}
AddメソッドでデータベースにINSERT
となっていますがデータベースに追加する処理を追加します。
fun Add(msg : String){
val dt = LocalDateTime.now()
val db = _helper.writableDatabase
val sqlInsert = "INSERT INTO cocktailmemos (dt, msg) VALUES (?, ?);"
var stmt = db.compileStatement(sqlInsert)
stmt.bindString(1, dt.toString())
stmt.bindString(2, msg)
val id = stmt.executeInsert()
map = mutableMapOf("msg" to msg,"dt" to dt,"id" to id)
mapList.add(map)
}
JavaかKotlinのデータベース処理が特殊なのか意味があるのかとにかくわかりにくくなっています。
SQLとしてフィールド dt に 8:06:54 msg に テストというレコードを追加する場合は
INSERT INTO tbltest (dt, msg) VALUES ("8:06:54", "テスト");
となります。
命令 | 説明 |
---|---|
INSERT INTO | データベースにレコードを追加 |
tbltest | レコードを追加するのはtbltestという名前のテーブル |
(dt, msg) | 追加するレコードに追加するフィールド名※複数 |
|VALUES|値を代入する命令|
|("8:06:54", "テスト");|dt に 8:06:54が msgにテストが入ったレコードが追加されます|
フィールドには _id があったはずですがこれは指定しません。
指定しても良いですが同じ値にならないように考えるのはデータベースの方に任せた方が楽です。
追加された _idの番号は 変数idに入ります。
入ったidはMutableListでも管理できるように idの処理が追加されています。
このSQLを作りたいがためにINSERT INTO tbltest (dt, msg) VALUES (?, ?);という文字列に値を埋め込む処理が行われています。
命令 | 説明 |
---|---|
stmt.bindString(1, dt.toString()) | 1番目の?に dtを文字列にしたものを入れる |
stmt.bindString(2, msg) | 2番目の?に msgの値が入る |
というSQLに変換されるのです。
DelメソッドでデータベースにDELETE
MutableListで管理しているインデックス番号 id に一致するレコードを削除します。
fun Del(idx : Int){
val id = mapList[idx].get("id").toString().toLong()
val db = _helper.writableDatabase
val sqlDelete = "DELETE FROM tbltest WHERE _id = ?"
var stmt = db.compileStatement(sqlDelete)
stmt.bindLong(1, id.toLong())
stmt.executeUpdateDelete()
mapList.removeAt(idx)
}
削除を指定したmuTablelistが持つ idの値を得て、その idを持つレコードを削除します。
データベースライブラリが複雑にしてしまってますがSQLとしては
DELETE FROM tbltest WHERE _id = ?
で?の部分がidに変換されて実行されるだけです。
命令 | 説明 |
---|---|
DELETE FROM | データベースのレコードを削除する命令 |
tbltest | 削除するレコードがあるテーブル名 |
WHERE | 以下一致するもの という命令 |
_id = | idの値に一致する物が選ばれる |
データベースをデバッグする
さてここからが本題です。
AddとDelを追加したところでエラーが出ないか試して見て下さい。
データベースのテーブル作成に失敗した場合は作っておいたデータベース削除命令を実行して下さい。
アプリで作成したファイルはどこにあるのか?
そもそもアプリでデータベースを作りましたがどこに入っているのでしょうか?
エミュレータによるデバッグで見るときは初期レイアウトでの右側に**デバイスファイルエクスプローラー
**というタブがありここでエミュレータ上のファイルを見ることが出来るようになっています。
ファイルの場所(エミュレータ)
デバイスファイルエクスプローラーの data -> data -> 新規プロジェクトで指定した名称の下にデータファイルが存在します。
今回はデータベースのファイルを作ったのでdatabaseというフォルダが出来ていてその中にsample.dbファイルが出来ています。
デバイスファイルエクスプローラーの同期
デバイスファイルエクスプローラーはエミュレーター上のファイル構成に変化があっても自動的に更新されないようです。更新されていない場合は右クリックで「同期」を選択するとすぐに反映されます。
デバッグのためデータベースを開く(失敗編)
ファイルがあるので開いて中を見ることが出来そうな気がしますが残念ながら出来ません。
パソコン上に閲覧する環境があればと思いSQLiteをインストールして見ましたがだめなようです。
デバッグのためデータベースを開く(成功編)
参考資料が非常に少ないのですが作ったデータベースファイルの中身を実機でもエミュレーターでも見ることが出来る方法がわかりました。
debug-dbを使う
たった1行追加するだけでデータベースのデバッグが可能なようです。
プロジェクトの構成ツリーには appの他にGradleツリーがありますがその中のappとついている方を開いて
**dependencies {**という囲みの中の最下行に
debugImplementation 'com.amitshekhar.android:debug-db:1.0.6'
という1行を追加します。バージョン番号は執筆時の最新版です。
このファイルを編集すると更新が必要なので更新します。
これでデバッグが可能となりました。
エミュレータでも実機でも良いのでアプリを実行します。
実行中にデータベースのレコードを追加したあと、ホーム画面のChromeを起動します。
起動するのは実機なら実機の、エミュレーターであればエミュレーターのを起動します。
URLの入力欄に
http://localhost:8080
sanmple.dbファイルが表示され、テーブルの中身も表示出来ます。
最後にデータベースを読み込んでmuTableListに反映させましょう
データベースをmuTableListに反映
SELECT SQLを使う
fun dataLoad(){
val db = _helper.writableDatabase
val sql = "SELECT * FROM tbltest"
val cursor = db.rawQuery(sql,null)
var dt = ""
var msg = ""
var id : Long= 0
while (cursor.moveToNext()){
val idxNotemsg = cursor.getColumnIndex("msg")
msg = cursor.getString(idxNotemsg)
val idxNotedt = cursor.getColumnIndex("dt")
dt = cursor.getString(idxNotedt)
val idxNoteid = cursor.getColumnIndex("_id")
id = cursor.getLong(idxNoteid)
map = mutableMapOf("msg" to msg,"dt" to dt,"id" to id)
mapList.add(map)
}
}
複雑に見えますがやっていることは単純です。
SQLに加工がありません。
SELECT * FROM tbltest
命令 | 説明 |
---|---|
SELECT | データベースのレコードを取得する命令 |
* | 全てのデータを対象とします |
FROM tbltest | tbltestというテーブルから読み込みます |
実行するとcursor(カーソル)という変数に何やら値が渡されていますがこれは表計算ソフトを開いて1行目にカーソルを持っていったと考えて下さい。
そのカーソルがある行のフィールをを読み込んでいるのが**cursor.getColumnIndex("msg")**という部分です。フィールド名msgの値を読み込んでいます。
カーソルを次のレコード(行)に移動させる場合はcursor.moveToNext()を使います。
レコードが無い場合はfalseになりループ処理を抜けます。
作ったdataLoadメソッドをアプリの表示のときに使用すれば以前のリストの状態が再現されるはずです。
最後に
データベースの基本操作がこれで出来るようになりました。
もっと簡単なやり方があるようなのですがわかりやすさを優先しています。
データベースの中身が見えないことには何も始まりませんのであらかじめデバッグが出来るSQLiteを使いましたが将来的にはどうなのでしょうか?
SQLiteで問題が無いのならmuTableListと連携するクラスをカプセル化してしまえば汎用性は高そうです。