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

在Android应用程序中对SQLite数据库执行查询时,什么会被视为最佳实践?

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

如果我有几个AsyncTasks,他们应该共享一个连接还是应该分别打开一个连接?

这些场景是否有最佳做法?


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

基本答案。

SqliteOpenHelper对象拥有一个数据库连接。 它似乎为您提供读写连接,但实际上并没有。 调用只读,无论如何你都会得到写数据库连接。

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

所以,一个帮助程序,一个数据库连接,它在java代码中被序列化。 一个线程,1000个线程,如果您使用它们之间共享的一个帮助程序实例,则您的所有数据库访问代码都是串行的。 生命是好的(ish)。

如果您尝试同时从实际不同的连接写入数据库,则会失败。 它不会等到第一个完成后才写。 它不会写你的改变。 更糟的是,如果你没有在SQLiteDatabase上调用正确版本的insert / update,你将不会得到异常。 你只需要在你的LogCat中获得一条消息,就是这样。

那么,多线程? 使用一名助手。 期。 如果您知道只有一个线程正在写入,您可能可以使用多个连接,并且您的读取速度会更快,但买方要小心。 我没有测试那么多。

这里有一个更详细的博客文章和一个示例应用程序。

  • Android Sqlite锁定(更新链接6/18/2012)
  • Android数据库锁定 - 碰撞 - 通过GitHub上的touchlab实例
  • Gray和我实际上正在包装一个基于Ormlite的ORM工具,该工具本身与Android数据库实现一起工作,并遵循我在博客文章中描述的安全创建/调用结构。 这应该很快就会出来。 看一看。


    与此同时,还有一篇后续博文:

  • 单个SQLite连接
  • 还可以通过前面提到的锁定示例的2point0来检出叉:

  • Android-Database-Locking-Collisions-GitHub上的2point0示例

  • 并发数据库访问

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

    我写了一篇描述如何使你的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()为Thread1和Thread2返回SQLiteDatabase对象的相同实例。 发生什么事情,Thread1可能会关闭数据库,而Thread2仍在使用它。 这就是为什么我们有IllegalStateException崩溃。

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


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


  • 使用ThreadAsyncTask进行长时间运行(50ms +)。 测试你的应用,看看它在哪里。 大多数操作(可能)不需要线程,因为大多数操作(可能)只涉及几行。 使用线程进行批量操作。
  • 在线程之间为磁盘上的每个DB共享一个SQLiteDatabase实例,并实现一个计数系统以跟踪打开的连接。
  • 这些场景是否有最佳做法?

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

    我的解决方案

    对于最新版本,请参阅https://github.com/JakarCo/databasemanager,但我会尽力让代码保持最新。 如果您想了解我的解决方案,请查看代码并阅读我的笔记。 我的笔记通常很有帮助。

  • 将代码复制/粘贴到名为DatabaseManager的新文件中。 (或从github下载)
  • 扩展DatabaseManager并像通常那样实现onCreateonUpgrade 。 您可以创建一个DatabaseManager类的多个子类以便在磁盘上拥有不同的数据库。
  • 实例化你的子类并调用getDb()来使用SQLiteDatabase类。
  • 为实例化的每个子类调用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 support@androidsqlitelibrary.com (or support@jakar.co )
     * 
     * 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();
                    }
            } 
        }
    }
    
    链接地址: http://www.djcxy.com/p/19747.html

    上一篇: What are the best practices for SQLite on Android?

    下一篇: Is there any NoSQL database as simple as SQLite?