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

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

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

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


当前回答

我对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线程首先获得它。

其他回答

并发数据库访问

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

我写了一篇小文章,描述了如何让访问你的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时,我们将关闭数据库连接。


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

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

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

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

我对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线程首先获得它。

您可以尝试应用谷歌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();
}