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

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

如果我有几个asynctask,他们应该共享一个连接还是应该各自打开一个连接?

对于这些场景有什么最佳实践吗?


数据库在多线程方面非常灵活。我的应用程序从许多不同的线程同时击中它们的db,它做得很好。在某些情况下,我有多个进程同时访问数据库,这也很好。

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


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

最基本的答案。

SqliteOpenHelper对象保留一个数据库连接。它看似为您提供了读写连接,但实际上并非如此。调用read-only,无论如何都将获得写数据库连接。

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

一个helper,一个db连接,在java代码中序列化。一个线程,1000个线程,如果你使用一个helper实例在它们之间共享,你所有的db访问代码都是串行的。生活是美好的。

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

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

这里有一篇博客文章提供了更多的细节和一个示例应用程序。

Android Sqlite锁定(更新链接6/18/2012) android - database - lock - collisions - example by touchlab on GitHub

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


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

单个SQLite连接

同样签出前面提到的锁定示例的2point0的fork:

Android-Database-Locking-Collisions-Example by 2point0 on GitHub


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

对象当然可以创建,但是如果不同的线程/进程(也)开始使用不同的SQLiteDatabase对象(就像我们在JDBC Connection中使用的那样),则插入/更新会失败。

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

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

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

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


在纠结了几个小时之后,我发现每次db执行只能使用一个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();

每次循环迭代时创建一个新的DBAdapter是我通过助手类将字符串放入数据库的唯一方法。


并发数据库访问

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

我写了一篇小文章,描述了如何让访问你的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();

您将在日志中得到以下消息,并且您的一个更改将不会被写入。

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

这是因为每次创建新的SQLiteOpenHelper对象时,实际上都在建立新的数据库连接。如果您试图同时从不同的实际连接写入数据库,其中一个将失败。(从上面的答案)

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

让我们创建一个单例类Database Manager,它将保存并返回一个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,这意味着我们需要创建新的数据库连接,如果不等于1,则数据库连接已经创建。

同样的情况也发生在closeDatabase()方法中。每次调用此方法时,计数器都会减小,当它趋于0时,我们将关闭数据库连接。


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


德米特罗的回答很适合我。 我认为最好将函数声明为同步的。至少在我的情况下,它会调用空指针异常,否则,例如getWritableDatabase尚未返回在一个线程和opendatabase在另一个线程同时调用。

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

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

对于这些场景有什么最佳实践吗?

在所有类之间共享一个静态字段。我过去常常保持一个单例,用于那些需要共享的东西。还应该使用计数方案(通常使用AtomicInteger)来确保不会过早关闭数据库或让数据库保持打开状态。

我的解决方案:

我写的旧版本可以在https://github.com/Taeluf/dev/tree/main/archived/databasemanager上找到,并且没有维护。如果您想了解我的解决方案,请查看代码并阅读我的注释。我的笔记通常都很有用。

将代码复制/粘贴到名为DatabaseManager的新文件中。(或从github下载) 扩展DatabaseManager和实现onCreate和onUpgrade就像你通常会。您可以为一个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();
                }
        } 
    }
}

在经历了一些问题之后,我想我已经明白为什么我走错了。

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

我使用了一个单例,在关闭的文档中有一些不祥的注释

关闭任何打开的数据库对象

(我的粗体)。

因此,当我使用后台线程访问数据库时,它们与前台同时运行,就会出现间歇性崩溃。

因此,我认为close()强制数据库关闭,而不管是否有其他线程持有引用——因此close()本身不是简单地撤销匹配的getWriteableDatabase,而是强制关闭任何打开的请求。大多数情况下,这不是问题,因为代码是单线程的,但在多线程的情况下,总是有机会打开和关闭不同步。

Having read comments elsewhere that explains that the SqLiteDatabaseHelper code instance counts, then the only time you want a close is where you want the situation where you want to do a backup copy, and you want to force all connections to be closed and force SqLite to write away any cached stuff that might be loitering about - in other words stop all application database activity, close just in case the Helper has lost track, do any file level activity (backup/restore) then start all over again.

虽然这听起来像一个好主意,尝试和关闭在受控的方式,现实是Android保留垃圾你的虚拟机的权利,所以任何关闭都是减少缓存更新不被写入的风险,但不能保证如果设备是有压力的,如果你已经正确释放游标和数据库引用(不应该是静态成员),那么助手将关闭数据库无论如何。

所以我的看法是:

使用getWriteableDatabase从单例包装器打开。(我使用了一个派生的应用程序类来提供应用程序上下文,从而解决了对上下文的需求)。

永远不要直接接近。

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

如果进行文件级处理,请暂停所有数据库活动,然后调用close,以防出现失控线程,假设您编写了适当的事务,那么失控线程将失败,关闭的数据库至少将拥有适当的事务,而不是部分事务的文件级副本。


您可以尝试应用谷歌I/O 2017上宣布的新架构方法。

它还包括新的ORM库Room

它包含三个主要组件:@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();
}

我知道响应晚了,但在android中执行sqlite查询的最佳方式是通过自定义内容提供程序。这样,UI就与数据库类(扩展SQLiteOpenHelper类的类)解耦了。此外,查询在后台线程(游标加载器)中执行。