SQLiteDatabase
這個(gè)類,大家都不陌生。其中:
- [
int delete(String table, String whereClause, String[] whereArgs)
](https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#delete(java.lang.String, java.lang.String, java.lang.String[])) - [
Cursor query(String table, String[] columns, String whereClause, String[] whereArgs, String groupBy, String having, String orderBy, String limit)
](https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#query(java.lang.String, java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String)) - [
int update(String table, ContentValues values, String whereClause, String[] whereArgs)
](https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#update(java.lang.String, android.content.ContentValues, java.lang.String, java.lang.String[]))
增刪改查,有三個(gè)接口需要進(jìn)行條件限定。限定條件通過參數(shù)String whereClause, String[] whereArgs
來指定。
whereClause
的值形如_id = ? AND condition1 >= ? OR condition2 != ?
,其中的?
用于參數(shù)綁定,按順序,填入whereArgs
數(shù)組內(nèi)。
但說實(shí)話,使用這種方式,需要先將限定部分的SQL語句寫出來,將限定的參數(shù)替換為?
,然后記住次序,填入數(shù)組內(nèi)。一次還好,寫多了挺煩人的,如果是修改的話,還需要仔細(xì)確保SQL語句書寫正確,確保修改不會(huì)弄錯(cuò)參數(shù)順序。
為了方便,有的同學(xué)們就直接放棄了whereClause
和whereArgs
這種搭配,直接傳入完整的SQL限定字符串作為whereClause
參數(shù)的值,在whereArgs
參數(shù)傳入了null
。
這種用法同樣能達(dá)成我們的需求,為什么SDK要搞得這么復(fù)雜呢?答:一切都是為了性能。
讓我們來看下源碼里是怎么處理的:
以int delete(String table, String whereClause, String[] whereArgs)
這個(gè)方法為切入點(diǎn),相關(guān)實(shí)現(xiàn)在SQLiteDatabase.java
里。
/**
* Convenience method for deleting rows in the database.
*
* @param table the table to delete from
* @param whereClause the optional WHERE clause to apply when deleting.
* Passing null will delete all rows.
* @param whereArgs You may include ?s in the where clause, which
* will be replaced by the values from whereArgs. The values
* will be bound as Strings.
* @return the number of rows affected if a whereClause is passed in, 0
* otherwise. To remove all rows and get a count pass "1" as the
* whereClause.
*/
public int delete(String table, String whereClause, String[] whereArgs) {
acquireReference();
try {
// 組裝成完整的SQL語句,實(shí)例化SQLiteStatement
SQLiteStatement statement = new SQLiteStatement(this, "DELETE FROM " + table +
(!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
try {
// 執(zhí)行SQL語句
return statement.executeUpdateDelete();
} finally {
statement.close();
}
} finally {
releaseReference();
}
}
源碼里并沒做什么神奇的事情,僅僅是組裝成完整的SQL語句,和參數(shù)數(shù)組一起,實(shí)例化SQLiteStatement
,然后執(zhí)行這個(gè)語句。
執(zhí)行的過程實(shí)現(xiàn)在SQLiteStatement.java
。
/**
* Execute this SQL statement, if the the number of rows affected by execution of this SQL
* statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements.
*
* @return the number of rows affected by this SQL statement execution.
* @throws android.database.SQLException If the SQL string is invalid for
* some reason
*/
public int executeUpdateDelete() {
acquireReference();
try {
// 獲取各個(gè)參數(shù):sql語句、要綁定的參數(shù)等
// 然后才是真正的執(zhí)行
return getSession().executeForChangedRowCount(
getSql(), getBindArgs(), getConnectionFlags(), null);
} catch (SQLiteDatabaseCorruptException ex) {
onCorruption();
throw ex;
} finally {
releaseReference();
}
}
獲取參數(shù),然后調(diào)用executeForChangedRowCount
方法。這個(gè)方法在SQLiteSession.java
。
/**
* Executes a statement that returns a count of the number of rows
* that were changed. Use for UPDATE or DELETE SQL statements.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param connectionFlags The connection flags to use if a connection must be
* acquired by this operation. Refer to {@link SQLiteConnectionPool}.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return The number of rows that were changed.
*
* @throws SQLiteException if an error occurs, such as a syntax error
* or invalid number of bind arguments.
* @throws OperationCanceledException if the operation was canceled.
*/
public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
CancellationSignal cancellationSignal) {
if (sql == null) {
throw new IllegalArgumentException("sql must not be null.");
}
// 這里雖然傳入了bindArgs,但并沒用到
if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
return 0;
}
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
// 真正用到sql和bindArgs的地方
return mConnection.executeForChangedRowCount(sql, bindArgs,
cancellationSignal); // might throw
} finally {
releaseConnection(); // might throw
}
}
Ok,繼續(xù)深入,來到SQLiteConnection.java
。
/**
* Executes a statement that returns a count of the number of rows
* that were changed. Use for UPDATE or DELETE SQL statements.
*
* @param sql The SQL statement to execute.
* @param bindArgs The arguments to bind, or null if none.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* @return The number of rows that were changed.
*
* @throws SQLiteException if an error occurs, such as a syntax error
* or invalid number of bind arguments.
* @throws OperationCanceledException if the operation was canceled.
*/
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 {
// 獲取預(yù)先編譯過的SQL
final PreparedStatement statement = acquirePreparedStatement(sql);
try {
throwIfStatementForbidden(statement);
// 參數(shù)綁定
bindArguments(statement, bindArgs);
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
// 交給SQLiteEngine執(zhí)行
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);
}
}
}
首先,會(huì)通過acquirePreparedStatement
去獲取PreparedStatement
實(shí)例,源碼如下:
private PreparedStatement acquirePreparedStatement(String sql) {
PreparedStatement statement = mPreparedStatementCache.get(sql);
boolean skipCache = false;
if (statement != null) {
if (!statement.mInUse) {
return statement;
}
// The statement is already in the cache but is in use (this statement appears
// to be not only re-entrant but recursive!). So prepare a new copy of the
// statement but do not cache it.
skipCache = true;
}
final long statementPtr = nativePrepareStatement(mConnectionPtr, sql);
try {
final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
final int type = DatabaseUtils.getSqlStatementType(sql);
final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
if (!skipCache && isCacheable(type)) {
mPreparedStatementCache.put(sql, statement);
statement.mInCache = true;
}
} catch (RuntimeException ex) {
// Finalize the statement if an exception occurred and we did not add
// it to the cache. If it is already in the cache, then leave it there.
if (statement == null || !statement.mInCache) {
nativeFinalizeStatement(mConnectionPtr, statementPtr);
}
throw ex;
}
statement.mInUse = true;
return statement;
}
可以看到,這里有個(gè)mPreparedStatementCache
用于緩存之前生成過的PreparedStatement
,如果之前有相同的SQL語句,則取出重用,避免重復(fù)編譯SQL。這個(gè)緩存本質(zhì)上是一個(gè)LruCache<String, PreparedStatement>
,key
為sql語句。
也即是,如果我們使用whereClause
和whereArgs
的方式操作數(shù)據(jù)庫的話,同樣的whereClause
,不同的whereArgs
取值,將能利用到這個(gè)緩存。但如果直接將限定語句拼接好,通常情況下,參數(shù)取值會(huì)改變,已有的緩存就派不上用場,白白浪費(fèi)了已有的PreparedStatement
緩存。
順便貼下綁定參數(shù)的代碼:
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 "
+ count + " were provided.");
}
if (count == 0) {
return;
}
final long 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;
}
}
}
代碼很簡單,就不多解釋了。
啰啰嗦嗦貼了這么多源碼,其實(shí)只是為了證明,whereClause
搭配whereArgs
是很有意義的。為了性能考慮,寫代碼的時(shí)候不要再用拼接字符串的方式直接生成限定語句了。
但,最開始提及的那種不便的使用方式,難道就只能默默忍受了?答案顯然并不是,通過簡單的抽象、封裝,能夠?qū)崿F(xiàn)如下的效果:
Statement statement =
Statement.where(UPDATE_TIME).lessOrEqual(now)
.and(EXPIRY_TIME).moreThan(now)
.or(AGE).eq(23)
.end();
statement.sql(); // 生成sql語句
statement.whereClause(); // 生成whereClause語句
statement.args(); // 對(duì)應(yīng)的參數(shù)數(shù)組
這是我嘗試造的一個(gè)輪子,用于通過語義化的方式,定義和生成whereClause
和whereArgs
。用起來就像是寫sql語句一樣自然,同時(shí)還能避免人工書寫sql語句導(dǎo)致的一些拼寫錯(cuò)誤,生成的whereClause
的參數(shù)順序也和whereArgs
參數(shù)數(shù)組嚴(yán)格對(duì)應(yīng)。
找時(shí)間整理下,分享到Gayhub供大家批判下。哈哈。