Android Studio + kotlin + SQLiteで音符フラッシュカードアプリを作る　⑫DB保存（SQLite、Insert）

#１．今回のテーマ

前回は、当日日付を取得して画面に表示する処理を実装しました。
今回は、ゲームの結果をデータベースに保存する処理を実装していきます。


#２．保存ボタンに対してlistenerをセット
結果画面で、「保存」ボタンをタップした時の処理として実装します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619303/7d86eeb0-6058-2575-d52a-8f2c1140b7e9.png)
保存ボタンは
**id : btSave**
で定義しています。

ボタンが押されてことを検知して処理を実行するときには、**listener**を使います。
リスナーについては以下で解説しています。
[⑤トップ画面からの遷移（インテント(putExtra)）](https://qiita.com/tomohiro-yamashita-5664/items/ae86d6273cfc8c13b482)

```kotlin:
class ResultActivity : AppCompatActivity() {

    private val _helper = DatabaseHelper(this@ResultActivity)

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

        //保存ボタンを取得
        val btSave = findViewById<Button>(R.id.btSave)
        //保存ボタンリスナー
        val listener5 = SaveButtonClickListener()
        //リスナーセット
        btSave.setOnClickListener(listener5)
    }
```

**btSave**というViewに対して、**SaveButtonClickListener**という名前のリスナーをセットしました。

以降の処理は、この**SaveButtonClickListener**クラス内に記述していきます。
その前に、androidでデータベースに接続する手順を次章で解説します。


#３．androidでのデータベース接続方法
android OSにはあらかじめSQLiteというデータベースが備わっています。
今回はこのSQLiteを使ってデータを保存していきます。

androidでのデータベース利用手順を図で表すと以下のような構成になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619303/c9e12b1f-394d-02a7-72f9-e5810b7dd01e.png)

・データベースヘルパークラスを作成し、そのインスタンスとしてデータベースヘルパーオブジェクトを生成
・アクティビティで、データベースヘルパーオブジェクトからデータベース接続オブジェクトを取得する
・データベース接続オブジェクトを使ってSQLを実行、結果を取得する
という流れです。

ということで、まずはデータベースへルパークラスを作成していきます。

#４．データベースヘルパークラスの実装
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619303/9bf7d231-2384-5121-a26e-5af35f1ea130.png)
android Studioから、新規のkotlinクラスを追加します。

[java]　⇒　[アプリケーション名]
を右クリックして、
[新規]　⇒　[Kotlin Class/File]
を選択します。
その後表示された画面で、クラス名にDatabaseHelperと入力してクラスを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619303/f17c8726-0878-e352-cd80-14aed1ca29a2.png)

DatabaseHelperに記述するコードは以下の通りです。

```kotlin:DatabaseHelper.kt
package com.websarva.wings.android.musicnotecardquiz

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class DatabaseHelper(context: Context): SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    companion object {
        private const val DATABASE_NAME = "musicnote.db"
        private const val DATABASE_VERSION = 1
    }

    override fun onCreate(db:SQLiteDatabase) {
        val sb = StringBuilder()
        sb.append("CREATE TABLE results (")
        sb.append("_id INTEGER PRIMARY KEY,")
        sb.append("name TEXT,")
        sb.append("level TEXT,")
        sb.append("time INTEGER,")
        sb.append("correctcount INTEGER,")
        sb.append("date TEXT")
        sb.append(");")
        val sql = sb.toString()

        db.execSQL(sql)
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {

    }
}
```

データベースヘルパークラスは、SQLiteOpenHelperクラスを継承して作ります。
その際、
・onCreate()
・onUpgrade()
の2つのメソッドを必ずオーバーライドする必要があります。
これらのメソッドの実装内容は後程解説します。

まずはSQLiteOperHelperクラスのコンストラクタを定義します。
SQLiteOpenHelperクラスのコンストラクタには4つの引数が必要になります。

|　　　　|引数の型と名称|内容|
|---|---|---|
|第1引数|context: Context|このDBHelperクラスを使うアクティビティ|
|第2引数|name: String|使用するデータベース名|
|第3引数|factory: SQLiteDatabase.CursorFactory|select結果を格納するcursorを自作する際に指定する。通常はnullで良い。|
|第4引数|version: Int|データベースのバージョン番号|

今回は以下の通り設定しました。
第1引数：オブジェクト作成時に引数として受け取る
第2引数：DATABASE_NAME
第3引数：null
第4引数：DATABASE_VERSION
第2引数と第4引数はクラス内でcompanion objectブロックとして固定値を定義します。
以下の部分です。

```kotlin:DatabaseHelper.kt
    companion object {
        private const val DATABASE_NAME = "musicnote.db"
        private const val DATABASE_VERSION = 1
    }
```

続いてonCreate()メソッドを実装します。
onCreate()は、android端末内に親クラスのコンストラクタ内で指定したデータベース名のデータベースが存在しないときに実行されます。つまり、アプリを最初に起動したときに1回だけ実行される処理になります。
ですので、onCreate()内では、テーブルを作成する処理など、初回に実行されるべき処理を実装する必要があります。

今回は**results**というテーブルを作成するSQL文を生成、実行しています。

```kotlin:DatabaseHelper.kt
    override fun onCreate(db:SQLiteDatabase) {
        val sb = StringBuilder()
        sb.append("CREATE TABLE results (")
        sb.append("_id INTEGER PRIMARY KEY,")
        sb.append("name TEXT,")
        sb.append("level TEXT,")
        sb.append("time INTEGER,")
        sb.append("correctcount INTEGER,")
        sb.append("date TEXT")
        sb.append(");")
        val sql = sb.toString()

        db.execSQL(sql)
    }
```

StringBuilderを使って細切れにSQLを生成していますが、実行したい文は以下です。

```sql
 CREATE TABLE results (_id INTEGER PRIMARY KEY, name TEXT, level TEXT, time INTEGER, correctcount INTEGER, date TEXT);
```

テーブルのカラムは以下の通りとしました。

|カラム名|内容|データ型|
|---|---|---|
|_id|行番号|INTEGER（主キー）|
|name|プレイヤー名|TEXT|
|level|ゲームレベル|TEXT|
|time|プレイ時間|INTEGER|
|correctcount|正解数|INTEGER|
|date|プレイ日付|TEXT|

最後に

```kotlin
db.execSQL(sql)
```

でSQLを実行します。

続いてonUpgrede()メソッドを記述していきます。

```kotlin:DatabaseHelper.kt
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}
```

上記のように、メソッドの中身は空っぽです。onUpgrade()は内部のデータベースのバージョン番号とコンストラクタの第2・第3引数で指定したバージョン番号に差異がある場合に実行されます。
今回は特に処理を実装する必要はないので空ですが、このメソッドは抽象メソッドなので、何も処理する必要がない場合でも記述する必要があります。

これでデータベースヘルパークラスが定義できました。
続いてデータベースヘルパークラスのオブジェクトを生成します。

#５．データベースヘルパーオブジェクトの生成、解放処理
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619303/175e039c-f4a1-4334-fe88-f4c78fc6774d.png)
4章で作成したデータベースヘルパーオブジェクトを生成する処理を、アクティビティクラスに記述していきます。データベースへの接続は様々な箇所で使われることを想定して、onCreate()などのメソッド内ではなくプロパティとしてアクティビティクラスの冒頭で記述します。

```kotlin:ResultActivity.kt
class ResultActivity : AppCompatActivity() {

    private val _helper = DatabaseHelper(this@ResultActivity)

    override fun onCreate(savedInstanceState: Bundle?) {
～～～略～～～
    }

}
```

データベースヘルパーオブジェクトはコンストラクタとして4つの引数が必要です。
この4つのうち第1引数であるcontextだけは、オブジェクト生成時に引数として渡す必要がありました。
データベースに接続したいアクティビティなので、自身(this@ResultActicity)を指定してオブジェクトを生成します。

生成したデータベースヘルパーオブジェクトは、適切なタイミングで解放してあげなければなりません。解放のタイミングは、アクティビティが終了するときです。
アクティビティ終了時に呼び出されるメソッドはonDestroy()です。
onDestory()メソッド内に解放処理を記述します。

```kotlin:ResultActivity.kt
class ResultActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
～～～略～～～
    }

    override fun onDestroy() {
        _helper.close()
        super.onDestroy()
    }
}
```

オブジェクトの解放を行うのは.close()メソッドです。
その後に親クラスのonDestroy()メソッドを実行してあげます。

以上で、データベースヘルパーオブジェクトの生成処理が記述できました。
ここまででデータベースに接続する準備が整いました。
後はデータベースヘルパーオブジェクト経由でデータベース接続オブジェクトを取得して、SQLを発行していきます。

#６．データ保存処理の実装
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619303/d843be5d-bef6-7bfb-36ea-29dbc927c1f7.png)

ここからの処理は、2章で実装した、保存ボタンが押された際に実行されるリスナークラス（SaveButtonClickListener）内に記述していきます。

処理の流れは、以下の通りです。
①データベース接続オブジェクトを取得
②保存したい画面データを取得
③主キーとなる_idを設定
④SQL文を生成
⑤バインド変数の設定
⑥SQL実行
です。
最後におまけでToastを表示します。（別途解説します）

SaveButtonClickListenerクラスのソースコードは以下の通りです。

```kotlin:ResultActivity.kt
    private inner class SaveButtonClickListener : View.OnClickListener {
        override fun onClick(view: View) {

            //データベース接続オブジェクトを取得
            val db = _helper.writableDatabase
            
            //各種画面データを取得
            val level = findViewById<TextView>(R.id.tvLevel).text
            val correctCount = findViewById<TextView>(R.id.tvCorrectCount).text
            val date = findViewById<TextView>(R.id.tvDate).text
            val name = findViewById<EditText>(R.id.etName).text
            //インテントからタイム（数値）を取得
            val timeValue = intent.getIntExtra("timeValue", 0)

            //現在のレコード件数を取得
            val sqlcount = "SELECT COUNT(*) FROM results"
            val cursor = db.rawQuery(sqlcount, null)
            var recordCount = ""

            while(cursor.moveToNext()) {
                val idxCount = cursor.getColumnIndex("COUNT(*)")
                recordCount = cursor.getString(idxCount)
            }


            //主キーは現在のレコード数 +1
            val _id = recordCount.toInt() +1

            val sqlInsert = "INSERT INTO results (_id, name, level, time, correctcount, date) VALUES (?, ?, ?, ?, ?, ?)"
            var stmt = db.compileStatement(sqlInsert)
            stmt.bindLong(1, _id.toLong())
            stmt.bindString(2, name.toString())
            stmt.bindString(3, level.toString())
            stmt.bindLong(4, timeValue.toLong())
            stmt.bindLong(5, correctCount.toString().toLong())
            stmt.bindString(6, date.toString())
            stmt.executeInsert()

            Toast.makeText(applicationContext, "保存しました。", Toast.LENGTH_LONG).show()

        }
    }
```

一つずつ解説していきます。

まず「①データベース接続オブジェクトを取得」の部分です。

```kotlin
            //データベース接続オブジェクトを取得
            val db = _helper.writableDatabase
```

プロパティとして宣言したデータベースヘルパーオブジェクト「**_helper**」から、データベース接続オブジェクトを取得します。
今回はデータを書き込む必要があるので、書き込み可能な**writableDatabase**を取得します。

続いて「②保存したい画面データを取得」です。
データベースに保存したいデータは、以下の通り。
【画面から取得する項目】
　・レベル：level
　・正解数：correctCount
　・日付：date
　・プレイヤー名：name
【インテントから取得する項目】
　・プレイタイム：timeValue
　※ランキング表示のため、テキスト型に変換した画面の項目ではなく、インテントで渡した数値データを保存します。

```kotlin
            //各種画面データを取得
            val level = findViewById<TextView>(R.id.tvLevel).text
            val correctCount = findViewById<TextView>(R.id.tvCorrectCount).text
            val date = findViewById<TextView>(R.id.tvDate).text
            val name = findViewById<EditText>(R.id.etName).text
            //インテントからタイム（数値）を取得
            val timeValue = intent.getIntExtra("timeValue", 0)
```

画面から取得する項目については、findViewById()で対象のビュー項目を指定して、.textプロパティを取得します。
インテントから取得する項目については、getIntExtra()で取得します。
ここまでは前回までの記事で解説済みなので詳細は割愛します。

続いて「③主キーとなる_idを設定」です。
テーブルの主キーとして定義した**_id**に値を設定していきます。
もう少し良いやり方があったかもしれませんが、これからインサートするレコードの主キーとして、現在のテーブルのレコード件数+1を設定することにしました。
保存した順に、1から順番に主キーが設定されることになります。

resultsテーブルのレコード件数を取得して、recordCountという変数に格納します。

```kotlin
            val sqlcount = "SELECT COUNT(*) FROM results"
            val cursor = db.rawQuery(sqlcount, null)
            var recordCount = ""

            while(cursor.moveToNext()) {
                val idxCount = cursor.getColumnIndex("COUNT(*)")
                recordCount = cursor.getString(idxCount)
            }
```

そしてrecordCountに1を加えた値を**_id**に設定します。

```kotlin
            val _id = recordCount.toInt() +1
```

以上で、データベースに保存するデータの値が一通り揃いました。


④SQL文を生成

```kotlin
           val sqlInsert = "INSERT INTO results (_id, name, level, time, correctcount, date) VALUES (?, ?, ?, ?, ?, ?)"
```

resultテーブルの「_id」「name」「level」「time」「correctcount」「date」各カラムにデータをインサートしていきます。
インサートする値はバインド変数として「?」で記述しています。
この後で実際の値（変数）を指定します。


⑤バインド変数の設定
バインド変数を使う場合、SQL実行のためにはデータベース接続オブジェクトの**compileStatement()**メソッドにSQL文字列を渡す必要があります。戻り値はSQLiteStatementオブジェクト（stmtという変数に格納）です。
**SQLiteStatement()**オブジェクトの、bindxxx()メソッドを使ってSQL文の中の?に変数を埋め込んでいきます。
数値型はbindLong()、文字列型はbindString()を使って変数をセットします。
第1引数は、SQL文の中に出てくる?の順番です。
1：_id
2：name
3：level
4：time
5：correctcount
6：date
です。それぞれテーブルの型に合わせて型変換をしてあげます。

```kotlin
            var stmt = db.compileStatement(sqlInsert)
            stmt.bindLong(1, _id.toLong())
            stmt.bindString(2, name.toString())
            stmt.bindString(3, level.toString())
            stmt.bindLong(4, timeValue.toLong())
            stmt.bindLong(5, correctCount.toString().toLong())
            stmt.bindString(6, date.toString())
```

⑥SQL実行

```kotlin
            stmt.executeInsert()
```

今回はインサート処理になりますので、SQLiteStatementオブジェクトの**executeInsert()**でSQLを実行します。
これでデータベースへのデータ保存ができました。


～～おまけ　Toast表示～～
Toastとは何か処理を実行したときに、画面にほわーんと出て少ししたら消える文字列です。↓でイメージつきますでしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619303/7f82fef7-e1dd-adb7-c5ed-d1a2bea09f71.png)
今回はデータベースへの保存が完了したらこのToastを表示させます。

```kotlin
Toast.makeText(applicationContext, "保存しました。", Toast.LENGTH_LONG).show()
```

第1引数のapplicationContextはお決まりの文言。
第2引数に表示させたいテキストを記述します。
第3引数はToastを表示させる長さです。
LENGTH_SHORTとLENGTH_LONGがあります。今回はちょっと長めのLENGTH_LONGにしました。
.show()メソッドで定義した文字列を画面に表示させます。


今回は以上です。
データベース操作は今回のアプリを作成するにあたって何としても実装したい処理でした。
データを永続化させて、プレイデータをランキング形式で比較したかったので。
今は端末内でのランキングですが、次のステップとしてはデータベースをサーバに持たせて、異なる端末間でもプレイデータを比較できるようにしていきたいと思っています。

次回は「もう一度」「トップへ戻る」ボタンを押した際の処理を実装します。




[①概要](https://qiita.com/tomohiro-yamashita-5664/items/e87f90fef9cf34faf9bf)
[②画面デザイン～トップ画面（Constraint Layout）～](https://qiita.com/tomohiro-yamashita-5664/items/72a529e12f292a8e1538)
[③画面デザイン～ゲーム画面（Linear Layout）～](https://qiita.com/tomohiro-yamashita-5664/items/9674fa7c1d0cc2de1c40)
[④画面デザイン～結果画面（Linear Layoutその2）～](https://qiita.com/tomohiro-yamashita-5664/items/08f99be14d2ac2254634)
[⑤トップ画面からの遷移（インテント(putExtra)）](https://qiita.com/tomohiro-yamashita-5664/items/ae86d6273cfc8c13b482)
[⑥トップ画面から引き継いだデータ表示（インテント(getExtra)）](https://qiita.com/tomohiro-yamashita-5664/items/bf50791fa5355818d7fb)
[⑦問題出題（ロジック実装）](https://qiita.com/tomohiro-yamashita-5664/items/a67eeb3fe9cc63eb1c65)
[⑧回答ボタン押下（効果音再生（MediaPlayer、正誤判定、次の問題出題）](https://qiita.com/tomohiro-yamashita-5664/items/8b032855a9377ba9897e)
[⑨タイムカウンターの実装（handler）](https://qiita.com/tomohiro-yamashita-5664/items/8f5633e68f0519d6841f)
[⑩ゲーム画面から引き継いだゲーム結果表示（インテント）](https://qiita.com/tomohiro-yamashita-5664/items/0e208a3c1a75c5b13376)
[⑪当日日付データ取得](https://qiita.com/tomohiro-yamashita-5664/items/59513080e5d5f419d6af)
[⑫DB保存（SQLite、Insert）（本記事）](https://qiita.com/tomohiro-yamashita-5664/items/5a0d967f2e42aa90ee24)
⑬もう一度、トップ画面へ戻るボタン（インテント）
⑭ランキング表示（SQLite、Select）
⑮実機でのテスト
⑯Google Playで公開