Android InsertHelper解決插入速度慢的問題

最近在項目中需要解決數據入庫慢的問題,業務場景是:從服務器獲取到數據後解析完成後,需要將數據入庫,由於數據量比較大,並且在每次入庫時需要判斷當前庫中是否存在此條數據,如果存在則不入庫,否則入庫。 其中一位開發人員將業務實現後,量小時勉強可以使用,當量大時,至少需要長達幾分鐘的時候。 經過分析後,發現在實現這個需求時發了過多的查詢SQL(存在入庫,不存在則不入庫),同時沒有使用SQLiteStatement預編譯。 優化後,只需要2-3S鍾數據就完成整個UI顯示流程。

主要使用以下手段,請做爲參考:

INSERT OR REPLACE 表示:插入或者替換。

當將要插入的數據主鍵或唯一鍵對應的值已經存在,則將進行替換。如果不存在,則進行插入操作。


INSERT OR IGNORE 表示:插入或者忽略。

當將要插入的數據主鍵或唯一鍵對應的值已經存在,則將進行忽略不執行插入操作,否則進行插入。


存在入庫,不存在則不入庫的邏輯,就可以使用 INSERT OR IGNORE來實現,減少了發出多餘的SQL,減少了IO。 同時使用InsertHelper來完成入庫。


InsertHelper代碼如下, 代碼也是從android源碼中摘取(android.database.DatabaseUtils.InsertHelper) ,本人針對自己的業務做了一些修改,請參考。

不過在4.4版本上此類已經廢棄,不過可以拿出來單獨使用。


package dw.test;

import java.util.HashMap;
import java.util.Map;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.util.Log;



public  class InsertHelper {
	
	private static final String TAG = "InsertHelper";
	
    private final SQLiteDatabase mDb;
    private final String mTableName;
    private HashMap<String, Integer> mColumns;
    private String mInsertSQL = null;
    private SQLiteStatement mInsertStatement = null;
    private SQLiteStatement mIgnoreStatement = null;
    private SQLiteStatement mReplaceStatement = null;
    private SQLiteStatement mPreparedStatement = null;

    /**
     * {@hide}
     *
     * These are the columns returned by sqlite's "PRAGMA
     * table_info(...)" command that we depend on.
     */
    public static final int TABLE_INFO_PRAGMA_COLUMNNAME_INDEX = 1;

    /**
     * This field was accidentally exposed in earlier versions of the platform
     * so we can hide it but we can't remove it.
     *
     * @hide
     */
    public static final int TABLE_INFO_PRAGMA_DEFAULT_INDEX = 4;

    /**
     * @param db the SQLiteDatabase to insert into
     * @param tableName the name of the table to insert into
     */
    public InsertHelper(SQLiteDatabase db, String tableName) {
        mDb = db;
        mTableName = tableName;
    }

    public void initColumns() {
    	getStatement(false);
	}
    
    private void buildSQL() throws SQLException {
        StringBuilder sb = new StringBuilder(128);
        sb.append("INSERT INTO ");
        sb.append(mTableName);
        sb.append(" (");

        StringBuilder sbv = new StringBuilder(128);
        sbv.append("VALUES (");

        int i = 1;
        Cursor cur = null;
        try {
            cur = mDb.rawQuery("PRAGMA table_info(" + mTableName + ")", null);
            mColumns = new HashMap<String, Integer>(cur.getCount());
            while (cur.moveToNext()) {
                String columnName = cur.getString(TABLE_INFO_PRAGMA_COLUMNNAME_INDEX);
                String defaultValue = cur.getString(TABLE_INFO_PRAGMA_DEFAULT_INDEX);

                mColumns.put(columnName, i);
                sb.append("'");
                sb.append(columnName);
                sb.append("'");

                if (defaultValue == null) {
                    sbv.append("?");
                } else {
                    sbv.append("COALESCE(?, ");
                    sbv.append(defaultValue);
                    sbv.append(")");
                }

                sb.append(i == cur.getCount() ? ") " : ", ");
                sbv.append(i == cur.getCount() ? ");" : ", ");
                ++i;
            }
        } finally {
            if (cur != null) cur.close();
        }

        sb.append(sbv);

        mInsertSQL = sb.toString();
    }

    private SQLiteStatement getStatement(boolean allowReplace) throws SQLException {
        if (allowReplace) {
            if (mReplaceStatement == null) {
                if (mInsertSQL == null) buildSQL();
                // chop "INSERT" off the front and prepend "INSERT OR REPLACE" instead.
                String replaceSQL = "INSERT OR REPLACE" + mInsertSQL.substring(6);
                mReplaceStatement = mDb.compileStatement(replaceSQL);
            }
            return mReplaceStatement;
        } else {
            if (mInsertStatement == null) {
                if (mInsertSQL == null) buildSQL();
                mInsertStatement = mDb.compileStatement(mInsertSQL);
            }
            return mInsertStatement;
        }
    }
    public SQLiteStatement getStatementIgnore() throws SQLException {
        if (mIgnoreStatement == null) {
            if (mInsertSQL == null) buildSQL();
            // chop "INSERT" off the front and prepend "INSERT OR IGNORE" instead.
            String replaceSQL = "INSERT OR IGNORE" + mInsertSQL.substring(6);
            mIgnoreStatement = mDb.compileStatement(replaceSQL);
        }
        return mIgnoreStatement;
    }
    /**
     * Performs an insert, adding a new row with the given values.
     *
     * @param values the set of values with which  to populate the
     * new row
     * @param allowReplace if true, the statement does "INSERT OR
     *   REPLACE" instead of "INSERT", silently deleting any
     *   previously existing rows that would cause a conflict
     *
     * @return the row ID of the newly inserted row, or -1 if an
     * error occurred
     */
    private long insertInternal(ContentValues values, boolean allowReplace) {
        // Start a transaction even though we don't really need one.
        // This is to help maintain compatibility with applications that
        // access InsertHelper from multiple threads even though they never should have.
        // The original code used to lock the InsertHelper itself which was prone
        // to deadlocks.  Starting a transaction achieves the same mutual exclusion
        // effect as grabbing a lock but without the potential for deadlocks.
        mDb.beginTransactionNonExclusive();
        try {
            SQLiteStatement stmt = getStatement(allowReplace);
            stmt.clearBindings();
            for (Map.Entry<String, Object> e: values.valueSet()) {
                final String key = e.getKey();
                int i = getColumnIndex(key);
                DatabaseUtils.bindObjectToProgram(stmt, i, e.getValue());
            }
            long result = stmt.executeInsert();
            mDb.setTransactionSuccessful();
            return result;
        } catch (SQLException e) {
            Log.e(TAG, "Error inserting " + values + " into table  " + mTableName, e);
            return -1;
        } finally {
            mDb.endTransaction();
        }
    }
    private long insertInternalIgnore(ContentValues values, boolean allowIgnore) {
        mDb.beginTransactionNonExclusive();
        try {
            SQLiteStatement stmt = getStatementIgnore();
            stmt.clearBindings();
            for (Map.Entry<String, Object> e: values.valueSet()) {
                final String key = e.getKey();
                int i = getColumnIndexForIngnore(key);
                DatabaseUtils.bindObjectToProgram(stmt, i, e.getValue());
            }
            long result = stmt.executeInsert();
            mDb.setTransactionSuccessful();
            return result;
        } catch (SQLException e) {
            Log.e(TAG, "Error inserting " + values + " into table  " + mTableName, e);
            return -1;
        } finally {
            mDb.endTransaction();
        }
    }
    
    public long insertIgnore() {
        mDb.beginTransactionNonExclusive();
        try {
            SQLiteStatement stmt = getStatementIgnore();
            stmt.clearBindings();
            
            long result = stmt.executeInsert();
            mDb.setTransactionSuccessful();
            return result;
        } catch (SQLException e) {
        	return -1;
        } finally {
            mDb.endTransaction();
        }
    }
    
    public long insertIgnore(SQLiteStatement stmt) {
        mDb.beginTransactionNonExclusive();
        try {
        	long result = stmt.executeInsert();
            mDb.setTransactionSuccessful();
            return result;
        } catch (SQLException e) {
        	return -1;
        } finally {
            mDb.endTransaction();
        }
    }
    
    public long insertIgnoreNonTransaction(SQLiteStatement stmt) {
        try {
        	long result = stmt.executeInsert();
            return result;
        } catch (SQLException e) {
        	return -1;
        } 
    }
    
    public void beginTransactionNonExclusive() {
        mDb.beginTransactionNonExclusive();
    }
    
    public void setTransactionSuccessful() {
        mDb.setTransactionSuccessful();
    }
    
    public void endTransaction(){
    	mDb.endTransaction();
    }

    /**
     * Returns the index of the specified column. This is index is suitagble for use
     * in calls to bind().
     * @param key the column name
     * @return the index of the column
     */
    public int getColumnIndex(String key) {
        getStatement(false);
        final Integer index = mColumns.get(key);
        if (index == null) {
            throw new IllegalArgumentException("column '" + key + "' is invalid");
        }
        return index;
    }
    public int getColumnIndexForIngnore(String key) {
        final Integer index = mColumns.get(key);
        if (index == null) {
            throw new IllegalArgumentException("column '" + key + "' is invalid");
        }
        return index;
    }
    /**
     * Bind the value to an index. A prepareForInsert() or prepareForReplace()
     * without a matching execute() must have already have been called.
     * @param index the index of the slot to which to bind
     * @param value the value to bind
     */
    public void bind(int index, double value) {
        mPreparedStatement.bindDouble(index, value);
    }

    /**
     * Bind the value to an index. A prepareForInsert() or prepareForReplace()
     * without a matching execute() must have already have been called.
     * @param index the index of the slot to which to bind
     * @param value the value to bind
     */
    public void bind(int index, float value) {
        mPreparedStatement.bindDouble(index, value);
    }

    /**
     * Bind the value to an index. A prepareForInsert() or prepareForReplace()
     * without a matching execute() must have already have been called.
     * @param index the index of the slot to which to bind
     * @param value the value to bind
     */
    public void bind(int index, long value) {
        mPreparedStatement.bindLong(index, value);
    }

    /**
     * Bind the value to an index. A prepareForInsert() or prepareForReplace()
     * without a matching execute() must have already have been called.
     * @param index the index of the slot to which to bind
     * @param value the value to bind
     */
    public void bind(int index, int value) {
        mPreparedStatement.bindLong(index, value);
    }

    /**
     * Bind the value to an index. A prepareForInsert() or prepareForReplace()
     * without a matching execute() must have already have been called.
     * @param index the index of the slot to which to bind
     * @param value the value to bind
     */
    public void bind(int index, boolean value) {
        mPreparedStatement.bindLong(index, value ? 1 : 0);
    }

    /**
     * Bind null to an index. A prepareForInsert() or prepareForReplace()
     * without a matching execute() must have already have been called.
     * @param index the index of the slot to which to bind
     */
    public void bindNull(int index) {
        mPreparedStatement.bindNull(index);
    }

    /**
     * Bind the value to an index. A prepareForInsert() or prepareForReplace()
     * without a matching execute() must have already have been called.
     * @param index the index of the slot to which to bind
     * @param value the value to bind
     */
    public void bind(int index, byte[] value) {
        if (value == null) {
            mPreparedStatement.bindNull(index);
        } else {
            mPreparedStatement.bindBlob(index, value);
        }
    }

    /**
     * Bind the value to an index. A prepareForInsert() or prepareForReplace()
     * without a matching execute() must have already have been called.
     * @param index the index of the slot to which to bind
     * @param value the value to bind
     */
    public void bind(int index, String value) {
        if (value == null) {
            mPreparedStatement.bindNull(index);
        } else {
            mPreparedStatement.bindString(index, value);
        }
    }

    /**
     * Performs an insert, adding a new row with the given values.
     * If the table contains conflicting rows, an error is
     * returned.
     *
     * @param values the set of values with which to populate the
     * new row
     *
     * @return the row ID of the newly inserted row, or -1 if an
     * error occurred
     */
    public long insert(ContentValues values) {
        return insertInternal(values, false);
    }
    
    public long insertIgnore(ContentValues values) {
        return insertInternalIgnore(values, true);
    }
    /**
     * Execute the previously prepared insert or replace using the bound values
     * since the last call to prepareForInsert or prepareForReplace.
     *
     * <p>Note that calling bind() and then execute() is not thread-safe. The only thread-safe
     * way to use this class is to call insert() or replace().
     *
     * @return the row ID of the newly inserted row, or -1 if an
     * error occurred
     */
    public long execute() {
        if (mPreparedStatement == null) {
            throw new IllegalStateException("you must prepare this inserter before calling "
                    + "execute");
        }
        try {
            return mPreparedStatement.executeInsert();
        } catch (SQLException e) {
            Log.e(TAG, "Error executing InsertHelper with table " + mTableName, e);
            return -1;
        } finally {
            // you can only call this once per prepare
            mPreparedStatement = null;
        }
    }

    /**
     * Prepare the InsertHelper for an insert. The pattern for this is:
     * <ul>
     * <li>prepareForInsert()
     * <li>bind(index, value);
     * <li>bind(index, value);
     * <li>...
     * <li>bind(index, value);
     * <li>execute();
     * </ul>
     */
    public void prepareForInsert() {
        mPreparedStatement = getStatement(false);
        mPreparedStatement.clearBindings();
    }

    /**
     * Prepare the InsertHelper for a replace. The pattern for this is:
     * <ul>
     * <li>prepareForReplace()
     * <li>bind(index, value);
     * <li>bind(index, value);
     * <li>...
     * <li>bind(index, value);
     * <li>execute();
     * </ul>
     */
    public void prepareForReplace() {
        mPreparedStatement = getStatement(true);
        mPreparedStatement.clearBindings();
    }

    public void prepareForIgnore() {
        mPreparedStatement = getStatementIgnore();
        mPreparedStatement.clearBindings();
    }
    
    /**
     * Performs an insert, adding a new row with the given values.
     * If the table contains conflicting rows, they are deleted
     * and replaced with the new row.
     *
     * @param values the set of values with which to populate the
     * new row
     *
     * @return the row ID of the newly inserted row, or -1 if an
     * error occurred
     */
    public long replace(ContentValues values) {
        return insertInternal(values, true);
    }
    
    
    /**
     * Close this object and release any resources associated with
     * it.  The behavior of calling <code>insert()</code> after
     * calling this method is undefined.
     */
    public void close() {
        if (mInsertStatement != null) {
            mInsertStatement.close();
            mInsertStatement = null;
        }
        if (mReplaceStatement != null) {
            mReplaceStatement.close();
            mReplaceStatement = null;
        }
        if (mIgnoreStatement != null) {
        	mIgnoreStatement.close();
        	mIgnoreStatement = null;
        }
        mInsertSQL = null;
        mColumns = null;
    }
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章