6 月 3 日の「#Android11: The Beta Launch Show」にぜひご参加ください。

SQLite を使用してデータを保存する

データをデータベースに保存する方法は、連絡先情報など、繰り返しデータや構造化データの場合に適しています。このページでは、SQL データベース全般について理解していることを前提として、Android 上で SQLite データベースを利用する方法について説明します。Android 上でデータベースを利用する際に必要となる API は、android.database.sqlite パッケージに含まれています。

注: 上記の API は強力ですが、極めてローレベルであるため、活用できるようになるにはかなりの時間と労力が必要となります。

  • RAW SQL クエリはコンパイル時に検証されません。データグラフに変更があった場合、影響を受ける SQL クエリを手動でアップデートする必要があります。これは、時間がかかり、エラーも発生しやすいプロセスです。
  • SQL クエリとデータ オブジェクトを変換するには、大量のボイラープレート コードを記述する必要があります。

そのため、アプリの SQLite データベース内の情報にアクセスするための抽象化レイヤとして Room 永続ライブラリを使用することを強くおすすめします

スキーマとコントラクトを定義する

SQL データベースの重要な要素の 1 つに、スキーマがあります。スキーマとは、データベースの編成方法に関する正式な宣言です。スキーマは、データベースを作成する際に使用する SQL ステートメントに反映されます。「コントラクト クラス」として知られるコンパニオン クラスを作成すると便利です。コントラクト クラスは、体系的かつ自己文書化的な方法で、スキーマのレイアウトを明示的に指定します。

コントラクト クラスは、URI、テーブル、列の名前を定義する定数のコンテナです。コントラクト クラスを利用すると、同一パッケージ内のさまざまなクラスを横断して同じ定数を使用できます。これにより、1 つの場所で列の名前を変更した場合に、それをコード全体に反映することができます。

コントラクト クラスを編成する方法として、データベース全体にグローバルに適用される定義をクラスのルートレベルに配置することをおすすめします。そして、各テーブルの内部クラスを作成します。各内部クラスは、対応するテーブルの列を列挙します。

注: BaseColumns インターフェースを実装することで、内部クラスは、_ID と呼ばれる主キーフィールドを継承できます。このフィールドは、CursorAdapter など、一部の Android クラスに含まれています。このフィールドは必須でありませんが、データベースが Android フレームワークと調和して動作するうえで役立ちます。

たとえば、次のコントラクトは、RSS フィードを示す単一テーブルのテーブル名と列名を定義しています。

Kotlin

    object FeedReaderContract {
        // Table contents are grouped together in an anonymous object.
        object FeedEntry : BaseColumns {
            const val TABLE_NAME = "entry"
            const val COLUMN_NAME_TITLE = "title"
            const val COLUMN_NAME_SUBTITLE = "subtitle"
        }
    }
    

Java

    public final class FeedReaderContract {
        // To prevent someone from accidentally instantiating the contract class,
        // make the constructor private.
        private FeedReaderContract() {}

        /* Inner class that defines the table contents */
        public static class FeedEntry implements BaseColumns {
            public static final String TABLE_NAME = "entry";
            public static final String COLUMN_NAME_TITLE = "title";
            public static final String COLUMN_NAME_SUBTITLE = "subtitle";
        }
    }
    

SQL ヘルパーを使用してデータベースを作成する

データベースの概要を定義したら、データベースとテーブルを作成、管理するためのメソッドを実装する必要があります。テーブルの作成と削除を行う典型的なステートメントは次のとおりです。

Kotlin

    private const val SQL_CREATE_ENTRIES =
            "CREATE TABLE ${FeedEntry.TABLE_NAME} (" +
                    "${BaseColumns._ID} INTEGER PRIMARY KEY," +
                    "${FeedEntry.COLUMN_NAME_TITLE} TEXT," +
                    "${FeedEntry.COLUMN_NAME_SUBTITLE} TEXT)"

    private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${FeedEntry.TABLE_NAME}"
    

Java

    private static final String SQL_CREATE_ENTRIES =
        "CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
        FeedEntry._ID + " INTEGER PRIMARY KEY," +
        FeedEntry.COLUMN_NAME_TITLE + " TEXT," +
        FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)";

    private static final String SQL_DELETE_ENTRIES =
        "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;
    

デバイスの内部ストレージに保存したファイルと同様、Android では、データベースは各アプリ専用のプライベート フォルダに保存されます。デフォルトでは、他のアプリやユーザーはこの領域にアクセスできないため、データの安全性が確保されます。

SQLiteOpenHelper クラスには、データベースを管理するうえで役立つ API のセットが用意されています。このクラスを使用してデータベースへの参照を取得すると、データベースの作成と更新という時間のかかる可能性の高い処理が実行されます。この処理は、アプリの起動時には実行されず、必要になったときにはじめて実行されます。getWritableDatabase() または getReadableDatabase() を呼び出すだけで実行されます。

注: 時間がかかる可能性があるため、getWritableDatabase()getReadableDatabase() の呼び出しは、AsyncTaskIntentService などを使用してバックグラウンド スレッドで行うようにしてください。

SQLiteOpenHelper を使用するには、onCreate()onUpgrade() のコールバック メソッドをオーバーライドするサブクラスを作成します。onDowngrade() メソッドや onOpen() メソッドを実装することもできますが、必須ではありません。

たとえば、上記のコマンドを一部使用した SQLiteOpenHelper の実装方法は以下のとおりです。

Kotlin

    class FeedReaderDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
        override fun onCreate(db: SQLiteDatabase) {
            db.execSQL(SQL_CREATE_ENTRIES)
        }
        override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
            // This database is only a cache for online data, so its upgrade policy is
            // to simply to discard the data and start over
            db.execSQL(SQL_DELETE_ENTRIES)
            onCreate(db)
        }
        override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
            onUpgrade(db, oldVersion, newVersion)
        }
        companion object {
            // If you change the database schema, you must increment the database version.
            const val DATABASE_VERSION = 1
            const val DATABASE_NAME = "FeedReader.db"
        }
    }
    

Java

    public class FeedReaderDbHelper extends SQLiteOpenHelper {
        // If you change the database schema, you must increment the database version.
        public static final int DATABASE_VERSION = 1;
        public static final String DATABASE_NAME = "FeedReader.db";

        public FeedReaderDbHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(SQL_CREATE_ENTRIES);
        }
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            // This database is only a cache for online data, so its upgrade policy is
            // to simply to discard the data and start over
            db.execSQL(SQL_DELETE_ENTRIES);
            onCreate(db);
        }
        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            onUpgrade(db, oldVersion, newVersion);
        }
    }
    

データベースにアクセスするには、SQLiteOpenHelper のサブクラスをインスタンス化します。

Kotlin

    val dbHelper = FeedReaderDbHelper(context)
    

Java

    FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());
    

情報をデータベースに格納する

データベースにデータを挿入するには、ContentValues オブジェクトを insert() メソッドに渡します。

Kotlin

    // Gets the data repository in write mode
    val db = dbHelper.writableDatabase

    // Create a new map of values, where column names are the keys
    val values = ContentValues().apply {
        put(FeedEntry.COLUMN_NAME_TITLE, title)
        put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle)
    }

    // Insert the new row, returning the primary key value of the new row
    val newRowId = db?.insert(FeedEntry.TABLE_NAME, null, values)
    

Java

    // Gets the data repository in write mode
    SQLiteDatabase db = dbHelper.getWritableDatabase();

    // Create a new map of values, where column names are the keys
    ContentValues values = new ContentValues();
    values.put(FeedEntry.COLUMN_NAME_TITLE, title);
    values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle);

    // Insert the new row, returning the primary key value of the new row
    long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);
    

insert() の最初の引数はテーブル名を示します。

2 番目の引数は、ContentValues が空の場合(値を put しなかった場合)にフレームワークが実行する内容を示します。列の名前を指定すると、フレームワークは行を挿入し、その列の値を null に設定します。上記のサンプルコードのように null を指定した場合、フレームワークは、値がないときには行を挿入しません。

insert() メソッドは、新しく作成された行の ID を返します。データの挿入中にエラーが発生した場合は -1 を返します。このエラーは、データベース内の既存のデータと競合している場合に発生します。

データベースから情報を読み取る

データベースからデータを読み取るには、query() メソッドを使用して、選択基準と目的の列を渡します。このメソッドは、insert()update() の要素を結合したものです。ただし、列リストは、挿入するデータではなく、取得するデータ(射影)を定義しています。クエリの結果は Cursor オブジェクトに返されます。

Kotlin

    val db = dbHelper.readableDatabase

    // Define a projection that specifies which columns from the database
    // you will actually use after this query.
    val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE)

    // Filter results WHERE "title" = 'My Title'
    val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?"
    val selectionArgs = arrayOf("My Title")

    // How you want the results sorted in the resulting Cursor
    val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC"

    val cursor = db.query(
            FeedEntry.TABLE_NAME,   // The table to query
            projection,             // The array of columns to return (pass null to get all)
            selection,              // The columns for the WHERE clause
            selectionArgs,          // The values for the WHERE clause
            null,                   // don't group the rows
            null,                   // don't filter by row groups
            sortOrder               // The sort order
    )
    

Java

    SQLiteDatabase db = dbHelper.getReadableDatabase();

    // Define a projection that specifies which columns from the database
    // you will actually use after this query.
    String[] projection = {
        BaseColumns._ID,
        FeedEntry.COLUMN_NAME_TITLE,
        FeedEntry.COLUMN_NAME_SUBTITLE
        };

    // Filter results WHERE "title" = 'My Title'
    String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
    String[] selectionArgs = { "My Title" };

    // How you want the results sorted in the resulting Cursor
    String sortOrder =
        FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

    Cursor cursor = db.query(
        FeedEntry.TABLE_NAME,   // The table to query
        projection,             // The array of columns to return (pass null to get all)
        selection,              // The columns for the WHERE clause
        selectionArgs,          // The values for the WHERE clause
        null,                   // don't group the rows
        null,                   // don't filter by row groups
        sortOrder               // The sort order
        );
    

3 番目と 4 番目の引数(selectionselectionArgs)を組み合わせて WHERE 句を作成しています。引数は選択クエリとは別に提供されるため、組み合わせる前にエスケープされます。これにより、選択ステートメントが SQL インジェクションの影響を受けなくなります。すべての引数の詳細については、query() リファレンスをご覧ください。

カーソル内の行を参照するには、Cursor 移動メソッドのいずれかを使用します。このメソッドは常に、値の読み取りを開始する前に呼び出す必要があります。カーソルは -1 の位置から始まり、moveToNext() を呼び出すと、結果内の最初のエントリに「読み取り位置」が配置され、カーソルが結果セットの最後のエントリを通り過ぎたかどうかが返されます。各行に対して、Cursor 取得メソッドのいずれか(getString()getLong() など)を呼び出すことで、列の値を読み取ることができます。各取得メソッドに対して、目的の列のインデックス位置を渡す必要があります。インデックス位置は、getColumnIndex()getColumnIndexOrThrow() を呼び出すことで取得できます。結果全体に対して反復処理が完了したら、カーソルに対して close() を呼び出して、リソースを解放します。たとえば、カーソル内に格納されているすべてのアイテム ID を取得してリストに追加する方法を以下に示します。

Kotlin

    val itemIds = mutableListOf<Long>()
    with(cursor) {
        while (moveToNext()) {
            val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID))
            itemIds.add(itemId)
        }
    }
    

Java

    List itemIds = new ArrayList<>();
    while(cursor.moveToNext()) {
      long itemId = cursor.getLong(
          cursor.getColumnIndexOrThrow(FeedEntry._ID));
      itemIds.add(itemId);
    }
    cursor.close();
    

データベースから情報を削除する

テーブルから行を削除するには、delete() メソッドに対して、削除対象の行を識別する選択基準を指定する必要があります。このメカニズムは、query() メソッドの選択引数と同じように機能します。このメカニズムにより、選択指定は、選択句と選択引数に分割されます。選択句は、参照する列を定義します。また、選択句により、列テストを組み合わせることができます。選択引数はテスト対象となる値であり、選択句にバインドされます。結果は、通常の SQL ステートメントとは扱いが異なり、SQL インジェクションの影響を受けません。

Kotlin

    // Define 'where' part of query.
    val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
    // Specify arguments in placeholder order.
    val selectionArgs = arrayOf("MyTitle")
    // Issue SQL statement.
    val deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs)
    

Java

    // Define 'where' part of query.
    String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
    // Specify arguments in placeholder order.
    String[] selectionArgs = { "MyTitle" };
    // Issue SQL statement.
    int deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);
    

delete() メソッドの戻り値は、データベースから削除された行の数を示します。

データベースを更新する

データベース値のサブセットを編集する必要がある場合は、update() メソッドを使用します。

テーブルを更新すると、insert()ContentValues 構文と、delete()WHERE 構文が結合されます。

Kotlin

    val db = dbHelper.writableDatabase

    // New value for one column
    val title = "MyNewTitle"
    val values = ContentValues().apply {
        put(FeedEntry.COLUMN_NAME_TITLE, title)
    }

    // Which row to update, based on the title
    val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?"
    val selectionArgs = arrayOf("MyOldTitle")
    val count = db.update(
            FeedEntry.TABLE_NAME,
            values,
            selection,
            selectionArgs)
    

Java

    SQLiteDatabase db = dbHelper.getWritableDatabase();

    // New value for one column
    String title = "MyNewTitle";
    ContentValues values = new ContentValues();
    values.put(FeedEntry.COLUMN_NAME_TITLE, title);

    // Which row to update, based on the title
    String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?";
    String[] selectionArgs = { "MyOldTitle" };

    int count = db.update(
        FeedReaderDbHelper.FeedEntry.TABLE_NAME,
        values,
        selection,
        selectionArgs);
    

update() メソッドの戻り値は、データベース内で影響を受けた行の数を示します。

データベース接続を永続化する

getWritableDatabase()getReadableDatabase() は、データベースを閉じているときに呼び出すと高コストになるため、アクセスする必要性がある限り、データベース接続は開いたままにしておくことをおすすめします。一般的に、データベースを閉じるのは、呼び出しアクティビティの onDestroy() の際に行うのが最適です。

Kotlin

    override fun onDestroy() {
        dbHelper.close()
        super.onDestroy()
    }
    

Java

    @Override
    protected void onDestroy() {
        dbHelper.close();
        super.onDestroy();
    }
    

データベースをデバッグする

Android SDK には、sqlite3 シェルツールが含まれており、SQLite データベースに対して、テーブル コンテンツを閲覧したり、SQL コマンドを実行したり、その他便利な関数を実行したりすることができます。詳細については、シェルコマンドを発行するをご覧ください。