代码之家  ›  专栏  ›  技术社区  ›  Vidar Vestnes

Android上的sqlite的最佳实践是什么?

  •  656
  • Vidar Vestnes  · 技术社区  · 16 年前

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

    从异步任务的doinbackground中运行插入、删除和选择查询是否安全?还是应该使用UI线程?我认为数据库查询可以是“重的”,不应该使用UI线程,因为它可以锁定应用程序,从而导致 Application Not Responding (ANR)。

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

    对于这些场景,是否有任何最佳实践?

    10 回复  |  直到 7 年前
        1
  •  614
  •   Uddhav P. Gautam    8 年前

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

    基本答案。

    sqliteOpenHelper对象保留一个数据库连接。它似乎为您提供了一个读写连接,但实际上没有。调用只读,您将获得写数据库连接,而不管如何。

    所以,一个助手实例,一个数据库连接。即使您从多个线程使用它,一次一个连接。SQLeDATABAASE对象使用Java锁来保持访问序列化。因此,如果100个线程有一个DB实例,那么对实际磁盘数据库的调用将被序列化。

    因此,一个助手,一个DB连接,在Java代码中被序列化。一个线程,1000个线程,如果使用它们之间共享的一个助手实例,那么所有的DB访问代码都是串行的。生活是美好的。

    如果您试图同时从实际的不同连接写入数据库,其中一个将失败。它不会等到第一次写完再写。它不会写你的零钱。更糟的是,如果您不在sqliteDatabase上调用正确版本的insert/update,则不会得到异常。您只需在日志cat中收到一条消息,就可以了。

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

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

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


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

    同时检查叉子 2点0 前面提到的锁定示例:

        2
  •  183
  •   Dmytro Danylyk    7 年前

    并发数据库访问

    Same article on my blog(I like formatting more)

    我写了一篇小文章,描述了如何保证Android数据库线程的安全。


    假设你有自己的 存取资料表 .

    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();
    

    您将在日志cat中收到以下消息,其中一个更改将不会被写入。

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

    这是因为每次你创造新的 存取资料表 对象,您实际上正在建立新的数据库连接。如果您试图同时从实际的不同连接写入数据库,其中一个将失败。(来自上述答案)

    要将数据库与多个线程一起使用,我们需要确保使用一个数据库连接。

    让我们来做单件课程 数据库管理器 它将保持和返回单一 存取资料表 对象。

    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 线程2 . 发生了什么事, 螺纹1 可以关闭数据库,而 线程2 仍在使用。这就是为什么我们 非法状态例外 撞车。

    我们需要确保没有人使用数据库,然后关闭它。StackoveFlow上的一些人建议永远不要关闭 数据操作 . 这将导致以下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
    

    每次需要数据库时,都应该调用 OpenDATABASE() 方法 数据库管理器 班级。在这个方法中,我们有一个计数器,它指示打开数据库的次数。如果它等于1,则意味着我们需要创建新的数据库连接,如果不是,则数据库连接已经创建。

    同样的情况也发生在 CuffEdababase[() 方法。每次调用此方法时,计数器都会减少,每当计数器变为零时,我们将关闭数据库连接。


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

        3
  •  16
  •   Reed    11 年前
    • 使用A Thread AsyncTask 对于长时间运行的操作(50ms以上)。测试你的应用程序,看看它在哪里。大多数操作(可能)不需要线程,因为大多数操作(可能)只涉及几行。使用线程进行批量操作。
    • 分享一 SQLiteDatabase 在线程之间的磁盘上的每个数据库实例,并实现一个计数系统来跟踪打开的连接。

    对于这些场景,是否有任何最佳实践?

    在所有类之间共享一个静态字段。我过去常常为这个和其他需要分享的事情而保留一个单身汉。计数方案(通常使用atomicinteger)也应该用于确保永远不要过早关闭数据库或将其保持打开状态。

    我的解决方案:

    有关最新版本,请参见 https://github.com/JakarCo/databasemanager 但我也会尽量使代码保持最新。如果您想理解我的解决方案,请看代码并阅读我的注释。我的笔记通常很有用。

    1. 将代码复制/粘贴到名为 DatabaseManager . (或从Github下载)
    2. 延伸 数据库管理器 实施 onCreate onUpgrade 就像你平时一样。您可以创建一个的多个子类 数据库管理器 类,以便在磁盘上具有不同的数据库。
    3. 实例化子类并调用 getDb() 使用 数据操作 班级。
    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 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();
                    }
            } 
        }
    }
    
        4
  •  10
  •   Brad Hein    16 年前

    数据库具有非常灵活的多线程功能。我的应用程序同时从多个不同的线程命中了它们的DBS,而且效果很好。在某些情况下,我有多个进程同时命中数据库,这也很好。

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

        5
  •  6
  •   gonglong    12 年前

    德米特洛的回答对我的案件很有效。 我认为最好将函数声明为synchronized。至少在我的情况下,它会调用空指针异常,否则,例如getwritabledatabase尚未在一个线程中返回,而opendatabse同时在另一个线程中调用。

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

    我对sqliteDatabase API的理解是,如果您有一个多线程的应用程序,那么您就不能拥有多个指向单个数据库的sqliteDatabase对象。

    当然可以创建该对象,但是如果不同的线程/进程也开始使用不同的sqliteDatabase对象(比如我们在JDBC连接中的使用方式),则插入/更新将失败。

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

    另外,您可以从数据库中执行“读取”,并在不同的线程中使用相同的sqliteDatabase对象(而另一个线程写入),并且不会出现数据库损坏,即“读取线程”在“写入线程”提交数据之前不会从数据库中读取数据,尽管两者都使用相同的sqliteDatabase对象。

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

    在我的企业应用程序中,我尝试使用条件检查,以便UI线程不必等待,而bg线程持有sqliteDatabase对象(独占)。我尝试预测用户界面操作,并将bg线程的运行延迟“x”秒。还可以维护PriorityQueue来管理分发sqliteDatabase连接对象,以便UI线程首先获取它。

        7
  •  4
  •   dell116    15 年前

    在为此挣扎了几个小时之后,我发现每个数据库执行只能使用一个db helper对象。例如,

    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();
    

    每次循环迭代时创建一个新的数据库适配器是我通过助手类将字符串获取到数据库中的唯一方法。

        8
  •  3
  •   Ian Spencer    9 年前

    有过一些问题,我想我已经明白为什么我会出错。

    我编写了一个数据库包装类,其中包括 close() 它称助手为 open() 它调用了GetWriteableDatabase,然后迁移到 ContentProvider . 模型 内容提供者 不使用 SQLiteDatabase.close() 我认为这是一个很大的线索,因为代码确实使用了 getWriteableDatabase 在某些情况下,我仍在进行直接访问(主界面中的屏幕验证查询,因此我迁移到了getwriteabledatabase/rawquery模型中)。

    我用的是单件的,在近距离的文档中有一些不祥的评论。

    关闭 任何 打开数据库对象

    (我大胆)。

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

    所以我想 关闭() 强制关闭数据库,而不管其他线程是否包含引用-所以 关闭() 本身并不是简单地取消匹配 获取可写数据库 但是强行关闭 任何 打开请求。大多数时候这不是问题,因为代码是单线程的,但是在多线程的情况下,总是有机会打开和关闭不同步。

    在阅读了其他解释sqliteDatabaseHelper代码实例计数的注释之后,您唯一想要关闭的时间就是您想要执行备份副本的情况,并且您想要强制关闭所有连接,并强制sqlite写下任何可能在徘徊的缓存内容-换句话说,st操作所有应用程序数据库活动,关闭以防帮助程序丢失跟踪,执行任何文件级活动(备份/还原),然后重新启动。

    虽然尝试以受控方式关闭听起来是个不错的主意,但现实情况是,Android保留对虚拟机进行垃圾处理的权利,因此任何关闭都会降低缓存更新未被写入的风险,但如果设备受到压力,以及如果您正确释放了光标和对数据库的引用(这是不应是静态成员),则帮助程序仍将关闭数据库。

    所以我的看法是:

    使用GetWriteableDatabase从单个包装打开。(我使用派生的应用程序类从静态提供应用程序上下文以解决对上下文的需求)。

    不要直接呼叫关闭。

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

    如果执行文件级处理,请停止所有数据库活动,然后调用close,以防出现失控线程,前提是您编写了正确的事务,这样失控线程将失败,关闭的数据库将至少具有正确的事务,而不是可能的部分事务的文件级副本。

        9
  •  3
  •   David stands with Ukraine    9 年前

    您可以尝试应用新的体系结构方法 anounced 在2017年的谷歌I/O上。

    它还包括新的ORM库 Room

    它包含三个主要组件:@entity、@dao和@database

    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.
    }
    

    用户道.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);
    }
    

    应用数据库.java

    @Database(entities = {User.class}, version = 1)
    public abstract class AppDatabase extends RoomDatabase {
      public abstract UserDao userDao();
    }
    
        10
  •  0
  •   Theo    8 年前

    我知道响应很晚,但是在Android中执行sqlite查询的最佳方法是通过自定义内容提供程序。这样,用户界面就与数据库类(扩展sqliteOpenHelper类的类)分离了。查询也在后台线程(光标加载程序)中执行。