协慌网

登录 贡献 社区

Android 上 SQLite 的最佳做法是什么?

在 Android 应用程序中的 SQLite 数据库上执行查询时,最佳做法是什么?

从 AsyncTask 的 doInBackground 中运行插入,删除和选择查询是否安全?还是应该使用 UI 线程?我想数据库查询可能 “繁重”,并且不应该使用 UI 线程,因为它可以锁定应用程序 - 导致应用程序无响应(ANR)。

如果我有几个 AsyncTask,它们应该共享一个连接还是应该每个打开一个连接?

这些方案是否有最佳实践?

答案

从多个线程进行插入,更新,删除和读取通常是可以的,但是 Brad 的答案是不正确的。您必须谨慎创建和使用连接。在某些情况下,即使数据库没有损坏,更新调用也会失败。

基本答案。

SqliteOpenHelper 对象可保持一个数据库连接。它似乎为您提供了读写连接,但实际上没有。调用只读,无论如何,您都将获得写数据库连接。

因此,一个帮助程序实例,一个数据库连接。即使您从多个线程使用它,一次也只有一个连接。 SqliteDatabase 对象使用 Java 锁来保持访问序列化。因此,如果 100 个线程有一个数据库实例,则对实际磁盘数据库的调用将被序列化。

因此,一个帮助程序和一个数据库连接已通过 Java 代码进行了序列化。一个线程,1000 个线程,如果您使用它们之间共享的一个帮助程序实例,则所有数据库访问代码都是串行的。生活是美好的(愉快的)。

如果尝试同时从实际的不同连接写入数据库,则连接将失败。它不会等到第一个完成后再写。它根本不会写您的更改。更糟糕的是,如果您没有在 SQLiteDatabase 上调用正确版本的 insert / update,您将不会获得异常。您只会在 LogCat 中收到一条消息,仅此而已。

那么,多个线程?使用一个助手。时期。如果您知道只有一个线程在写,则您可能可以使用多个连接,并且读取速度会更快,但请注意。我还没测试那么多。

这是一篇博客文章,其中包含更多详细信息和示例应用程序。

Gray 和我实际上是在基于 Ormlite 的基础上包装一个 ORM 工具,该工具可与 Android 数据库实现本地兼容,并遵循我在博客文章中描述的安全的创建 / 调用结构。那应该很快出来。看一看。


同时,有一个后续博客文章:

还要通过前面提到的锁定示例的2point0 来检查 fork:

并发数据库访问

同一篇文章在我的博客上(我喜欢格式化)

我写了一篇小文章,描述了如何确保对您的 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()用于线程 1线程返回SQLiteDatabase对象的同一个实例。发生了什么, Thread1可能关闭了数据库,而Thread2仍在使用它。这就是为什么我们有IllegalStateException崩溃的原因。

我们需要确保没有人使用数据库,然后才关闭它。一些使用 stackoveflow 的人建议不要关闭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()方法中也会发生同样的情况。每次调用此方法时,counter 都会减少,当计数器变为零时,我们将关闭数据库连接。


现在,您应该能够使用数据库,并确保它是线程安全的。

  • 使用ThreadAsyncTask进行长时间运行的操作(50ms +)。测试您的应用,看看它在哪里。大多数操作(可能)不需要线程,因为大多数操作(可能)仅涉及几行。使用线程进行批量操作。
  • 在线程之间为磁盘上的每个数据库共享一个SQLiteDatabase实例,并实现一个计数系统来跟踪打开的连接。

这些方案是否有最佳实践?

在所有类之间共享一个静态字段。我过去经常为这个和其他需要共享的东西保持单身。还应使用计数方案(通常使用 AtomicInteger)来确保您永远不会过早关闭数据库或将其保持打开状态。

我的解决方案:

我编写的旧版本可从https://github.com/Taeluf/dev/tree/main/archived/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();
                }
        } 
    }
}