2011年1月10日月曜日

Android あらかじめ作成した SQLite database をアプリに取り込む

Android のサンプルやチュートリアルでは、アプリ実行時に SQLite データベースを作成してデータの追加や更新、削除などを行っているのがほとんどです。

しかし、あらかじめ作成しておいた SQLite database をアプリに仕込みたい場合があります。

そこで、ここでは sqlite3 など使って作成した自分の SQLite database ファイルを、アプリの asset に入れ、初回起動時にアプリのシステムデータベース領域にコピーする方法を紹介します。


1. SQLite database ファイルを用意する

私は Ubuntu 派なので普通に sqlite3 を使います。(Windows とか Mac はよくわかりません... これとか? SQLite Database Browser)

主テーブルの他に android_metadata という名前のテーブルを作成します。中身は locale です。主テーブルの primary key は _id という名前にします(Android が id を理解できる用にするためです)。

$ cat createTable.sql create table android_metadata ( locale text default 'en_US' ); create table sample_data ( _id integer primary key not null, name text, address text, tel text );
ちなみに日本は ja_JP です。


データベースとテーブルを作成します。
$ sqlite3 mysqlite_database.db < createTable.sql

作成したテーブルに CSVファイルをインポートします。
ここでは、

1,name1,address1,tel1
2,name2,address2,tel2
...

というデータを想定しています。

# 改行コードの "\r" や、各カラムの " 囲みを削除するには
#
# $ tr -d "\r" < old.csv | tr -d '"' > new.csv
#
# とかやると幸せになれるかも。
# 文字コードは nkf とかで。
# primary key 部分は awk とかで頑張ってみるとか。

sqlite3 -separator , mysqlite_database.db ".import mydata.csv sample_data"

なにも言われなければうまくいったかも
sqlite3 mysqlite_database.db "select * from sample_data where _id=1" とかで中身をチェックして入っていればOK。


2. 展開&コピーして Android アプリからアクセスできるようにする

1. で作った SQLite database ファイルを asset に入れ、SQLiteOpenHelper を継承したクラスを作成します。

public class DataBaseHelper extends SQLiteOpenHelper { private static String DB_NAME = "my_database"; private static String DB_NAME_ASSET = "my_database.db"; private static final int DATABASE_VERSION = 17; private SQLiteDatabase mDatabase; private final Context mContext; private final File mDatabasePath; public DataBaseHelper(Context context) { super(context, DB_NAME, null, DATABASE_VERSION); mContext = context; mDatabasePath = mContext.getDatabasePath(DB_NAME); } /** * asset に格納したデータベースをコピーするための空のデータベースを作成する */ public void createEmptyDataBase() throws IOException { boolean dbExist = checkDataBaseExists(); if (dbExist) { // すでにデータベースは作成されている } else { // このメソッドを呼ぶことで、空のデータベースがアプリのデフォルトシステムパスに作られる getReadableDatabase(); try { // asset に格納したデータベースをコピーする copyDataBaseFromAsset(); String dbPath = mDatabasePath.getAbsolutePath(); SQLiteDatabase checkDb = null; try { checkDb = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READWRITE); } catch (SQLiteException e) { } if (checkDb != null) { checkDb.setVersion(DATABASE_VERSION); checkDb.close(); } } catch (IOException e) { throw new Error("Error copying database"); } } } /** * 再コピーを防止するために、すでにデータベースがあるかどうか判定する * * @return 存在している場合 {@code true} */ private boolean checkDataBaseExists() { String dbPath = mDatabasePath.getAbsolutePath(); SQLiteDatabase checkDb = null; try { checkDb = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY); } catch (SQLiteException e) { // データベースはまだ存在していない } if (checkDb == null) { // データベースはまだ存在していない return false; } int oldVersion = checkDb.getVersion(); int newVersion = DATABASE_VERSION; if (oldVersion == newVersion) { // データベースは存在していて最新 checkDb.close(); return true; } // データベースが存在していて最新ではないので削除 File f = new File(dbPath); f.delete(); return false; } /** * asset に格納したデーだベースをデフォルトのデータベースパスに作成したからのデータベースにコピーする */ private void copyDataBaseFromAsset() throws IOException{ // asset 内のデータベースファイルにアクセス InputStream mInput = mContext.getAssets().open(DB_NAME_ASSET); // デフォルトのデータベースパスに作成した空のDB OutputStream mOutput = new FileOutputStream(mDatabasePath); // コピー byte[] buffer = new byte[1024]; int size; while ((size = mInput.read(buffer)) > 0) { mOutput.write(buffer, 0, size); } // Close the streams mOutput.flush(); mOutput.close(); mInput.close(); } public SQLiteDatabase openDataBase() throws SQLException { return getReadableDatabase(); } @Override public void onCreate(SQLiteDatabase db) { } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } @Override public synchronized void close() { if(mDataBase != null) mDataBase.close(); super.close(); } }


あとは、普通にアクセスする

private DataBaseHelper mDbHelper; private SQLiteDatabase db; private StationCodeDao mStationCodeDao; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setDatabase(); } @Override public void onDestroy() { db.close(); super.onDestroy(); } private void setDatabase() { mDbHelper = new DataBaseHelper(this); try { mDbHelper.createEmptyDataBase(); db = mDbHelper.openDataBase(); } catch (IOException ioe) { throw new Error("Unable to create database"); } catch(SQLException sqle){ throw sqle; } } private static final String[] COLUMNS = {"_id", "name", "address", "tel"}; private Cursor findData(int id) { Cursor cursor = db.query(TABLE_NAME, COLUMNS, "where _id=" + id, null, null, null, COLUMN_ID); return cursor; }

---------------------------------------

2011年2月4日 追記

assets に入れた db が 1MB 以上の場合、うまくいかないことがあるみたいです。
そんなときは、itog さんのブログ

apkに1MB以上のデータベースファイルを含める - Hacking My Way ~ itogのhack日記 -

を参考に、db を圧縮するなり分割するなりすると、うまくいくようです。


---------------------------------------

2013年10月22日 追記

データベースのパスを固定から Context の getDatabasePath() を使うように修正しました。
バージョン番号を使って、アップデート時にデータベースを再度 Asset からコピーできるようにしました。



 

9 件のコメント:

  1. はじめまして、平塚と申します
    すみません、質問させてください。

    上記の方法で、実装してるのですが、
    while ((size = mInput.read(buffer)) > 0){
    のところでIOExceptionをはいて、落ちてしまいます。

    この場合に考えられる原因ってなんでしょうか??
    教えていただけると幸いです。
    よろしくお願いいたします

    返信削除
  2. はじめまして、塚原と申します。
    お聞きしたいことがあり、書き込みしました。

    上記の方法で実装しているのですが、携帯によって何事もなく動く携帯と強制終了する携帯があるのですが、原因がわからずこまっています。教えて頂けると幸いです。
    よろしくお願いいたします。

    動く携帯はGALAXYS SC-02B

    動かない携帯はARROWS X LTE F-05D

    返信削除
  3. 私もArrowsシリーズで動かなかったので調べたところ、Arrowsでは
    "/data/data/YOUR_PACKAGE/databases/" フォルダへのファイルアクセスがパーミッションで制限されているようです。
    databases を使わずに、以下のようにfilesフォルダを利用するようにしたら動きました。


    public void createEmptyDataBase() throws IOException {
    boolean dbExist = checkDataBaseExists();

    if (dbExist) {
    // すでにデータベースは作成されている
    } else {
    try {
    // asset に格納したデータベースをコピーする
    copyDataBaseFromAsset();
    } catch (IOException e) {
    throw new Error("Error copying database");
    }
    }
    }

    private boolean checkDataBaseExists() {
    SQLiteDatabase checkDb = null;

    try {
    File fileObj = mContext.getFileStreamPath(DATABASE_NAME);
    String dbPath = fileObj.getAbsolutePath();
    checkDb = SQLiteDatabase.openDatabase(dbPath, null,
    SQLiteDatabase.OPEN_READONLY);
    } catch (SQLiteException e) {
    // データベースはまだ存在していない
    }

    if (checkDb != null) {
    checkDb.close();
    }
    return checkDb != null ? true : false;
    }

    private void copyDataBaseFromAsset() throws IOException {
    // asset 内のデータベースファイルにアクセス
    InputStream mInput = mContext.getAssets().open(DATABASE_NAME);
    OutputStream mOutput = mContext.openFileOutput(DATABASE_NAME, Context.MODE_PRIVATE);

    // コピー
    byte[] buffer = new byte[1024];
    int size;
    while ((size = mInput.read(buffer)) > 0) {
    mOutput.write(buffer, 0, size);
    }

    // Close the streams
    mOutput.flush();
    mOutput.close();
    mInput.close();
    }

    @Override
    public SQLiteDatabase getWritableDatabase() {
    SQLiteDatabase db = null;

    try {
    File fileObj = mContext.getFileStreamPath(DATABASE_NAME);
    String dbPath = fileObj.getAbsolutePath();
    db = SQLiteDatabase.openDatabase(dbPath, null,
    SQLiteDatabase.OPEN_READWRITE);
    } catch (SQLiteException e) {
    throw new Error("Error opening database");
    }

    return db;
    }

    @Override
    public SQLiteDatabase getReadableDatabase() {
    SQLiteDatabase db = null;

    try {
    File fileObj = mContext.getFileStreamPath(DATABASE_NAME);
    String dbPath = fileObj.getAbsolutePath();
    db = SQLiteDatabase.openDatabase(dbPath, null,
    SQLiteDatabase.OPEN_READONLY);
    } catch (SQLiteException e) {
    throw new Error("Error opening database");
    }

    return db;
    }

    返信削除
    返信
    1. 唐沢 様

      タロイです。
      ありがとうございます。

      早速、その方法で作り直して動かしてみたいと思います。
      困り果てていたので、助かりました。本当にありがとうございます。(*・ω・)*_ _))ペコリン

      削除
  4. //The Android のデフォルトでのデータベースパス
    private static String DB_PATH = "/data/data/YOUR_PACKAGE/databases/"

    このパスにデータベースが置かれるかどうかは Androidのバージョンによって依存するでしょうから APIを利用して取得すべきでしょう

    返信削除
  5. はじめまして。
    参考にさせていただき、動かすことができました。
    ありがとうございました。

    しかし、つい先日まで、何の問題もなく動いていたのですが、

    メジャーアップデートした実機(android4.1.2)で
    android.database.sqlite.SQliteException:no such table・・・のエラーを吐いて落ちるようになってしまいました。

    もちろんテーブルもちゃんと存在していますし、何より、全くプログラムは動いていたものから変わっていないのです。

    どう対応すればよいのか分からず困っています。
    どんなことが考えられるでしょうか。

    返信削除
  6. In case you are interested in earning money from your websites or blogs via popup ads, you should try one of the most established networks: Pop Ads.

    返信削除
  7. eToro is the #1 forex broker for new and professional traders.

    返信削除