首页 文章

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

提问于
浏览
645

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

从AsyncTask的doInBackground运行插入,删除和选择查询是否安全?或者我应该使用UI线程?我想数据库查询可以是"heavy",不应该使用UI线程,因为它可以锁定应用程序 - 导致Application Not Responding(ANR) .

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

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

10 回答

  • 180
    • 使用 ThreadAsyncTask 进行长时间运行(50ms) . 测试您的应用以查看它的位置 . 大多数操作(可能)不需要线程,因为大多数操作(可能)只涉及几行 . 使用线程进行批量操作 .

    • 为线程之间的磁盘上的每个数据库共享一个 SQLiteDatabase 实例,并实现计数系统以跟踪打开的连接 .

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

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

    我的解决方案:

    对于最新版本,请参阅https://github.com/JakarCo/databasemanager,但我也会尝试在此处更新代码 . 如果您想了解我的解决方案,请查看代码并阅读我的笔记 . 我的笔记通常非常有帮助 .

    • 将代码复制/粘贴到名为 DatabaseManager 的新文件中 . (或从github下载)

    • 延伸 DatabaseManager 并像往常一样实施 onCreateonUpgrade . 您可以创建一个 DatabaseManager 类的多个子类,以便在磁盘上具有不同的数据库 .

    • 实例化您的子类并调用 getDb() 以使用 SQLiteDatabase 类 .

    • 为您实例化的每个子类调用 close()

    copy/paste 的代码:

    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
         *  
    If the new counter is 0, then the database will be closed. *

    This needs to be called before application exit. *
    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(); } } } }
  • 0

    并发数据库访问

    Same article on my blog(I like formatting more)

    我写了一篇小文章,描述了如何安全地访问你的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对象时,实际上都在 Build 新的数据库连接 . 如果您尝试同时从实际的不同连接写入数据库,则会失败 . (从上面的回答)

    要使用具有多个线程的数据库,我们需要确保使用一个数据库连接 .

    让我们制作单例类数据库管理器,它将保存并返回单个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崩溃 .

    我们需要确保没有人使用数据库,然后才关闭它 . 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()方法中也是如此 . 每次调用此方法时,计数器都会减少,只要它变为零,我们就会关闭数据库连接 .


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

  • 4

    Dmytro的答案适用于我的案例 . 我认为最好将该函数声明为synchronized . 至少在我的情况下,它会调用空指针异常,否则,例如, getWritableDatabase尚未在一个线程中返回,而openDatabse同时在另一个线程中调用 .

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

    遇到了一些问题,我想我已经理解了为什么我出错了 .

    我编写了一个数据库包装类,其中包含一个 close() ,它将helper关闭为 open() 的镜像,调用getWriteableDatabase,然后迁移到 ContentProvider . ContentProvider 的模型不使用 SQLiteDatabase.close() ,我认为这是一个很大的线索,因为代码确实使用 getWriteableDatabase 在某些情况下,我仍然在进行直接访问(主要的屏幕验证查询,所以我迁移到getWriteableDatabase / rawQuery模型 .

    我使用单身人士而且有在密切的文档中略有不祥的评论

    关闭所有打开的数据库对象

    (我的粗体) .

    所以我有间歇性的崩溃,我使用后台线程来访问数据库,它们与前台同时运行 .

    所以我认为 close() 强制数据库关闭而不管任何其他线程持有引用 - 所以 close() 本身不是简单地撤消匹配 getWriteableDatabase 而是强制关闭 any 打开请求 . 大多数情况下,这不是问题,因为代码是单线程,但在多线程情况下,总是有可能打开和关闭不同步 .

    读过其他地方的注释,解释了SqLiteDatabaseHelper代码实例的重要性,那么您希望关闭的唯一时间就是您希望进行备份副本的情况,并且您希望强制关闭所有连接并强制SqLite写下任何可能正在游荡的缓存内容 - 换句话说,停止所有应用程序数据库活动,关闭以防Helper丢失跟踪,执行任何文件级别活动(备份/恢复)然后重新开始 .

    尽管尝试以受控方式关闭听起来是个好主意,但事实是Android保留了删除VM的权利,因此任何关闭都可以降低未写入缓存更新的风险,但如果设备无法保证强调,如果你已正确释放你的游标和对数据库的引用(不应该是静态成员),那么帮助程序无论如何都将关闭数据库 .

    所以我的看法是:方法是:

    使用getWriteableDatabase从单例包装器打开 . (我使用派生的应用程序类从静态提供应用程序上下文来解决对上下文的需求) .

    永远不要直接打电话 .

    永远不要将结果数据库存储在任何没有明显范围的对象中,并依赖引用计数来触发隐式close() .

    如果进行文件级别处理,将所有数据库活动停止,然后调用close,以防万一有一个失控的线程,假设您编写了正确的事务,因此失控的线程将失败,并且关闭的数据库将至少具有正确的事务而不是可能是部分交易的文件级副本 .

  • 10

    我对SQLiteDatabase API的理解是,如果你有一个多线程应用程序,你不能有超过1个SQLiteDatabase对象指向一个数据库 .

    绝对可以创建对象,但如果不同的线程/进程(也)开始使用不同的SQLiteDatabase对象(比如我们在JDBC Connection中的使用方式),则插入/更新会失败 .

    这里唯一的解决方案是坚持使用1个SQLiteDatabase对象,每当在多个线程中使用startTransaction()时,Android就会跨不同的线程管理锁定,并且一次只允许1个线程拥有独占的更新访问权限 .

    你也可以从数据库中“读取”并在不同的线程中使用相同的SQLiteDatabase对象(而另一个线程写入)并且永远不会有数据库损坏,即“读取线程”不会从数据库中读取数据,直到“写线程“提交数据虽然两者都使用相同的SQLiteDatabase对象 .

    这与JDBC中的连接对象的不同之处在于,如果您在读写线程之间传递(使用相同的)连接对象,那么我们可能也会打印未提交的数据 .

    在我的企业应用程序中,我尝试使用条件检查,以便UI线程永远不必等待,而BG线程保持SQLiteDatabase对象(独占) . 我尝试预测UI动作并推迟BG线程运行'x'秒 . 还可以维护PriorityQueue来管理分发SQLiteDatabase Connection对象,以便UI Thread首先获取它 .

  • 3

    您可以尝试在Google I / O 2017上应用新的架构方法anounced .

    它还包括名为Room的新ORM库

    它包含三个主要组件:@ Entity,@ Dao和@Database

    User.java

    @Entity
    public class User {
      @PrimaryKey
      private int uid;
    
      @ColumnInfo(name = "first_name")
      private String firstName;
    
      @ColumnInfo(name = "last_name")
      private String lastName;
    
      // Getters and setters are ignored for brevity,
      // but they're required for Room to work.
    }
    

    UserDao.java

    @Dao
    public interface UserDao {
      @Query("SELECT * FROM user")
      List<User> getAll();
    
      @Query("SELECT * FROM user WHERE uid IN (:userIds)")
      List<User> loadAllByIds(int[] userIds);
    
      @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
           + "last_name LIKE :last LIMIT 1")
      User findByName(String first, String last);
    
      @Insert
      void insertAll(User... users);
    
      @Delete
      void delete(User user);
    }
    

    AppDatabase.java

    @Database(entities = {User.class}, version = 1)
    public abstract class AppDatabase extends RoomDatabase {
      public abstract UserDao userDao();
    }
    
  • 3

    数据库非常灵活,具有多线程功能 . 我的应用程序同时从许多不同的线程中击中了他们的数据库,它确实很好 . 在某些情况下,我有多个进程同时命中数据库,并且工作正常 .

    您的异步任务 - 尽可能使用相同的连接,但如果必须,可以从不同的任务访问数据库 .

  • 607

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

    The basic answer.

    SqliteOpenHelper对象保留一个数据库连接 . 它似乎为您提供读写连接,但它确实没有 . 调用只读,你就会得到写入无论数据库连接 .

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

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

    如果您尝试同时从实际的不同连接写入数据库,则会失败 . 它不会等到第一个完成然后写 . 它根本不会写下您的更改 . 更糟糕的是,如果您没有在SQLiteDatabase上调用正确版本的插入/更新,则不会出现异常 . 您只需在LogCat中收到一条消息即可 .

    那么,多线程?使用一个帮手 . 期 . 如果你知道只有一个线程会写,你可以使用多个连接,你的读取会更快,但买家要小心 . 我没有测试那么多 .

    这是一篇博文,里面有更详细的内容和一个示例应用 .

    格雷和我实际上是根据他的Ormlite包装一个ORM工具,该工具本身与Android数据库实现一起工作,并遵循我在博客文章中描述的安全创建/调用结构 . 那应该很快就会出来 . 看一看 .


    与此同时,还有一篇跟进博客文章:

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

    _999Android-Database-Locking-Collisions-Example by 2point0在GitHub上

  • 6

    我知道响应很晚,但在android中执行sqlite查询的最佳方法是通过自定义内容提供程序 . 通过这种方式,UI与数据库类(扩展SQLiteOpenHelper类的类)分离 . 查询也在后台线程(Cursor Loader)中执行 .

  • 16

    经过几个小时的努力,我发现每个数据库执行只能使用一个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是我通过我的帮助器类将我的字符串放入数据库的唯一方法 .

相关问题