android - studio - sqlite use database




Android上SQLite的最佳做法是什麼? (7)

在Android應用程序中對SQLite數據庫執行查詢時,什麼會被視為最佳實踐?

從AsyncTask的doInBackground運行插入,刪除和選擇查詢是否安全? 或者我應該使用UI線程? 我想數據庫查詢可能是“沉重的”,不應該使用UI線程,因為它可以鎖定應用程序 - 導致應用程序無響應 (ANR)。

如果我有幾個AsyncTasks,他們應該共享一個連接還是應該分別打開一個連接?

這些場景是否有最佳做法?


並發數據庫訪問

同一篇文章在我的博客(我喜歡格式化更多)

我寫了一篇描述如何使你的android數據庫線程安全訪問的小文章。

假設你有自己的SQLiteOpenHelper

public class DatabaseHelper extends SQLiteOpenHelper { ... }

現在你想在不同的線程中將數據寫入數據庫。

 // Thread 1
 Context context = getApplicationContext();
 DatabaseHelper helper = new DatabaseHelper(context);
 SQLiteDatabase database = helper.getWritableDatabase();
 database.insert(…);
 database.close();

 // Thread 2
 Context context = getApplicationContext();
 DatabaseHelper helper = new DatabaseHelper(context);
 SQLiteDatabase database = helper.getWritableDatabase();
 database.insert(…);
 database.close();

您將在logcat中收到以下消息,並且您的更改之一將不會被寫入。

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

發生這種情況是因為每次創建新的SQLiteOpenHelper對象時,實際上都在建立新的數據庫連接。 如果您嘗試同時從實際不同的連接寫入數據庫,則會失敗。 (從上面的答案)

要在多線程中使用數據庫,我們需要確保我們正在使用一個數據庫連接。

讓我們創建單例類數據庫管理器 ,它將保存並返回單個SQLiteOpenHelper對象。

public class DatabaseManager {

    private static DatabaseManager instance;
    private static SQLiteOpenHelper mDatabaseHelper;

    public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
        if (instance == null) {
            instance = new DatabaseManager();
            mDatabaseHelper = helper;
        }
    }

    public static synchronized DatabaseManager getInstance() {
        if (instance == null) {
            throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
                    " is not initialized, call initialize(..) method first.");
        }

        return instance;
    }

    public SQLiteDatabase getDatabase() {
        return new mDatabaseHelper.getWritableDatabase();
    }

}

在不同的線程中將數據寫入數據庫的更新代碼將如下所示。

 // In your application class
 DatabaseManager.initializeInstance(new MySQLiteOpenHelper());
 // Thread 1
 DatabaseManager manager = DatabaseManager.getInstance();
 SQLiteDatabase database = manager.getDatabase()
 database.insert(…);
 database.close();

 // Thread 2
 DatabaseManager manager = DatabaseManager.getInstance();
 SQLiteDatabase database = manager.getDatabase()
 database.insert(…);
 database.close();

這會給你帶來另一次崩潰。

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase

由於我們只使用一個數據庫連接,所以方法getDatabase()Thread1Thread2返回SQLiteDatabase對象的相同實例。 發生什麼事情, Thread1可能會關閉數據庫,而Thread2仍在使用它。 這就是為什麼我們有IllegalStateException崩潰。

我們需要確保沒有人使用數據庫,只有關閉它。 建議的一些人永遠不要關閉你的SQLiteDatabase 。 它不僅聽起來很愚蠢,而且還以下面的logcat消息向你表示敬意。

Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed

工作樣本

public class DatabaseManager {

    private int mOpenCounter;

    private static DatabaseManager instance;
    private static SQLiteOpenHelper mDatabaseHelper;
    private SQLiteDatabase mDatabase;

    public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
        if (instance == null) {
            instance = new DatabaseManager();
            mDatabaseHelper = helper;
        }
    }

    public static synchronized DatabaseManager getInstance() {
        if (instance == null) {
            throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
                    " is not initialized, call initializeInstance(..) method first.");
        }

        return instance;
    }

    public synchronized SQLiteDatabase openDatabase() {
        mOpenCounter++;
        if(mOpenCounter == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

    public synchronized void closeDatabase() {
        mOpenCounter--;
        if(mOpenCounter == 0) {
            // Closing database
            mDatabase.close();

        }
    }

}

如下使用它。

SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way

每次需要數據庫時,都應該調用DatabaseManager類的openDatabase()方法。 在這個方法裡面,我們有一個計數器,表示數據庫打開了多少次。 如果它等於1,這意味著我們需要創建新的數據庫連接,否則,數據庫連接已經創建。

closeDatabase()方法也會發生同樣的情況。 每次我們調用這個方法時,計數器都會減少,每當它變為零時,我們都會關閉數據庫連接。

現在你應該能夠使用你的數據庫並確保它是線程安全的。


Dmytro的答案對我的情況很好。 我認為將函數聲明為synchronized是更好的。 至少在我的情況下,它會調用空指針異常,否則,例如getWritableDatabase尚未在一個線程中返回,而openDatabse在另一個線程中調用。

public synchronized SQLiteDatabase openDatabase() {
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

我對SQLiteDatabase API的理解是,如果你有一個多線程應用程序,你不能有超過1個SQLiteDatabase對象指向單個數據庫。

該對象肯定可以創建,但如果不同的線程/進程(太)開始使用不同的SQLiteDatabase對象(比如我們在JDBC Connection中使用的方式),插入/更新將失敗。

這裡唯一的解決方案是堅持使用1個SQLiteDatabase對象,並且每當在一個以上的線程中使用startTransaction()時,Android管理跨不同線程的鎖定,並且一次只允許一個線程擁有獨占更新訪問權限。

您也可以從數據庫執行“讀取”操作,並在另一個線程中使用相同的SQLiteDatabase對象(而另一個線程寫入),並且永遠不會發生數據庫損壞,即“讀取線程”不會從數據庫中讀取數據,直到“寫入線程“提交數據,儘管兩者都使用相同的SQLiteDatabase對象。

這與連接對像在JDBC中的連接對像是不同的,如果您在讀取和寫入線程之間傳遞(使用相同的)連接對象,那麼我們可能會打印未提交的數據。

在我的企業應用程序中,我嘗試使用條件檢查,以便UI線程永不需要等待,而BG線程持有SQLiteDatabase對象(專用)。 我嘗試預測UI操作並推遲BG線程運行'x'秒。 還可以維護PriorityQueue來管理SQLiteDatabase連接對象的處理,以便UI線程首先獲取它。


我知道響應遲了,但在android中執行sqlite查詢的最佳方式是通過自定義內容提供者。 通過這種方式,UI與數據庫類(擴展SQLiteOpenHelper類的類)分離開來。 查詢也在後台線程(Cursor Loader)中執行。


經過幾個小時的努力之後,我發現你只能使用每db執行一個db助手對象。 例如,

for(int x = 0; x < someMaxValue; x++)
{
    db = new DBAdapter(this);
    try
    {

        db.addRow
        (
                NamesStringArray[i].toString(), 
                StartTimeStringArray[i].toString(),
                EndTimeStringArray[i].toString()
        );

    }
    catch (Exception e)
    {
        Log.e("Add Error", e.toString());
        e.printStackTrace();
    }
    db.close();
}

適用於:

db = new DBAdapter(this);
for(int x = 0; x < someMaxValue; x++)
{

    try
    {
        // ask the database manager to add a row given the two strings
        db.addRow
        (
                NamesStringArray[i].toString(), 
                StartTimeStringArray[i].toString(),
                EndTimeStringArray[i].toString()
        );

    }
    catch (Exception e)
    {
        Log.e("Add Error", e.toString());
        e.printStackTrace();
    }

}
db.close();

每次循環迭代時創建一個新的DBAdapter是我可以通過我的助手類將我的字符串導入數據庫的唯一方法。


遇到了一些問題,我想我已經明白了為什麼我會出錯。

我編寫了一個數據庫包裝類,其中包含一個close()函數,該函數將helper關閉為open()的鏡像,它調用getWriteableDatabase,然後將其遷移到ContentProviderContentProvider的模型沒有使用SQLiteDatabase.close() ,我認為這是一個很大的線索,因為代碼使用getWriteableDatabase在某些情況下,我仍然在直接訪問(主屏幕驗證查詢,所以我遷移到getWriteableDatabase / rawQuery模型。

我使用一個單身人士,並在關閉文件中有一些不祥的評論

關閉任何打開的數據庫對象

(我的大膽)。

所以我有間歇性的崩潰,我使用後台線程來訪問數據庫,他們與前台同時運行。

所以我認為close()強制數據庫關閉,而不管其他任何線程持有引用 - 所以close()本身不是簡單地撤消匹配的getWriteableDatabase而是強制關閉所有打開的請求。 大多數情況下這不是問題,因為代碼是單線程的,但在多線程的情況下總會有打開和關閉不同步的機會。

讀過其他地方的註釋,說明SqLiteDatabaseHelper代碼實例是計數的,那麼只有當你想要關閉的時候,你希望你想要做一個備份副本的情況,並且你想強制關閉所有的連接並強制SqLite去寫出任何可能緩存的緩存內容 - 換句話說,停止所有應用程序數據庫活動,關閉以防助手失去踪跡,執行任何文件級活動(備份/恢復),然後重新開始。

儘管嘗試以受控方式關閉是一個不錯的主意,但實際情況是,Android保留垃圾回收的權利,因此任何關閉都會降低緩存更新未被寫入的風險,但如果設備無法保證是強調的,如果你已經正確地釋放了你的游標和對數據庫的引用(它不應該是靜態成員),那麼這個助手將會關閉數據庫。

所以我認為這種方法是:

使用getWriteableDatabase從單例包裝器打開。 (我使用派生的應用程序類從靜態提供應用程序上下文來解決對上下文的需求)。

切勿直接致電關閉。

切勿將結果數據庫存儲在任何不具有明顯範圍的對像中,並依靠引用計數來觸發隱式close()。

如果進行文件級別的處理,將所有數據庫活動暫停,然後調用close,以防假設您編寫正確的事務時出現失控線程,以便失控線程失敗,並且關閉的數據庫至少具有正確的事務比可能是部分事務的文件級副本。


  • 使用ThreadAsyncTask進行長時間運行(50ms +)。 測試你的應用,看看它在哪裡。 大多數操作(可能)不需要線程,因為大多數操作(可能)只涉及幾行。 使用線程進行批量操作。
  • 在線程之間為磁盤上的每個DB共享一個SQLiteDatabase實例,並實現一個計數係統以跟踪打開的連接。

這些場景是否有最佳做法?

在所有類之間共享一個靜態字段。 我曾經為這個和其他需要共享的東西保持單身。 一個計數方案(通常使用AtomicInteger)也應該用來確保你從不關閉數據庫提前關閉或打開它。

我的解決方案

對於最新版本,請參閱https://github.com/JakarCo/databasemanager但我會盡力讓代碼保持最新。 如果您想了解我的解決方案,請查看代碼並閱讀我的筆記。 我的筆記通常很有幫助。

  1. 將代碼複製/粘貼到名為DatabaseManager的新文件中。 (或從github下載)
  2. 擴展DatabaseManager並像通常那樣實現onCreateonUpgrade 。 您可以創建一個DatabaseManager類的多個子類以便在磁盤上擁有不同的數據庫。
  3. 實例化你的子類並調用getDb()來使用SQLiteDatabase類。
  4. 為實例化的每個子類調用close()

複製/粘貼的代碼:

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;

import java.util.concurrent.ConcurrentHashMap;

/** Extend this class and use it as an SQLiteOpenHelper class
 *
 * DO NOT distribute, sell, or present this code as your own. 
 * for any distributing/selling, or whatever, see the info at the link below
 *
 * Distribution, attribution, legal stuff,
 * See https://github.com/JakarCo/databasemanager
 * 
 * If you ever need help with this code, contact me at [email protected] (or [email protected] )
 * 
 * Do not sell this. but use it as much as you want. There are no implied or express warranties with this code. 
 *
 * This is a simple database manager class which makes threading/synchronization super easy.
 *
 * Extend this class and use it like an SQLiteOpenHelper, but use it as follows:
 *  Instantiate this class once in each thread that uses the database. 
 *  Make sure to call {@link #close()} on every opened instance of this class
 *  If it is closed, then call {@link #open()} before using again.
 * 
 * Call {@link #getDb()} to get an instance of the underlying SQLiteDatabse class (which is synchronized)
 *
 * I also implement this system (well, it's very similar) in my <a href="http://androidslitelibrary.com">Android SQLite Libray</a> at http://androidslitelibrary.com
 * 
 *
 */
abstract public class DatabaseManager {

    /**See SQLiteOpenHelper documentation
    */
    abstract public void onCreate(SQLiteDatabase db);
    /**See SQLiteOpenHelper documentation
     */
    abstract public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
    /**Optional.
     * *
     */
    public void onOpen(SQLiteDatabase db){}
    /**Optional.
     * 
     */
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
    /**Optional
     * 
     */
    public void onConfigure(SQLiteDatabase db){}



    /** The SQLiteOpenHelper class is not actually used by your application.
     *
     */
    static private class DBSQLiteOpenHelper extends SQLiteOpenHelper {

        DatabaseManager databaseManager;
        private AtomicInteger counter = new AtomicInteger(0);

        public DBSQLiteOpenHelper(Context context, String name, int version, DatabaseManager databaseManager) {
            super(context, name, null, version);
            this.databaseManager = databaseManager;
        }

        public void addConnection(){
            counter.incrementAndGet();
        }
        public void removeConnection(){
            counter.decrementAndGet();
        }
        public int getCounter() {
            return counter.get();
        }
        @Override
        public void onCreate(SQLiteDatabase db) {
            databaseManager.onCreate(db);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onUpgrade(db, oldVersion, newVersion);
        }

        @Override
        public void onOpen(SQLiteDatabase db) {
            databaseManager.onOpen(db);
        }

        @Override
        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onDowngrade(db, oldVersion, newVersion);
        }

        @Override
        public void onConfigure(SQLiteDatabase db) {
            databaseManager.onConfigure(db);
        }
    }

    private static final ConcurrentHashMap<String,DBSQLiteOpenHelper> dbMap = new ConcurrentHashMap<String, DBSQLiteOpenHelper>();

    private static final Object lockObject = new Object();


    private DBSQLiteOpenHelper sqLiteOpenHelper;
    private SQLiteDatabase db;
    private Context context;

    /** Instantiate a new DB Helper. 
     * <br> SQLiteOpenHelpers are statically cached so they (and their internally cached SQLiteDatabases) will be reused for concurrency
     *
     * @param context Any {@link android.content.Context} belonging to your package.
     * @param name The database name. This may be anything you like. Adding a file extension is not required and any file extension you would like to use is fine.
     * @param version the database version.
     */
    public DatabaseManager(Context context, String name, int version) {
        String dbPath = context.getApplicationContext().getDatabasePath(name).getAbsolutePath();
        synchronized (lockObject) {
            sqLiteOpenHelper = dbMap.get(dbPath);
            if (sqLiteOpenHelper==null) {
                sqLiteOpenHelper = new DBSQLiteOpenHelper(context, name, version, this);
                dbMap.put(dbPath,sqLiteOpenHelper);
            }
            //SQLiteOpenHelper class caches the SQLiteDatabase, so this will be the same SQLiteDatabase object every time
            db = sqLiteOpenHelper.getWritableDatabase();
        }
        this.context = context.getApplicationContext();
    }
    /**Get the writable SQLiteDatabase
     */
    public SQLiteDatabase getDb(){
        return db;
    }

    /** Check if the underlying SQLiteDatabase is open
     *
     * @return whether the DB is open or not
     */
    public boolean isOpen(){
        return (db!=null&&db.isOpen());
    }


    /** Lowers the DB counter by 1 for any {@link DatabaseManager}s referencing the same DB on disk
     *  <br />If the new counter is 0, then the database will be closed.
     *  <br /><br />This needs to be called before application exit.
     * <br />If the counter is 0, then the underlying SQLiteDatabase is <b>null</b> until another DatabaseManager is instantiated or you call {@link #open()}
     *
     * @return true if the underlying {@link android.database.sqlite.SQLiteDatabase} is closed (counter is 0), and false otherwise (counter > 0)
     */
    public boolean close(){
        sqLiteOpenHelper.removeConnection();
        if (sqLiteOpenHelper.getCounter()==0){
            synchronized (lockObject){
                if (db.inTransaction())db.endTransaction();
                if (db.isOpen())db.close();
                db = null;
            }
            return true;
        }
        return false;
    }
    /** Increments the internal db counter by one and opens the db if needed
    *
    */
    public void open(){
        sqLiteOpenHelper.addConnection();
        if (db==null||!db.isOpen()){
                synchronized (lockObject){
                    db = sqLiteOpenHelper.getWritableDatabase();
                }
        } 
    }
}




sqlite3