データを保存、検索する

データを保存する場合、オンライン データベースや、ローカル SQLite データベース、さらにはテキスト ファイルなど、さまざまな方法があります。アプリに最適なソリューションを判断するのはデベロッパーです。このレッスンでは、強力な全文検索を提供できる SQLite 仮想テーブルを作成する方法について紹介します。ここでは、ファイル内の各行に単語と定義のペアが指定されたテキスト ファイルからデータを仮想テーブルに入力します。

仮想テーブルを作成する

仮想テーブルは SQLite テーブルと同様に動作しますが、メモリ内でのオブジェクトの読み書きには、データベース ファイルではなく、コールバックを使用します。仮想テーブルを作成するには、そのテーブルのクラスを作成します。

Kotlin

    class DatabaseTable(context: Context) {

        private val databaseOpenHelper = DatabaseOpenHelper(context)

    }
    

Java

    public class DatabaseTable {
        private final DatabaseOpenHelper databaseOpenHelper;

        public DatabaseTable(Context context) {
            databaseOpenHelper = new DatabaseOpenHelper(context);
        }
    }
    

DatabaseTable 内に SQLiteOpenHelper を拡張する内部クラスを作成します。SQLiteOpenHelper クラスは、抽象メソッドを定義しています。それをオーバーライドして、データベース テーブルを作成し、必要に応じてアップグレードします。例として、辞書アプリ用の単語を格納するデータベース テーブルを宣言するサンプルコードを以下に示します。

Kotlin

    private const val TAG = "DictionaryDatabase"

    //The columns we'll include in the dictionary table
    const val COL_WORD = "WORD"
    const val COL_DEFINITION = "DEFINITION"

    private const val DATABASE_NAME = "DICTIONARY"
    private const val FTS_VIRTUAL_TABLE = "FTS"
    private const val DATABASE_VERSION = 1

    private const val FTS_TABLE_CREATE =
            "CREATE VIRTUAL TABLE $FTS_VIRTUAL_TABLE USING fts3 ($COL_WORD, $COL_DEFINITION)"

    class DatabaseTable(context: Context) {

        private val databaseOpenHelper: DatabaseOpenHelper

        init {
            databaseOpenHelper = DatabaseOpenHelper(context)
        }

        private class DatabaseOpenHelper internal constructor(private val helperContext: Context) :
                SQLiteOpenHelper(helperContext, DATABASE_NAME, null, DATABASE_VERSION) {
            private lateinit var mDatabase: SQLiteDatabase

            override fun onCreate(db: SQLiteDatabase) {
                mDatabase = db
                mDatabase.execSQL(FTS_TABLE_CREATE)
            }

            override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
                Log.w(
                        TAG,
                        "Upgrading database from version $oldVersion to $newVersion , which will " +
                                "destroy all old data"
                )

                db.execSQL("DROP TABLE IF EXISTS $FTS_VIRTUAL_TABLE")
                onCreate(db)
            }

        }
    }
    

Java

    public class DatabaseTable {

        private static final String TAG = "DictionaryDatabase";

        //The columns we'll include in the dictionary table
        public static final String COL_WORD = "WORD";
        public static final String COL_DEFINITION = "DEFINITION";

        private static final String DATABASE_NAME = "DICTIONARY";
        private static final String FTS_VIRTUAL_TABLE = "FTS";
        private static final int DATABASE_VERSION = 1;

        private final DatabaseOpenHelper databaseOpenHelper;

        public DatabaseTable(Context context) {
            databaseOpenHelper = new DatabaseOpenHelper(context);
        }

        private static class DatabaseOpenHelper extends SQLiteOpenHelper {

            private final Context helperContext;
            private SQLiteDatabase mDatabase;

            private static final String FTS_TABLE_CREATE =
                        "CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE +
                        " USING fts3 (" +
                        COL_WORD + ", " +
                        COL_DEFINITION + ")";

            DatabaseOpenHelper(Context context) {
                super(context, DATABASE_NAME, null, DATABASE_VERSION);
                helperContext = context;
            }

            @Override
            public void onCreate(SQLiteDatabase db) {
                mDatabase = db;
                mDatabase.execSQL(FTS_TABLE_CREATE);
            }

            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
                        + newVersion + ", which will destroy all old data");
                db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE);
                onCreate(db);
            }
        }
    }
    

仮想テーブルにデータを入力する

次に、テーブルに保存するデータが必要になります。単語とその定義を格納するテキスト ファイル(res/raw/definitions.txt 内にあります)を読み取る方法、そのファイルを解析する方法、ファイル内の各行を仮想テーブル内の行として挿入する方法を下記のサンプルコードに示します。UI がロックされるのを防ぐため、この処理はすべて別スレッド上で実行します。DatabaseOpenHelper 内部クラスに下記のコードを追加します。

ヒント: このスレッドの完了を UI アクティビティに通知するコールバックをセットアップすることもできます。

Kotlin

    private fun loadDictionary() {
        Thread(Runnable {
            try {
                loadWords()
            } catch (e: IOException) {
                throw RuntimeException(e)
            }
        }).start()
    }

    @Throws(IOException::class)
    private fun loadWords() {
        val inputStream = helperContext.resources.openRawResource(R.raw.definitions)

        BufferedReader(InputStreamReader(inputStream)).use { reader ->
            var line: String? = reader.readLine()
            while (line != null) {
                val strings: List<String> = line.split("-").map { it.trim() }
                if (strings.size < 2) continue
                val id = addWord(strings[0], strings[1])
                if (id < 0) {
                    Log.e(TAG, "unable to add word: ${strings[0]}")
                }
                line = reader.readLine()
            }
        }
    }

    fun addWord(word: String, definition: String): Long {
        val initialValues = ContentValues().apply {
            put(COL_WORD, word)
            put(COL_DEFINITION, definition)
        }

        return database.insert(FTS_VIRTUAL_TABLE, null, initialValues)
    }
    

Java

    private void loadDictionary() {
            new Thread(new Runnable() {
                public void run() {
                    try {
                        loadWords();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }).start();
        }

    private void loadWords() throws IOException {
        final Resources resources = helperContext.getResources();
        InputStream inputStream = resources.openRawResource(R.raw.definitions);
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

        try {
            String line;
            while ((line = reader.readLine()) != null) {
                String[] strings = TextUtils.split(line, "-");
                if (strings.length < 2) continue;
                long id = addWord(strings[0].trim(), strings[1].trim());
                if (id < 0) {
                    Log.e(TAG, "unable to add word: " + strings[0].trim());
                }
            }
        } finally {
            reader.close();
        }
    }

    public long addWord(String word, String definition) {
        ContentValues initialValues = new ContentValues();
        initialValues.put(COL_WORD, word);
        initialValues.put(COL_DEFINITION, definition);

        return database.insert(FTS_VIRTUAL_TABLE, null, initialValues);
    }
    

適宜 loadDictionary() メソッドを呼び出して、テーブルにデータを入力します。適切な場所と考えられるのは、テーブルを作成した直後の DatabaseOpenHelper クラスの onCreate() メソッド内です。

Kotlin

    override fun onCreate(db: SQLiteDatabase) {
        database = db
        database.execSQL(FTS_TABLE_CREATE)
        loadDictionary()
    }
    

Java

    @Override
    public void onCreate(SQLiteDatabase db) {
        database = db;
        database.execSQL(FTS_TABLE_CREATE);
        loadDictionary();
    }
    

仮想テーブルを作成してデータを入力したら、SearchView が提供するクエリを使用してデータを検索します。DatabaseTable クラスに下記のメソッドを追加して、クエリを検索する SQL ステートメントを作成します。

Kotlin

    fun getWordMatches(query: String, columns: Array<String>?): Cursor? {
        val selection = "$COL_WORD MATCH ?"
        val selectionArgs = arrayOf("$query*")

        return query(selection, selectionArgs, columns)
    }

    private fun query(
            selection: String,
            selectionArgs: Array<String>,
            columns: Array<String>?
    ): Cursor? {
        val cursor: Cursor? = SQLiteQueryBuilder().run {
            tables = FTS_VIRTUAL_TABLE
            query(databaseOpenHelper.readableDatabase,
                    columns, selection, selectionArgs, null, null, null)
        }

        return cursor?.run {
            if (!moveToFirst()) {
                close()
                null
            } else {
                this
            }
        } ?: null
    }
    

Java

    public Cursor getWordMatches(String query, String[] columns) {
        String selection = COL_WORD + " MATCH ?";
        String[] selectionArgs = new String[] {query+"*"};

        return query(selection, selectionArgs, columns);
    }

    private Cursor query(String selection, String[] selectionArgs, String[] columns) {
        SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
        builder.setTables(FTS_VIRTUAL_TABLE);

        Cursor cursor = builder.query(databaseOpenHelper.getReadableDatabase(),
                columns, selection, selectionArgs, null, null, null);

        if (cursor == null) {
            return null;
        } else if (!cursor.moveToFirst()) {
            cursor.close();
            return null;
        }
        return cursor;
    }
    

getWordMatches() を呼び出すことで、クエリを検索します。適合する結果はすべて Cursor 内に返されます。これを反復処理したり、利用して ListView を作成したりすることができます。この例では、検索アクティビティの handleIntent() メソッド内で getWordMatches() を呼び出します。以前作成したインテント フィルタに基づいて、この検索アクティビティは、ACTION_SEARCH インテント内のクエリをエクストラとして受け取ります。

Kotlin

    private val db = DatabaseTable(this)

    ...

    private fun handleIntent(intent: Intent) {

        if (Intent.ACTION_SEARCH == intent.action) {
            val query = intent.getStringExtra(SearchManager.QUERY)
            val c = db.getWordMatches(query, null)
            //process Cursor and display results
        }
    }
    

Java

    DatabaseTable db = new DatabaseTable(this);

    ...

    private void handleIntent(Intent intent) {

        if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
            String query = intent.getStringExtra(SearchManager.QUERY);
            Cursor c = db.getWordMatches(query, null);
            //process Cursor and display results
        }
    }