読者です 読者をやめる 読者になる 読者になる

yunomuのブログ

酒とゲームと上から目線

Android SDKのSQLiteDatabase#execSQLのbindArgsの型

AndroidSDKのSQLiteDatabase#execSQLのマニュアル
[http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#execSQL(java.lang.String, java.lang.Object[])]
よくあるsqlとbindArgs(Object配列)を渡す構造なんですが、このbindArgsのマニュアルを見ると

Parameters

sql the SQL statement to be executed. Multiple statements separated by semicolons are not supported.
bindArgs only byte[], String, Long and Double are supported in bindArgs.

byte[], String, Long and Doubleのどれかしかサポートしてないよと書いてあります。

すると「え、でもInteger入れたいんだけどどうなのよ」ってなるわけじゃないですか。
で、中身を見てみると

SQLiteDatabase.java

    public void execSQL(String sql, Object[] bindArgs) throws SQLException {
        if (bindArgs == null) {
            throw new IllegalArgumentException("Empty bindArgs");
        }
        executeSql(sql, bindArgs);
    }

ここはnullを弾いてるだけ。

SQLiteDatabase.java

    private int executeSql(String sql, Object[] bindArgs) throws SQLException {
        acquireReference();
        try {
            if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) {
                boolean disableWal = false;
                synchronized (mLock) {
                    if (!mHasAttachedDbsLocked) {
                        mHasAttachedDbsLocked = true;
                        disableWal = true;
                    }
                }
                if (disableWal) {
                    disableWriteAheadLogging();
                }
            }

            SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs);
            try {
                return statement.executeUpdateDelete();
            } finally {
                statement.close();
            }
        } finally {
            releaseReference();
        }
    }

ここはSQLiteStatementのコンストラクタに渡して、その後にSQLiteStatement#executeUpdateDeleteを呼んでる。

SQLiteStatement.java

    SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
        super(db, sql, bindArgs, null);
    }

コンストラクタでは親クラスのコンストラクタに渡しているだけ。おそらく単に親のフィールドに格納していて、getBindArgs()で取ってきたりしているんだろう。というかしてた。ちょっと気を使った書き方になってたのでライブラリ作る人は見てみてもいいかも。

SQLiteStatement.java

    public int executeUpdateDelete() {
        acquireReference();
        try {
            return getSession().executeForChangedRowCount(
                    getSql(), getBindArgs(), getConnectionFlags(), null);
        } catch (SQLiteDatabaseCorruptException ex) {
            onCorruption();
            throw ex;
        } finally {
            releaseReference();
        }
    }

executeUpdateDeleteメソッドでは、getBindArgsで親クラスから取ってきた値をSQLiteSession#executeForChangedRowCountに渡している。

SQLiteSession.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
            return 0;
        }

        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
        try {
            return mConnection.executeForChangedRowCount(sql, bindArgs,
                    cancellationSignal); // might throw
        } finally {
            releaseConnection(); // might throw
        }
    }

ここでは、2箇所でbindArgsが使われているけれど、1つ目のexecuteSpecialの中では使われていなかったので、2つ目のSQLiteConnection#executeForChangedRowCountに渡されている方が本命。

SQLiteConnection.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        int changedRows = 0;
        final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
                sql, bindArgs);
        try {
            final PreparedStatement statement = acquirePreparedStatement(sql);
            try {
                throwIfStatementForbidden(statement);
                bindArguments(statement, bindArgs);
                applyBlockGuardPolicy(statement);
                attachCancellationSignal(cancellationSignal);
                try {
                    changedRows = nativeExecuteForChangedRowCount(
                            mConnectionPtr, statement.mStatementPtr);
                    return changedRows;
                } finally {
                    detachCancellationSignal(cancellationSignal);
                }
            } finally {
                releasePreparedStatement(statement);
            }
        } catch (RuntimeException ex) {
            mRecentOperations.failOperation(cookie, ex);
            throw ex;
        } finally {
            if (mRecentOperations.endOperationDeferLog(cookie)) {
                mRecentOperations.logOperation(cookie, "changedRows=" + changedRows);
            }
        }
    }

真ん中あたりのbindArgumentsで使われている。これ以降には出てこないので、たぶんここでstatementの中に入っちゃってるんだろう。nativeExecuteForChangedRowCountまで見れば処理の本体が見られるだろうけど、今回の興味はここではない。
ところでこのtry節のネストっぷりは、なるほどなぁと思った。次からこうしよう。

SQLiteConnection.java

    private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
        final int count = bindArgs != null ? bindArgs.length : 0;
        if (count != statement.mNumParameters) {
            throw new SQLiteBindOrColumnIndexOutOfRangeException(
                    "Expected " + statement.mNumParameters + " bind arguments but "
                    + bindArgs.length + " were provided.");
        }
        if (count == 0) {
            return;
        }

        final int statementPtr = statement.mStatementPtr;
        for (int i = 0; i < count; i++) {
            final Object arg = bindArgs[i];
            switch (DatabaseUtils.getTypeOfObject(arg)) {
                case Cursor.FIELD_TYPE_NULL:
                    nativeBindNull(mConnectionPtr, statementPtr, i + 1);
                    break;
                case Cursor.FIELD_TYPE_INTEGER:
                    nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).longValue());
                    break;
                case Cursor.FIELD_TYPE_FLOAT:
                    nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).doubleValue());
                    break;
                case Cursor.FIELD_TYPE_BLOB:
                    nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
                    break;
                case Cursor.FIELD_TYPE_STRING:
                default:
                    if (arg instanceof Boolean) {
                        // Provide compatibility with legacy applications which may pass
                        // Boolean values in bind args.
                        nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                                ((Boolean)arg).booleanValue() ? 1 : 0);
                    } else {
                        nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
                    }
                    break;
            }
        }
    }

ここではfor文の中でなんぞ判定して、数値ならNumberにキャストしてからlongValueなりdoubleValueを呼び出してる。これならIntegerでもいけるっぽい。
ところでこのDatabaseUtils.getTypeOfObjectって何だ。

DatabaseUtils.java

    public static int getTypeOfObject(Object obj) {
        if (obj == null) {
            return Cursor.FIELD_TYPE_NULL;
        } else if (obj instanceof byte[]) {
            return Cursor.FIELD_TYPE_BLOB;
        } else if (obj instanceof Float || obj instanceof Double) {
            return Cursor.FIELD_TYPE_FLOAT;
        } else if (obj instanceof Long || obj instanceof Integer
                || obj instanceof Short || obj instanceof Byte) {
            return Cursor.FIELD_TYPE_INTEGER;
        } else {
            return Cursor.FIELD_TYPE_STRING;
        }
    }

このように、実際にはここでinstanceofを使って型を判別しております。見たところIntegerでもShortでもFloatでも良さそうですね。まあFloatをintValueで変換とかはしてくれなさそうですが、おおむね問題は起きないようにしてあるっぽいです。

で、マッチしなかったらStringとみなされて、さっきのbindArgumentsに戻ってみると、そこでさらにBooleanかどうかの判定が行われて、trueなら1、falseなら0、BooleanじゃなければObject#toStringが呼ばれる、と。
これ、このswitch文は必要なのかな。DatabaseUtils.getTypeOfObjectは消して、このswitch文と同化してしまえばいい気がするけどどうなんだろう。getTypeOfObjectがどこかの使い回しだったりするのかな。まあするんだろうなUtilsだし。

最後にtoStringしてるってことは、enumにtoStringを実装しておいてぶっ込むとかもできるし、細かく楽ができそうですね。