Comments
Description
Transcript
ストレージ - commucom.jp(コミュコム)
第 11 章 ストレージ 著:深見浩和 11-1 ストレージ(1) 著:深見浩和 KEYWORD LESSON SharedPreferences 本章では、アプリのデータを端末内に保存したり、読み込 んだりする方法を学びます。本節では、その中でもSharedP referencesやファイルの扱い方を学びます。 Key-Value形式 File ストリーム 内部ストレージ 外部ストレージ READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE この節を学ぶとできること SharedPreferencesにデータを 読み書きできるようになる アプリの設定情報など、簡単なデータの読み 書きができるようになります。 ファイルの読み書きができるようになる 画像など、やや大きめのデータをファイルに保存したり、ファイル から読み込んだりできるようになります。 6 11 -1-1 ストレージについての理解を深めよう 第 11 章 ストレージとは、データを永続的に記録する装置です。PCではハードディスクや ス ト レ ー ジ SSDがストレージに該当します。Android端末にもストレージはあり、アプリはAPIを 介してストレージにデータを記録したりできます。この章では、Androidアプリ開発で 主に用いられる3つのストレージAPIについて学びます。 ストレージとメモリ まず、データの記録について学びましょう。これまでのアプリ開発で使用した「変 数」や「フィールド」は一時的なデータを記録するための仕組みです。変数やフィー ルドに保存されたデータは、 アプリ実行中、 メモリ上に記録されるため、 アプリを終了 させたり端末の電源を切ったりすると、記録した内容は失われてしまいます。 これに対し、 ストレージに記録されたデータは、 アプリを終了させたり、端末の電源 を切っても失われることはありません。 (ただし、経年劣化などで物理的に壊れたりす ると、 データが失われることがあります)。 ファイル ストレージを扱う時の基本単位となるデータのまとまりを「ファイル」 と呼びます。画 像データが保存されているファイルや、Javaソースコードが保存されているファイルな ど、既になじみ深いものだと思います。PCのハードディスクやSSDなどへの読み書き がファイル単位で行われるのと同様に、Androidでもストレージへの読み書きはファイ ル単位で行われます。 ファイルシステム ハードディスクやSSDの記録装置には、ファイルを「0」 「1」 といったデジタルデー タに分解して書き込みを行います。この状態では、OSやアプリからの使い勝手がよ Androidでは2.2以前ではext3が使 用されていました くありません。たくさんのデジタルデータをまとめたファイル単位で記録したり、読み込 めるようになっていたほうが好都合です。そのためのシステムを「ファイルシステム」 と 呼んでいます。ファイルシステムはOSによって異なり、Windowsでは「NTFS」。 Linuxでは「ext3」 「ext4」など、いろいろな種類があります。Androidでは 「ext4」が使用されています。 Androidアプリ開発で主に利用するストレージAPI Androidでは、ストレージに対してデータを作成したり、読み込んだりするための 7 APIや仕組みがいくつか用意されています。代表的なものは次の3つです。 ・ SharedPreferences ・ File ・ SQLiteDatabase 本章では、 この3つの代表的なAPIの使い方を学びます。 Androidには、 そのほかにも以下のように開発したアプリのデータを他アプリに提 供したり、他アプリ (たとえば電話帳など) からデータを取得するための仕組みも用 意されています。 ・ ContentProvider ・ Storage Access Framework(Android 4.4で追加) 11 -1-2 設定などの情報を記録する「SharedPreferences」 「SharedPreferences」 とは、アプリの設定情報などを保存する仕組みです。 SharedPreferencesは、データを「Key-Value」形式で保存します。KeyValue形式とは、データを「キー」 と 「値」のペアで保存する形式で、身近なものだ と辞書がこれに該当します。 SharedPreferencesに保存可能なデータの種類 SharedPreferencesに値として保存可能なデータの種類は次の6種類です。 ・ String ・ int ・ long ・ float ・ boolean ・ Set<String> SharedPreferencesにデータを保存する SharedPreferencesにデータを保存するには、次の4ステップを行います。 8 ・ 「Context(Activity)」の「getSharedPreferences()」で「SharedPreferenc 第 11 es」 オブジェクトを取得する 章 ・ 「SharedPreferences」 オブジェクトの「edit()で、Editor」 オブジェクトを取得する ス ト レ ー ジ ・保存するデータを 「Editor」 オブジェクトの「put」メソッドでセットする ・ 「apply()」 を呼び出して変更を反映させる SharedPreferencesにデータを保存する private static final String KEY_NAME = "name"; private static final String KEY_AGE = "age"; SharedPreferences pref = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor edit = pref.edit(); edit.putString(KEY_NAME, name); edit.putString(KEY_AGE, age); edit.apply(); SharedPreferencesオブジェクトを取得するには「Context(Activity)」の 「getSharedPreferences()」を呼びます。第1引数には、自分で決めたShared Preferences名を指定します。第2引数は「Context.MODE_PRIVATE」を指 定します。 次に、SharedPreferencesオブジェクトの「edit()」で、 データを書き込むための オブジェクトを取得します。このオブジェクトには「putString()」や「putInt()」など、 値の種類に応じたメソッドが用意されています。ここでは、入力された文字列を 「name」 というキーに対する値として保存しています。保存したいデータが複数ある 場合は「putXXX()」をデータの数だけ呼びます。また、同じキーを指定した場合、 値は上書きされます。 最後に「apply()」を呼ぶことでデータの変更を反映させます。これを呼び忘れる とデータは保存されないので注意しましょう。 SharedPreferencesからデータを読み込む SharedPreferencesからデータを読み込むには、次の2ステップを行います。 ・ 「Context(Activity)」の「getSharedPreferences()」でSharedPreferencesオ ブジェクトを取得する ・保存したデータをSharedPreferencesオブジェクトの「get」メソッドで取得する SharedPreferencesからデータを読み込む SharedPreferences pref = getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); String name = pref.getString(KEY_NAME, DEFAULT_NAME); 9 SharedPreferencesオブジェクトの取得は書き込み時と同様です。値を読み 込むには、値の型に応じた「getXXX()」を呼びます。ここでは保存した名前を文 字列として取得するため、SharedPreferencesオブジェクトの「getString()」を 呼んでいます。第1引数で指定したキーに対応する値が無かったり、型が違ったり した場合、getString()は第2引数で指定した値を代わりに返します。 11 -1-3 SharedPreferences が苦手とすること SharedPreferencesはデータの読み書きが数行で実現できる反面、苦手とす ることがいくつかあります。 データの検索には向かない SharedPreferencesでデータを読み込むには、キーを事前に知っておく必要 があります。なので、 「年齢が20であるデータを全部取得したい」 というような、検索 を必要とするデータの保存には向きません。 複数プロセスで読み書きすると予想外の挙動をすることが ある 残念なことに、SharedPreferencesは複数プロセスから読み書きすると、 「アプリ で保存したはずの値がServiceで読み込めない」 といった挙動をすることがありま す。問題となるパターンは基礎の範囲を超えるため、 ここでは紹介しません。ですが、 「問題がある」 ということは頭の片隅に置いておきましょう。 練習問題 図1のように「TextView」 「EditText」 「Button」を1つずつ配置してくださ い。そして、次のような動作をするよう、SharedPreferencesを用いて処理を記述 し動作確認しましょう。 ・ Buttonをタップすると、EditTextに入力された内容をSharedPreferencesを用 いて保存する ・ アプリ起動時に、SharedPreferencesに値が保存されていれば図2のようにTex tViewの文字列をセットする 10 第 11 章 ス ト レ ー ジ 図1:練習問題レイアウト 図2:保存した名前を表示 11 -1-4 ファイルを読み書きする方法を知ろう Androidアプリがファイルを保存できる場所は、次の2箇所です。 ・アプリの内部に指定されたストレージ ・アプリの外部にあるストレージ アプリの内部ストレージに保存したファイルは、他のアプリは読み書きできません。他 のアプリと連携する必要のないデータの保存などに向きます。一方、アプリの外部スト レージに保存したファイルは、他のアプリから読み書きできます。一般的には、外部スト レージはmicroSDのような本体内蔵ではないストレージを指します。ただし、 ここでいう 内部/外部ストレージは、そうしたハードウェアからの視点ではなく、アプリの内側か 外側かという意味でハードウェアとは関係がありません。保存場所を内部にするか、 外部にするかは用途に応じて使い分けます。 11 ファイル(File)とストリーム(Stream) ファイル(File)とストリーム(Stream)は、Androidアプリでファイルの読み書き時に 使用するクラスです。Javaで既にこれらの使い方を学んでいる方は、この節を読み 飛ばしてもかまいません。 ファイル ディスク上のファイルやフォルダー (ディレクトリ) を表す ストリーム データの流れや、流れ方を表す ストリームについては、さらに2つの呼び方があります。流れてきたデータを読み込 むストリームを「InputStream」 と呼び、データを流すためのストリームを「OutputS tream」 と呼びます。 ImputStreamとOutputStreamのイメージ 内部ストレージにファイルを作成する 内部ストレージにファイルを作成するには、次の3ステップを行います。 ・「Context」の「openFileOutput()」 を呼び「OutputStream」 オブジェクトを取 得する ・ 取得した「OutputStream」 オブジェクトに対し、 データを書き込む ・「OutputStream」 オブジェクトの「close()」 を呼び、処理の終了を伝える 12 内部ストレージにファイルを作成する 第 11 public class MainActivity extends ActionBarActivity { 章 // 中略 ス ト レ ー ジ void internalSaveClicked() { OutputStream out = null; OutputStreamWriter writer = null; BufferedWriter bw = null; try { out = openFileOutput("myText.txt", Context.MODE_PRIVATE); writer = new OutputStreamWriter(out); bw = new BufferedWriter(writer); String text = mEdit.getText().toString(); bw.write(text); Toast.makeText(this, R.string.save_done, Toast.LENGTH_SHORT).show(); } catch (IOException e) { Log.e("Internal", "IO Exception " + e.getMessage(), e); } finally { try { if (bw != null) { bw.close(); } if (writer != null) { writer.close(); } if (out != null) { out.close(); } } catch (IOException e) { Log.e("Internal", "IO Exception " + e.getMessage(), e); } } } } まず「openFileOutput()」を呼び「OutputStream」オブジェクトを取得します。 第1引数にはファイル名を指定します。第2引数には「Context.MODE_PRIVA TE」を指定します。 続いて「OutputStreamWriter」 オブジェクトと 「BufferedWriter」オブジェク トを生成します。 「OutputStream」オブジェクトを用いて直接データを書き込んでも よいのですが、OutputStreamクラスには文字列を効率的に書き込むためのメソッ ドが用意されていないので、 このようにBufferedWriterオブジェクトを用意します。 BufferedWriterオブジェクトの生成まで完了したら、後は「write()」でストリー ムに文字列を書き込みます。 ファイルのオープンや書き込みは「IOException」が発生することがあります。そ のため、処理全体を「try」∼「catch」で囲みます。また、処理途中でIOExcept ionが発生した時も確実に各ストリームを閉じる必要があるため「finally」の部分で 「close()」を呼びます。 13 内部ストレージ内のファイルを読み込む 内部ストレージ内のファイルを読み込むには、次の3ステップを行います。 ・ ContextのopenFileInput()を呼び、InputStreamオブジェクトを取得する ・ 取得したInputStreamオブジェクトから、 データを読み込む ・ InputStreamオブジェクトのclose()を呼び、処理の終了を伝える まずは、内部ストレージのファイルを読み込みます。 内部ストレージ内のファイルを読み込む public class MainActivity extends ActionBarActivity { // 中略 void internalLoadClicked() { InputStream in = null; InputStreamReader sr = null; BufferedReader br = null; try { in = openFileInput("myText.txt"); sr = new InputStreamReader(in); br = new BufferedReader(sr); String line; StringBuilder sb = new StringBuilder(); while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } mEdit.setText(sb.toString()); Toast.makeText(this, R.string.load_done, Toast.LENGTH_SHORT).show(); } catch (IOException e) { Log.e("Internal", "IO Exception " + e.getMessage(), e); } finally { try { if (br != null) { br.close(); } if (sr != null) { sr.close(); } if (in != null) { in.close(); } } catch (IOException e) { Log.e("Internal", "IO Exception " + e.getMessage(), e); } } } } 「openFileInput()」を呼び「InputStream」オブジェクトを取得します。第1引 数にはファイル名を指定します。次に「InputStreamReader」 と 「BufferedRea der」オブジェクトを生成します。ファイルの書き込みと同様、InputStreamクラスに は文字列を効率的に読み込むためのメソッドが用意されていないので、Buffered 14 Readerオブジェクトを用意します。 第 11 BufferedReaderオブジェクトの生成まで完了したら 「readLine()」を用いて1 章 行ずつ読み込みます。 ファイルの書き込みと同様に、ファイルのオープンや読み込みは「IOException」 が発生することがあります。そのため、処理全体を「try」∼「catch」で囲みます。ま た、処理途中でIOExceptionが発生した時も確実に各ストリームを閉じる必要が IOExceptionとは、実行時、入出力 に関するエラーが発生した時に投げ られる例外です。たとえば、存在しな いファイルを読み込み用でオープンし ようとした時に発生します。 あるため「finally」の部分で「close()」を呼びます。 外部ストレージにファイルを作成する 外部ストレージにファイルを作成するには、次の4ステップを行います。 ・ Fileオブジェクトを作成する ・ 作成したFileオブジェクトを元に、FileOutputStreamオブジェクトを作成する ・ 作成したFileOutputStreamオブジェクトに対し、 データを書き込む ・ OutputStreamオブジェクトのclose()を呼び、処理の終了を伝える 「OutputStream」 オブジェクトを作成した後の処理は内部ストレージへの書き込 みと同様なので、 ここではOutputStreamオブジェクトを作成するまでを解説します。 それでは、外部ストレージにファイルを作成してみましょう。 外部ストレージにファイルを作成する public class MainActivity extends ActionBarActivity { // 中略 void externalSaveClicked() { OutputStream out = null; OutputStreamWriter writer = null; BufferedWriter bw = null; try { File foler = Environment.getExternalStoragePublicDirectory( "MyDocuments"); if (!foler.exists()) { boolean result = foler.mkdir(); if (!result) { return; } } File file = new File(foler, "myText.txt"); out = new FileOutputStream(file); // 以下、内部ストレージへの書き込みと同様 } } } 15 ス ト レ ー ジ まず、保存先のフォルダーを表すオブジェクトを「Environment.getExternalS toragePublicDirectory()」で取得します。引数にはフォルダーの種類を文字列 で指定します。ここでは独自に決めた「MyDocuments」 という種類を指定していま すが、 システムで用意されている定数を指定することもできます。 Environment.DIRECTORY_ALARMS Environment.DIRECTORY_PICTURES Environment.DIRECTORY_MUSIC Environment.DIRECTORY_MOVIES 次に、フォルダーが実際に存在するかを「exists()」で確認します。もし存在しな い場合は「mkdir()」でフォルダーを作成します。 フォルダーの作成まで完了したら 「new」で保存先となるFileオブジェクトを作成 します。第1引数には先ほど作成したフォルダーオブジェクトを指定します。第2引数 にはファイル名を指定します。 ファイルオブジェクト作成後、newで「FileOutputStream」オブジェクトを作成 します。引数には保存先となるFileオブジェクトを指定します。 外部ストレージ内のファイルを読み込む 外部ストレージ内のファイルを読み込むには、次の4ステップを行います。 ・ Fileオブジェクトを作成する ・ 作成したFileオブジェクトを元に、FileInputStreamオブジェクトを作成する ・ 取得したFileInputStreamオブジェクトから、 データを読み込む ・「InputStream」 オブジェクトの「close()」 を呼び、処理の終了を伝える。 「InputStream」オブジェクトを作成した後の処理は内部ストレージ内の読み込 みと同様なので、 ここではInputStreamオブジェクトを作成するまでを解説します。 16 外部ストレージ内のファイルを読み込む 第 11 public class MainActivity extends ActionBarActivity { 章 // 中略 ス ト レ ー ジ void externalLoadClicked() { InputStream in = null; InputStreamReader sr = null; BufferedReader br = null; try { File foler = Environment.getExternalStoragePublicDirectory( "MyDocuments"); File file = new File(foler, "myText.txt"); in = new FileInputStream(file); /// 以下、内部ストレージ内のファイル読み込みと同様 } } } Fileオブジェクトの作成までは、外部ストレージにファイルを作成する時と同様で す。Fileオブジェクトの作成が完了したら、newでFileInputStreamオブジェクトを 作成します。引数には読み込み元を表すFileオブジェクトを指定します。 Permission 「Permission」 とは、読み書きを行うための権限のことです。内部ストレージへの ファイル読み書きには特別なPermissionは不要ですが、外部ストレージへのファイ ル読み書きは他アプリに影響が出るため、Permissionの追加が必要です。 外部ストレージのファイルを読み込には「android.permission.READ_EXTE RNAL_STORAGE」を、外部ストレージのファイルに書き込む場合は「android. permission.WRITE_EXTERNAL_STORAGE」を「AndroidManifest. xml」に追加します。 Permissionの追加 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.androidopentextbook.storage.filesample" > <!-- ファイルを読み込む場合 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <!-- ファイルに書き込む場合 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <!-- 中略 --> </manifext> 17 練習問題 図3のような、入力内容をファイルに保存するメモ帳アプリを作成してみましょう。メ モが他のアプリに読まれてしまうのを防ぐため、内部ストレージに保存するようにして みましょう。 図3 ファイルを使ったメモ帳 18 11-2 第 11 ストレージ(2) 章 著:深見浩和 KEYWORD LESSON SQL この節では、SQLiteを使ってリレーショナルデータベース をAndroidで扱う方法を学びます。リレーショナルデータ ベースとはデータを関連性に基づき管理する仕組みです。 CREATE TABLE / INSERT / SELECT / UPDATE / DELETE文 SQLite SQLiteOpenHelper SQLiteDatabase Cursor この節を学ぶとできること 基本的なSQL文が書けるように なる テーブル作成と、基本的なデータ操作を 行うSQL文が書けるようになります。 AndroidのSQLiteを用いたプログラムが 書けるようになる 「SQLiteOpenHelper」を継承したクラスの作成と 「SQ LiteDatabase」クラスを使ったデータベース操作ができる ようになります。 19 ス ト レ ー ジ 11 -2 -1 SQLite を知る 「SQLite」 とは、Android標準で利用可能なデータベース管理システムのことで す。ここでは、SQLiteの使い方を学びます。 リレーショナルデータベース SharedPreferencesはKey-Value形式でデータを管理する仕組みでしたが、 関係モデル (リレーショナルモデル) に基づいてデータを管理するシステムもあります。 このシステムを「リレーショナルデータベースマネジメントシステム (RDBMS)」 と呼 び、SQLiteはこのRDBMSの一つです。RDBMSによって構築されるデータベー スを「リレーショナルデータベース」 と呼び、データベース内の関係(リレーション) は 一般に「表(テーブル)」 と呼ばれます。図1に、RDBMSとリレーショナルデータベー ス・テーブルの関係を示します。 RDBMS リレーショナルデータベース a b 1 ・ ・ ・ ・ ・ ・ 2 ・ ・ ・ ・ ・ ・ c d 20 ・ ・ ・ ・ ・ ・ 30 ・ ・ ・ ・ ・ ・ テーブル 図1:RDBMSとリレーショナルデータベース・テーブル 関係モデルについて説明すると、そ れだけで1冊の教科書となってしまい ます。ここでは、 「データを表のような かたちで表すモデル」 というイメージ の説明にとどめておきます テーブル・列・行 リレーショナルデータベースでは、データは図2のように表の形式で保存されてい ます。この表のことを「テーブル」 と呼びます。テーブルには名前があり、 1つのデータ ベースに複 数のテーブルを格 納することができます。また、図2のテーブルで 「name」や「email」に相当するものを「列」 と呼びます。 「列」は名前のほかに、数 値や文字列などの「型」の情報も持ちます。テーブル内のデータ1つ1つを「行」 と 呼びます。各データがどのようなフィールドで構成されているかは、 「列」を見ればわ かります。 20 列 第 11 章 ID name email 1 shima [email protected] 2 mhidaka [email protected] 3 fkm [email protected] ス ト レ ー ジ 行 図2:テーブル・列・行 SQLとは Androidアプリ開発では、端末に対する命令などをJavaという言語で記述しまし た。これに対し、RDBMSに対する命令(問い合わせ) は「SQL」 と呼ばれる言語で 記述します。Android標準のSQLiteに対する問い合わせもSQLで記述します。 SQLの文法は、大きく3種類に分けられます。 ・データ定義言語(DDL) ・データ操作言語(DML) ・データ制御言語(DCL) SQLについて詳しく説明すると、それだけで数百ページの教科書となってしまうの で、ここではAndroidでSQLiteを使うために最低限必要となる文法のみ解説しま す。 11 -2 -2 PC で SQL を実行する Android実機にはアプリから利用するためのSQLiteソフトウェアが入っています が、Android SDKにもSQLiteソフトウェアが入っています。これを利用して、SQL をPC上で実行することができます。 Windowsでは「<Android SDKのインストールフォルダー>¥platf ormtools」内に「sqlite3.exe」ファイルがあるので、これをダブルクリックで起動します。 (図3) 21 図3:platform-tools内のsqlite3.exe MacやLinuxでは「<Android SDKのインストールフォルダー>/platformtools」内に「sqlite3」ファイルがあるので、 ターミナルで実行します。 sqlite3を起動すると、図4のようなウィンドウが表示されます。終了する時は 「.exit」を実行します。 「Connected to a transient in-memory database」 というメッセージは、起動直後、メモリ上のデータベースに接続されたという意味で す。このデータベースは「.exit」などで終了すると同時に消えてしまうので、気軽に SQLを実行して動作を確認することができます。 図4:sqlite3.exeが起動したウィンドウ sqlite3は、初期設定では結果の表示方法がやや不親切なので、次の2つのコ マンドを実行しておきます。 22 sqlite3の設定変更 第 11 .mode column 章 .header on ス ト レ ー ジ 図5 設定を変更する テーブルを作る まず、SQLに慣れるところから始めましょう。SQLでデータベースにテーブルを作る には「CREATE」文を使用します。 CREATE文 CREATE TABLE <テーブル名>( 列名1 <型>, 列名2 <型>, ...) 「<型>」には、SQLiteでは次の5つが指定できます。 ・ TEXT ・ INTEGER ・ NUMERIC ・ REAL ・ NONE 次は、名前と年齢、 2つの列をもつ「user」 というテーブルを作る例です。 23 userテーブルを作る CREATE TABLE user( name TEXT, age INTEGER) sqlite3では、図6のようにCREATE文を実行します。sqlite3では、セミコロンま でを1つのSQLと解釈して実行するため、途中で改行を入れても大丈夫です。途 中で改行を入れた場合は、入力行の先頭がsqliteから...に変化します。 図6:CREATE文をsqlite3で実行する 練習問題 SQLiteでは、日付を扱うデータ型が ありません。したがって、INTEGER 型の列に19 7 0 年1月1日からの経 過秒数(もしくはミリ秒数) を入れて日 表1のような「lecture」テーブルを作成するSQLを書いてみましょう。列の型は 次のようにします。 付を表すことが多いです。 _id : INTEGER date : INTEGER title : TEXT _id date 1 4/2 開発環境セットアップ 2 4/3 Java基礎 3 4/4 Java応用 表1:lectureテーブル 24 title 11 -2 -3 テーブルを操作してみよう 第 11 章 ス ト レ ー ジ テーブルに行を追加する テーブルができたら、次はそのテーブルにデータ (行) を追加しましょう。行の追加 は「INSERT」文を使います。 INSERT文 INSERT INTO <テーブル名>(列名1,列名2,...) VALUES(値1,値2,...) 次は、userテーブルに名前=fkm/年齢=30という行を追加する例です。 userテーブルに行を追加する INSERT INTO user(name, age) VALUES('fkm', 30) 列名と値の対応がとれていれば、次のように列の順序を入れ替えても正しく動作 します。 列の順序を変えてINSERT INSERT INTO user(age, name) VALUES(30, 'fkm') テーブルから行を取り出す テーブルに追加した行(データ) を取り出す (取得する) には、SELECT文を使い ます。すべてのパターンを説明すると膨大な量になるので、ここでは代表的なものの み解説します。 すべて取得するSELECT文 SELECT * FROM <テーブル名> 指定した列だけ取得したい場合は、SELECTの後に*ではなく列名をカンマ区 切りで指定します。次は、名前列だけ取得するSELECT文です。 名前だけ取得するSELECT文 SELECT name FROM user 25 指定した条件を満たす行(データ) だけ取得したい場合は、FROM <テーブル 名> の後にWHEREと条件(これをWHERE句と呼びます) を追加します。次は、 年齢が20歳未満の行を取得するためのSELECT文です。 20歳未満の行を取得するSELECT文 SELECT * FROM user WHERE age < 20 条件部分には、次のような演算子が使用できます。 ・ A = B : AとBが等しい ・ A <> B : AとBが等しくない ・ A < B : AがBより小さい ・ A <= B : AがBより小さいか、等しい ・ A > B : AがBより大きい ・ A >= B : AがBより大きいか、等しい また、条件をANDやORでつなぐこともできます。次は身長(height)が180より大き く、収入(earnings)が1000以上である行を取得するSELECT文です。 ANDを使う例 SELECT * FROM worker WHERE height > 180 AND earnings >= 1000 図7に、sqlite3でINSERT文とSELECT文を実行した例を示します。 図7:INSERTとSELECT文をsqlite3で実行する 26 行を書き換える 第 11 章 テーブル内の行を書き換える (更新する) には、UPDATE文を使用します。 ス ト レ ー ジ UPDATE文 UPDATE <テーブル名> SET 列名1=値,列名2=値,... WHERE 条件 W H E R E 句で指 定した条 件に一 致した行の、指 定した列を更 新します。 WHERE句を忘れると、テーブル内のすべての行を更新してしまうので注意します。 次は、id=1の行の収入を300に更新する例です。 UPDATE文の例 UPDATE worker SET earnings=300 WHERE id=1 行を削除する テーブル内の行を削除するには、DELETE文を使用します。 DELETE文 DELETE FROM <テーブル名> WHERE 条件 WHERE句で指定した条件に一致した行を削除します。WHERE句を忘れる と、 テーブル内のすべての行を削除してしまうので特に注意します。次は、id=2の行 を削除する例です。 DELETE文の例 DELETE FROM worker WHERE id=2 図8に、sqlite3でUPDATE文とDELETE文を実行した例を示します。DEL ETE文を実行した後、再度SELECT文で行を取得しようとすると、結果が0件で あることがわかります。 27 図8:UPDATEとDELETE文をsqlite3で実行する 11 -2 -4 Android で SQLite を使う SQLに慣れてきたところで、AndroidアプリでSQLiteを使ってみましょう。Andr oidでSQLiteを使うには、準備として次の2ステップを行います。 ・ SQLiteOpenHelperクラスを継承したクラスを定義する。 ・ 必要な場面で、定義したクラスのオブジェクトを作り、 メソッドを呼ぶ。 SQLiteOpenHelperを継承したクラスを定義する まず「SQLiteOpenHelper」を継承した「MyHelper」 というクラスを定義しま す。 MyHelperクラスを定義する public class MyHelper extends SQLiteOpenHelper { private static final String DB_NAME = "my.db"; private static final int DB_VERSION = 1; /** * コンストラクタ */ public MyHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } } 28 「SQLiteOpenHelper」クラスには引数ありコンストラクタが既に定義されている 第 ので、 「MyHelper」クラスにもコンストラクタを用意し 「super()」を呼びます。第2引 11 数はデータベース名、第3引数は「null」を指定します。第4引数は自分で決めた ス ト レ ー ジ 章 データベースのバージョンを指定します。現時点では「1」を指定しておけばよいで しょう。 onCreate()でテーブル作成SQLを実行する 「SQLiteOpenHelper」には2つの抽象メソッドが定義されています。MyHelp erクラスはこの2つのメソッドをOverrideしなければなりません。 ・ public void onCreate(SQLiteDatabase db) ・ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVe rsion) 「onCreate()」は、データベースのファイルを作る必要が生じた時に呼ばれます。 「onUpgrade()」は、データベースのバージョンに変化が起きた時(たとえばアプリ のバージョンアップなど) に呼ばれます。 「onCreate()」が呼ばれた時に、データベースの初期化としてテーブルを作成す るSQLを実行します。SQLを実行するには「onCreate()」の引数で渡される 「SQ LiteDatabase」オブジェクトの「execSQL()」を呼びます。 29 memoテーブルを作成する public class MyHelper extends SQLiteOpenHelper { public static final String TABLE_NAME = "memo"; private static final String SQL_CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + Columns._ID + " INTEGER primary key autoincrement," + Columns.MEMO + " TEXT," + Columns.CREATE_TIME + " INTEGER," + Columns.UPDATE_TIME + " INTEGER)"; public interface Columns extends BaseColumns { public static final String MEMO = "memo"; public static final String CREATE_TIME = "create_time"; public static final String UPDATE_TIME = "update_time"; } /** * データベースファイルを作成すべき時に呼ばれる。 */ @Override public void onCreate(SQLiteDatabase db) { // CREATE文を実行する db.execSQL(SQL_CREATE_TABLE); } /** * データベースのバージョン (コンストラクタの第4引数) が * 変化した時に呼ばれる。 */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 現時点では何もしない } } ここでは、データベースファイル作成と同時に「CREATE TABLE」文を実行 し、 「memo」テーブルを作成しています。 このため、memoテーブルに行を追加した り、検索しようとした時に、テーブルが作成されていないといった問題が起きなくなりま す。 SQLのCREATE TABLE文は、間違いが発生しないよう、文字列定数の足 し算で構成しています。実際にexecSQL()メソッドで実行されるSQLは次のとおり です。 CREATE TABLE文 CREATE TABLE memo( _id INTEGER primary key autoincrement, memo TEXT, create_time INTEGER, update_time INTEGER) 30 「_id」列に「primary key」 と 「autoincrement」 というキーワードが付いていま 第 す。 「primary key」は「主キー」 と呼び、テーブル内で行を一意に識別するため 11 の列であることを示します。ここでは、 「_id」列をprimary keyに指定しているので、 ス ト レ ー ジ 章 「_id=1」 となる行はmemoテーブルに1つしか存在できません。 「_id=1」 となる行が memoテーブルに存在する状態で、別の「_id=1」 となる行を追加しようとするとエ ラーになります。 「autoincrement」を付けると、行の追加時に自動で1つずつ増や しながら値を設定してくれます。 Androidでは、主キーがINTEGER型の_idであるテーブルに対して特定の機 能を提供するAPIが存在するため、特に理由が無い場合は主キーを_idにしておき ます。 MyHelperオブジェクトを作る MyHelperクラスの定義ができたら、次にオブジェクトを作ります。ここでは、Acti vityのonCreate()内で生成し、 フィールドにセットしておきます。 MyHelperオブジェクトを作る public class MainActivity extends ActionBarActivity { private MyHelper mHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // MyHelperオブジェクトを作り、フィールドにセット mHelper = new MyHelper(this); } } 行を追加する データベースに行を追加するには、次の3ステップを行います。 ・ MyHelperオブジェクトのgetWritableDatabase()を呼び、SQLiteDatabaseオ ブジェクトを取得する ・ 必要なデータを準備し、SQLiteDatabaseオブジェクトのinsert()を呼ぶ ・ SQLiteDatabaseオブジェクトのclose()を呼び、処理の終了を伝える 31 行を追加する public class MainActivity extends ActionBarActivity { // 中略 // 引数memoは、画面で入力された内容とします。 private void insert(String memo) { SQLiteDatabase db = mMemoDB.getWritableDatabase(); // 列に対応する値をセットする ContentValues values = new ContentValues(); values.put(MyHelper.Columns.MEMO, memo); values.put(MyHelper.Columns.CREATE_TIME, System.currentTimeMillis()); values.put(MyHelper.Columns.UPDATE_TIME, System.currentTimeMillis()); // データベースに行を追加する long id = db.insert(MyHelperDB.TABLE_NAME, null, values); if (id == -1) { Log.v("Database", "行の追加に失敗したよ"); } // データベースを閉じる (処理の終了を伝える) db.close(); } } 追加する行のデータは「ContentValues」オブジェクトのput()で列毎に指定し ます。ここでは、次のように値をセットしています。 ・ memo列:入力された値 ・ create_time列とupdate_time列:現在時刻 行を検索する 次に、追加した行をデータベースを検索して取得しましょう。行の検索は次の7ス テップです。 ①MyHelperオブジェクトのgetReadableDatabase()を呼び、SQLiteDatabaseオ ブジェクトを取得する ②SQLiteDatabaseオブジェクトのquery()を呼ぶ。検索結果はCursorオブジェクト として返却される ③CursorオブジェクトのmoveToFirst()を呼び、読み込み中の位置を検索結果の 最初の行に移動させる。これがfalseを返した場合は、検索結果は0件 ④CursorオブジェクトのgetColumnIndex()を呼び、列に対応するインデックスを取 得する 32 ⑤do - whileを用いて、1行ずつ読み込み位置をずらしながら行のデータを取得す 第 11 る。 章 ⑥Cursorオブジェクトのclose()を呼び、読み込み終了を伝える ス ト レ ー ジ ⑦SQLiteDatabaseオブジェクトのclose()を呼び、処理の終了を伝える ややステップ数が多いので、最初にコード全体を示します。 行の検索 public class MainActivity extends ActionBarActivity { // 中略 private List<Memo> loadMemo() { // 1. SQLiteDatabaseオブジェクト取得 SQLiteDatabase db = mMyHelper.getReadableDatabase(); // 2. query()を呼び、検索を行う。 Cursor cursor = db.query(MyHelper.TABLE_NAME, null, null, null, null, null, MyHelper.Columns.CREATE_TIME + " ASC"); // 3. 読み込み位置を先頭にする。falseの場合は結果0件 if (!cursor.moveToFirst()) { cursor.close(); db.close(); return new ArrayList<>(); } // 4. 列のindex (位置) を取得する int idIndex = cursor.getColumnIndex(MyHelper.Columns._ID); int memoIndex = cursor.getColumnIndex(MyHelper.Columns.MEMO); int createIndex = cursor.getColumnIndex(MyHelper.Columns.CREATE_TIME); int updateIndex = cursor.getColumnIndex(MyHelper.Columns.UPDATE_TIME); // 5. 行を読み込む。 List<Memo> list = new ArrayList<>(cursor.getCount()); do { Memo item = new Memo(); item.mId = cursor.getInt(idIndex); item.mMemo = cursor.getString(memoIndex); item.mCreateTime = cursor.getLong(createIndex); item.mUpdateTime = cursor.getLong(updateIndex); list.add(item); // 読み込み位置を次の行に移動させる。 // 次の行が無い時はfalseを返すのでループを抜ける } while (cursor.moveToNext()); // 6. Cursorを閉じる cursor.close(); // 7. データベースを閉じる db.close(); return list; } } 33 SQLiteDatabaseオブジェクトの「query()」を使用すると、自分で複雑な「SEL ECT」文を記述することなく検索が行えます。ここでは全件取得し、作成時刻の早 い順(昇順) で並び替えています。 メモリ節約のため、取得する列を指定する場合は「query()」の第2引数に列名 の配列を指定します。次は、 「_id」 と 「memo」列だけ取得する例です。 指定した列だけ取得する String[] columns = { MyHelper.Columns._ID, MyHelper.Columns.MEMO }; Cursor cursor = db.query(MemoDB.TABLE_NAME, columns, null, null, null, null, MyHelper.Columns.CREATE_TIME + " ASC"); SQLの「WHERE」句で条件を指定して、特定の行だけ取得するには、第3引 数にWHEREの内容を、第4引数には第3引数の「?」の部分に入れる値を指定し ます。文章で説明するとイメージしにくいので、 「_id」が指定したものと一致する行 だけ取得する例で説明します。 WHERE句で条件を指定する // idは引数で渡された値とします。 String where = MyHelper.Columns._ID + "=?"; String[] args = { String.valueOf(id) }; Cursor cursor = db.query(MyHelper.TABLE_NAME, null, where, args, null, null, MyHelper.Columns.CREATE_TIME + " ASC"); ここでは、第3引数は「_id=?」 という文字列になっています。 「=」の右辺はユー ザーの操作によって実行時に変化するので「?」を指定します。そして、第4引数で? の部分に入れる値を指定しています。第3引数が「height > ? AND earnings > ?」のように、 「?」を複数含む場合は、第4引数は「?」の数と同じ長さの配列にし ます。 勘がいい方は、 「なぜ第3引数を 『"_id=" + id』 のようにしないんだろう?」 と思う かもしれません。なぜこのように?で場所を指定し、第4引数で値を指定するかは後 ほど説明します。 query()の結果は「Cursor」オブジェクトで返されます。Cursorオブジェクトには 検索結果の行と、読み込み中の行の位置が格納されています。query()直後は 読み込み中の行の位置が不定なので「moveToFirst()」を呼び、先頭に移動さ せます。検索結果が0件の場合は「moveToFirst()」が「false」を返すので、 そこ で処理を終了します。 結果が1件以上あった場合、次にCursorオブジェクトの「getColumnInd 34 ex()」で、指定した列名に対するインデックス (位置) を列毎に取得します。これは、 第 Cursorオブジェクトから読み込み中の行の、指定した列のデータを読み込む際、 11 列名ではなく位置を指定するためです。 ス ト レ ー ジ 列名に対する位置が取得できたら、いよいよ各行のデータを読み込みます。 「 m o v e T o F i r s t ( ) 」で読み込み位 置が最 初の行に移 動しているので、 「dowhile」ループを使用します。ここでは、1行分のデータを表すMemoオブジェクトを 章 Cursorクラスには「次の要素がある か」 を判定するメソッドが無いため、こ のようにdo-whileループを用いるし かありません。 作り、 その中に読み込んだデータを格納するようにしています。 行を更新する 行の更新は行の追加と似ています。 ①MyHelperオブジェクトのgetWritableDatabase()を呼び、SQLiteDatabaseオ ブジェクトを取得する。 ②必要なデータを準備し、SQLiteDatabaseオブジェクトのupdate()を呼ぶ。 ③SQLiteDatabaseオブジェクトのclose()を呼び、処理の終了を伝える。 行を更新する public class MainActivity extends ActionBarActivity { // 中略 private void updateMemo(int id, String memo) { // 1. SQLiteDatabase取得 SQLiteDatabase db = mMyHelper.getWritableDatabase(); // 2. 更新する値をセット ContentValues values = new ContentValues(); values.put(MyHelper.Columns.MEMO, memo); values.put(MyHelper.Columns.UPDATE_TIME, System.currentTimeMillis()); // 更新する行をWHEREで指定 String where = MyHelper.Columns._ID + "=?"; String[] args = { String.valueOf(id) }; int count = db.update(MyHelper.TABLE_NAME, values, where, args); if (count == 0) { Log.v("Edit", "Failed to update"); } // 3. データベースを閉じる db.close(); } } 35 SQLのUPDATE文で説明しましたが、行の更新はWHERE句で更新する行 を指定します。WHERE部分の指定はupdate()の第3引数と第4引数で行いま す。指定方法はquery()の時と同様で、第3引数で?を含む条件を記述し、第4引 数で?に入れる値を指定します。update()は、更新に成功した行数を返します。ここ では、_id列が指定された値と一致する行の「memo」列と 「update_time」列を 更新しています。 行を削除する 行の削除はSQLiteDatabaseオブジェクトの「delete()」を呼びます。呼ぶまで の手順は行の更新とほぼ同じです。 ①MyHelperオブジェクトのgetWritableDatabase()を呼び、SQLiteDatabaseオ ブジェクトを取得する。 ②必要なデータを準備し、SQLiteDatabaseオブジェクトのdelete()を呼ぶ。 ③SQLiteDatabaseオブジェクトのclose()を呼び、処理の終了を伝える。 行の削除 public class MainActivity extends ActionBarActivity { // 中略 private void deleteMemo(int id) { // 1. SQLiteDatabaseを取得 SQLiteDatabase db = mMyHelper.getWritableDatabase(); // 2. 削除する行の条件を設定 String where = MyHelper.Columns._ID + "=?"; String[] args = { String.valueOf(id) }; int count = db.delete(MyHelper.TABLE_NAME, where, args); if (count == 0) { Log.v("Edit", "Failed to delete"); } // 3. データベースを閉じる db.close(); } } こちらもupdate()と同様、どの行を削除するかをdelete()の第2引数と第3引数で 指定します。指定方法もupdate()の時と同様です。delete()は削除した行数を返 します。ここでは「_id」列が指定した値と同じ行を削除しています。 36 なぜWHERE句に?を使用するか 第 11 章 query()やupdate()などは、SQLiteDatabaseクラスの内部でSQLを組み立て ス ト レ ー ジ た上で実行されます。この時、WHERE句で指定した条件はSQL組み立て時に そのまま使用されます。もし、WHERE句に?が含まれていた場合は、SQLとして意 味が変わらないよう、順に値が割り当てられます。 では、WHERE句の条件に?を使用せず「"name=" + name」のように文字 列を連結したものを渡した場合を考えてみます。変数「name」はString型として、 「"fkm"」が入っていた場合、最終的に次のようなSQLが実行されるでしょう。 nameにfkmが入っていた場合 SELECT * FROM user WHERE name=fkm もし、変数nameに"a OR 1=1"が入っていた場合、次のようなSQLが実行され てしまいます。 nameにa OR 1=1が入っていた場合 SELECT * FROM user WHERE name=a OR 1=1 これは、 「userテーブル内で、nameがaと等しいか、1=1が真となる行をすべて取 得しなさい」 という意味になります。この場合、 「1=1」は常に真となるので、userテー ブル内のすべての行が取得できてしまいます。 「?」を含む文字列を条件として指定し、 その次の引数で?に割り当てる値を指定し た場合、次のようなSQLが実行され、 「全件取得してしまう」 というような意図しない 動作を防ぐことができます。 条件に?を用いた場合 SELECT * FROM user WHERE name='a OR 1=1' このように、WHERE句の指定に?を使用せず、入力値を連結したものを使用し た場合、値によって意図していないSQLが実行されてしまうことがあります。上記の nameのような変数にSQLの動作を変える文字列を入れることで、不正にデータを 取得したり、 データベースを破壊したりすることを「SQLインジェクション」 と呼びます。 37 練習問題 図9のようなレイアウトを作成し、入力されたToDoをSQLiteに保存するミニアプリ を作ってみましょう。 ・「保存」ボタンを押すと、入力されたToDoをtodoテーブルにinsertする。 ・「取得」ボタンを押すと、todoテーブルの内容を全件取得し、TextViewに1行ず つ表示する。 図9:ToDoを保存するアプリ テーブル定義などは、 これまでの内容を参考にして、自分で決めてみましょう。 まとめ 本章では、Androidアプリ開発で主に利用する3つのAPIの使い方を学びまし た。アプリが扱うデータの性質に応じて、適切な仕組みを選択できるようになりましょ う。 38