@blog.justoneplanet.info

日々勉強

ContentProviderを使ってSQLiteに複数行Insertする

■ContentProvider

以下のようにbulkInsertメソッドをOverrideする。

public class Provider extends ContentProvider {
    public static final Uri URI = Uri.parse("content://com.example.android.hoge/");
    @Override
    public String getType(Uri uri) {
        return null;
    }
    @Override
    public boolean onCreate() {
        return false;
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getReadableDatabase();
        Cursor cursor = sdb.query(
                Table.TABLENAME,
                new String[]{Table.ID, Table.DATA, Table.CREATED},
                selection,
                selectionArgs,
                null,
                null,
                sortOrder,
                null
        );
        return cursor;
    }
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        sdb.insert(Table.TABLENAME, null, values);
        getContext().getContentResolver().notifyChange(uri, null);
        return uri;
    }
    
    /**
     * superクラスでinsertを普通に呼ぶようなのでcompileStatementの処理を自前で書く
     */
    @Override
    public int bulkInsert(Uri uri, ContentValues[] values) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        
        sdb.beginTransaction();
        SQLiteStatement stmt = sdb.compileStatement(
            "INSERT INTO `" + Table.TABLENAME + "`(`" + Table.DATA + "`, `" + Table.CREATED + "`) VALUES (?, ?);"
        );
        int length = values.length;
        for(int i = 0; i < length; i++){
            stmt.bindString(1, values[i].getAsString(Table.DATA));
            stmt.bindLong(2, values[i].getAsLong(Table.CREATED));
            stmt.executeInsert();
        }
        sdb.setTransactionSuccessful();
        sdb.endTransaction();
        return length;
    }
    
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        int rows = sdb.update(Table.TABLENAME, values, selection, selectionArgs);
        getContext().getContentResolver().notifyChange(uri, null);
        return rows;
    }
    
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        int rows = sdb.delete(Table.TABLENAME, selection, selectionArgs);
        getContext().getContentResolver().notifyChange(uri, null);
        return rows;
    }
    
    private static class Helper extends SQLiteOpenHelper {
        static Helper INSTANCE = null;
        private Helper(Context context, CursorFactory factory) {
            super(context, Table.FILENAME, factory, Table.VERSION);
        }
        public static Helper getInstance(Context context, CursorFactory factory) {
            if (INSTANCE == null) {
                INSTANCE = new Helper(context, factory);
            }
            return INSTANCE;
        }
        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(
                "CREATE TABLE `" + Table.TABLENAME + "`(" +
                " `" + Table.ID      + "` INTEGER PRIMARY KEY AUTOINCREMENT," +
                " `" + Table.CREATED + "` INTEGER," +
                " `" + Table.DATA    + "` TEXT" +
                ");"
            );
        }
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        }
    }
}

super.bulkInsert(uri, values)は内部でinsert(uri, values)を複数回呼び出すようになっており、Insert文が複数回発行されて非常に遅くなる。

■クライアントコード

ContentValues[] contentValues = new ContentValues[length];
for(int i = 0; i < length; i++){
    ContentValues values = new ContentValues();
    values.put(DATA, "data_" + i);
    values.put(CREATED, new Date().getTime());
    contentValues[i] = values;
}
getApplicationContext().getContentResolver().bulkInsert(Provider.URI, contentValues);

androidのSQLiteでAUTOINCREMENT値を取得する

SELECT `seq` FROM sqlite_sequence WHERE `name` = 'TABLENAME';

上述を踏まえて以下のようにすることで取得できる。

    /**
     * auto_incrementで代入した最大値
     * @return
     */
    public long getLastInsertId() {
        long index = 0;
        SQLiteDatabase sdb = getReadableDatabase();
        Cursor cursor = sdb.query(
                "sqlite_sequence",
                new String[]{"seq"},
                "name = ?",
                new String[]{TABLENAME},
                null,
                null,
                null,
                null
        );
        if (cursor.moveToFirst()) {
            index = cursor.getLong(cursor.getColumnIndex("seq"));
        }
        cursor.close();
        return index;
    }

AsyncQueryHandlerを使ってSQLiteのクエリを非同期処理する

特に重いクエリは使ってないんだが、とりあえずAsyncQueryHandlerを使ってSQLiteのクエリを非同期で処理する。

■実装

以下のようにmanifestファイルのapplication要素内にproviderを追加する。

<provider 
    android:name=".Provider"
    android:authorities="info.justoneplanet.android.sample.provider"<!--アクセスするためのURI-->
    android:exported="false"/><!--非公開にしないと他のアプリからアクセスできてしまう-->

セキュリティには注意する。

Helper.java

class Helper extends SQLiteOpenHelper {
    private static final int VERSION = 1;
    private static final String FILENAME   = "info.justoneplanet.android.sample.db";
    static final String TABLENAME   = "tbl";
    static final String ID          = "_id";
    static final String CREATED     = "created";
    static final String NAME        = "name";
    static Helper INSTANCE = null;
    
    private Helper(Context context, CursorFactory factory) {
        super(context, FILENAME, factory, VERSION);
    }
    public static Helper getInstance(Context context, CursorFactory factory) {
        if (INSTANCE == null) {
            INSTANCE = new Helper(context, factory);
        }
        return INSTANCE;
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(
            "CREATE TABLE `" + TABLENAME + "`(" +
            " `" + ID         + "` INTEGER PRIMARY KEY AUTOINCREMENT," +
            " `" + CREATED    + "` INTEGER," +
            " `" + NAME       + "` TEXT" +
            ");"
        );
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

Provider.java

以下のようにContentProviderクラスを継承したProviderクラスを作る。

public class Provider extends ContentProvider {
    @Override
    public String getType(Uri uri) {
        return null;
    }
    @Override
    public boolean onCreate() {
        return false;
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getReadableDatabase();
        Cursor cursor = sdb.query(
                Helper.TABLENAME,
                new String[]{Helper.ID, Helper.NAME, Helper.CREATED},
                selection,
                selectionArgs,
                null,
                null,
                sortOrder,
                null
        );
        return cursor;
    }
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        sdb.insert(Helper.TABLENAME, null, values);
        getContext().getContentResolver().notifyChange(uri, null);
        return null;
    }
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        int rows = sdb.update(Helper.TABLENAME, values, selection, selectionArgs);
        getContext().getContentResolver().notifyChange(uri, null);
        return rows;
    }
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        int rows = sdb.delete(Helper.TABLENAME, selection, selectionArgs);
        getContext().getContentResolver().notifyChange(uri, null);
        return rows;
    }
}

Table.java

public class Table {
    private static final Uri URI = Uri.parse("content://info.justoneplanet.android.sample.provider/");
    private Context mContext;
    
    public Table(Context context) {
        mContext = context;
    }
    
    /**
     * データを取得する
     * @return ListActivity用のadapter
     */
    public void load(final LoadObserver observer)
    {
        AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
            @Override
            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
                super.onQueryComplete(token, cookie, cursor);
                observer.onLoadCursor(cursor);
            }
        };
        handler.startQuery(0, null, URI, null, null, null, Helper.CREATED + " DESC");
    }
    /**
     * 保存する
     * @return
     */
    public void add(String name) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(Helper.NAME, name);
        contentValues.put(Helper.CREATED, new Date().getTime());
        AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
            @Override
            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
                super.onQueryComplete(token, cookie, cursor);
            }
        };
        handler.startInsert(0, null, URI, contentValues);
    }
    /**
     * 一定時間経過したものを削除する
     * @param created
     * @return
     */
    public void delete(long created) {
        AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) {
        };
        handler.startDelete(0, null, URI, Helper.CREATED + " < ?", new String[]{String.valueOf(created)});
    }
    /**
     * helper経由でdbをcloseする
     */
    public void close() {
        Helper.getInstance(mContext, null).close();
    }
    public interface LoadObserver {
        public void onLoadCursor(Cursor cursor);
    }
}

CursorからAdapterを生成する

■実装

以下のようにすることでListActivityで使うadapterをcursorから直接生成できる。

SQLiteDatabase sdb = getReadableDatabase();
Cursor cursor = sdb.query(
        "table",
        new String[]{"_id", "name", "address", "created"},
        null,
        new String[]{},
        null,
        null,
        "created DESC",
        null
);

// Cursorを元にしてListActivity用のadapterを生成する
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
        mContext,
        R.layout.list,
        cursor,// datasource
        new String[]{"name", "address"},// データベースカラム名...(a)
        new int[]{R.id.list_face, R.id.list_tag}// (a)に対応するTextViewのid
);

上述の方法ではSQLiteで取得したデータになんらかの文字列を加えたりすることはできない。

■表示データの加工

以下のようにSimpleCursorAdapterクラスを継承することでデータを加工して表示できる。

static class ArrangeListAdapter extends SimpleCursorAdapter {
    public ArrangeListAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
        super(context, layout, c, from, to);
        setViewBinder(new ArrangeListViewBinder());
    }
    
    static class ArrangeListViewBinder implements SimpleCursorAdapter.ViewBinder {
        @Override
        public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
            if (columnIndex == cursor.getColumnIndex("name")) {// nameの時に文字列を付加する
                String p = cursor.getString(columnIndex);// viewにはまだattachされていないのでcursorから取得
                ((TextView) view).setText(p + " さん");
                return true;
            }
            return false;
        }
    }
}

AndroidでSQLiteを使ってテーブル構造を変える

class HogeHelper extends SQLiteOpenHelper {
    private Context mContext;
    public HogeHelper(Context context, CursorFactory factory) {
        super(context, CategoryTable.FILENAME, factory, CategoryTable.VERSION);
        mContext = context;
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE `tbl`(`_id` INTEGER PRIMARY KEY AUTOINCREMENT,`name` TEXT);");
    }
    
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion == 1 && newVersion == 2) {
            // ordカラムの追加
            db.execSQL("ALTER TABLE `tbl` ADD `ord` INTEGER AFTER `name`;");
            
            // 既存データの取得(idだけ)
            Cursor cursor = db.query(
                    "tbl",
                    new String[]{
                            "_id"
                    },
                    null,
                    null,
                    null,
                    null,
                    "_id ASC"
            );
            
            // 既存データのordカラムのデータを入れる
            int ord = 0;
            if(cursor.moveToFirst()){
                do{
                    db.execSQL(
                        "UPDATE `tbl` SET `ord` = ? WHERE `_id` = ?",
                        new String[]{String.valueOf(ord),
                        String.valueOf(cursor.getLong(cursor.getColumnIndex("_id")))}
                    );
                    ord = ord + 10;
                }
                while(cursor.moveToNext());
            }
            cursor.close();
        }
    }
}

onUpgradeメソッド内で以下のようにデータの取得はできる。

Cursor cursor = db.query(
    "tbl",
    new String[]{"_id"},
    null,
    null,
    null,
    null,
    "_id ASC"
);

onUpgradeメソッド内で以下のようにデータの変更はできない。

db.update(...);

dbはread専用であると怒られる。以下のように書込専用のdbを取得することもできない。

getWritableDatabase();

getWritableDatabaseメソッドがhelperのonUpgradeをcallするためである。