在 Android 应用程序中对 SQLite 数据库执行查询时,最佳实践是什么?
从 AsyncTask 的 doInBackground 运行插入、删除和选择查询是否安全?还是我应该使用 UI 线程?我想数据库查询可能是“繁重的”并且不应该使用 UI 线程,因为它可以锁定应用程序 - 导致 Application Not Responding (ANR)。
如果我有多个 AsyncTask,它们应该共享一个连接还是应该各自打开一个连接?
这些场景是否有任何最佳实践?
从多个线程中插入、更新、删除和读取通常都可以,但 Brad 的 answer 不正确。您必须小心创建和使用连接的方式。即使您的数据库没有损坏,在某些情况下您的更新调用也会失败。
基本答案。
SqliteOpenHelper 对象保持一个数据库连接。它似乎为您提供了读写连接,但实际上并没有。调用只读,无论如何你都会得到写数据库连接。
所以,一个助手实例,一个数据库连接。即使您从多个线程中使用它,一次一个连接。 SqliteDatabase 对象使用 java 锁来保持访问序列化。因此,如果 100 个线程有一个 db 实例,则对实际磁盘数据库的调用将被序列化。
所以,一个助手,一个数据库连接,它是用java代码序列化的。一个线程,1000 个线程,如果您使用它们之间共享的一个帮助程序实例,那么您所有的数据库访问代码都是串行的。生活是美好的(ish)。
如果您尝试同时从实际的不同连接写入数据库,则会失败。它不会等到第一个完成后再写入。它根本不会写你的改变。更糟糕的是,如果您没有在 SQLiteDatabase 上调用正确版本的插入/更新,您将不会得到异常。您只会在 LogCat 中收到一条消息,就是这样。
那么,多线程?使用一名助手。时期。如果您知道只有一个线程在写入,您可能可以使用多个连接,并且您的读取速度会更快,但买家要小心。我没有测试那么多。
这是一篇更详细的博客文章和一个示例应用程序。
Android Sqlite 锁定(更新链接 6/18/2012)
Android-Database-Locking-Collisions-Example by touchlab on GitHub
Gray 和我实际上正在打包一个基于他的 Ormlite 的 ORM 工具,该工具与 Android 数据库实现原生配合,并遵循我在博客文章中描述的安全创建/调用结构。那应该很快就出来了。看一看。
与此同时,还有一篇后续博文:
单个 SQLite 连接
还要检查前面提到的锁定示例的 2point0 分叉:
Android-Database-Locking-Collisions-Example by 2point0 on GitHub
并发数据库访问
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 对象时,实际上都是在建立新的数据库连接。如果您尝试同时从实际的不同连接写入数据库,则会失败。 (来自上面的答案)
要使用多线程数据库,我们需要确保我们使用的是一个数据库连接。
让我们创建单例类数据库管理器,它将保存并返回单个 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() 方法中也会发生同样的情况。每次我们调用这个方法时,计数器都会减少,每当它变为零时,我们就会关闭数据库连接。
现在您应该能够使用您的数据库并确保它是线程安全的。
if(instance==null)
的私有构造函数?您别无选择,只能每次调用初始化;您怎么知道它是否已在其他应用程序等中初始化?
initializeInstance()
有一个 SQLiteOpenHelper
类型的参数,但在您的评论中您提到使用 DatabaseManager.initializeInstance(getApplicationContext());
。到底是怎么回事?这怎么可能起作用?
使用 Thread 或 AsyncTask 进行长时间运行的操作 (50ms+)。测试您的应用程序以查看它在哪里。大多数操作(可能)不需要线程,因为大多数操作(可能)只涉及几行。使用线程进行批量操作。
在线程之间为磁盘上的每个数据库共享一个 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
,则必须再次调用 open
,然后才能使用该类的同一实例,或者您可以创建一个新实例。由于在 close
代码中,我设置了 db=null
,因此您将无法使用来自 getDb
的返回值(因为它会为 null),因此如果您执行某项操作,您将得到一个 NullPointerException
像myInstance.close(); myInstance.getDb().query(...);
getDb()
和 open()
组合成一个方法?
数据库非常灵活,具有多线程。我的应用程序同时从许多不同的线程访问它们的数据库,并且效果很好。在某些情况下,我有多个进程同时访问数据库,而且效果也很好。
您的异步任务 - 尽可能使用相同的连接,但如果必须,可以从不同的任务访问数据库。
AsyncTask
s/Thread
上的不同 SQLiteDatabase
对象访问 DB,但有时会导致错误,这就是为什么 SQLiteDatabase(第 1297 行)使用 Lock
s
在为此苦苦挣扎了几个小时后,我发现每次执行 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 是我可以通过帮助程序类将字符串放入数据库的唯一方法。
Dmytro 的回答适用于我的情况。我认为最好将函数声明为同步。至少在我的情况下,否则它将调用空指针异常,例如 getWritableDatabase 尚未在一个线程中返回,而 openDatabse 同时在另一个线程中调用。
public synchronized SQLiteDatabase openDatabase() {
if(mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
我对 SQLiteDatabase API 的理解是,如果你有一个多线程应用程序,你不能承受超过 1 个 SQLiteDatabase 对象指向一个数据库。
该对象肯定可以创建,但如果不同的线程/进程(也)开始使用不同的 SQLiteDatabase 对象(如我们在 JDBC 连接中使用的方式),则插入/更新会失败。
这里唯一的解决方案是坚持使用 1 个 SQLiteDatabase 对象,并且每当在超过 1 个线程中使用 startTransaction() 时,Android 管理跨不同线程的锁定,并且一次只允许 1 个线程具有独占更新访问权限。
您也可以从数据库中“读取”并在不同的线程中使用相同的 SQLiteDatabase 对象(而另一个线程写入)并且永远不会发生数据库损坏,即“读取线程”不会从数据库中读取数据,直到“ write thread" 提交数据,尽管两者都使用相同的 SQLiteDatabase 对象。
这与 JDBC 中的连接对象不同,如果您在读取和写入线程之间传递(使用相同的)连接对象,那么我们也可能会打印未提交的数据。
在我的企业应用程序中,我尝试使用条件检查,以便 UI 线程永远不必等待,而 BG 线程则持有 SQLiteDatabase 对象(独占)。我尝试预测 UI 操作并推迟 BG 线程运行“x”秒。也可以维护 PriorityQueue 来管理分发 SQLiteDatabase Connection 对象,以便 UI 线程首先获得它。
"read thread" wouldn't read the data from the database till the "write thread" commits the data although both use the same SQLiteDatabase object
。这并非总是如此,如果您在“写入线程”之后启动“读取线程”,您可能无法获得新更新的数据(在写入线程中插入或更新)。读线程可能在写线程开始之前读取数据。发生这种情况是因为写操作最初启用保留锁而不是排他锁。
您可以在 Google I/O 2017 上尝试应用新的架构方法anounced。
它还包括名为 Room 的新 ORM 库
它包含三个主要组件:@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();
}
遇到了一些问题,我想我已经明白为什么我出错了。
我编写了一个数据库包装类,其中包括一个 close()
,它调用助手关闭作为 open()
的镜像,它调用 getWriteableDatabase,然后迁移到 ContentProvider
。 ContentProvider
的模型不使用 SQLiteDatabase.close()
,我认为这是一个很大的线索,因为代码确实使用了 getWriteableDatabase
rawQuery 模型。
我使用单例,关闭文档中有一点不祥的评论
关闭任何打开的数据库对象
(我的粗体字)。
所以我有间歇性的崩溃,我使用后台线程访问数据库,它们与前台同时运行。
因此,我认为 close()
会强制关闭数据库,而不管任何其他持有引用的线程 - 所以 close()
本身并不是简单地撤消匹配的 getWriteableDatabase
而是强制关闭 any 打开请求。大多数时候这不是问题,因为代码是单线程的,但在多线程的情况下,总是有可能打开和关闭不同步。
阅读了其他地方的评论,解释了 SqLiteDatabaseHelper 代码实例很重要,那么您唯一想要关闭的时间就是您想要进行备份副本的情况,并且您想要强制关闭所有连接并强制 SqLite写掉任何可能徘徊的缓存内容 - 换句话说,停止所有应用程序数据库活动,关闭以防帮助程序丢失轨道,执行任何文件级活动(备份/恢复)然后重新开始。
虽然尝试以受控方式关闭听起来是个好主意,但现实情况是 Android 保留丢弃您的 VM 的权利,因此任何关闭都可以降低未写入缓存更新的风险,但不能保证设备是否压力很大,并且如果您正确释放了游标和对数据库的引用(不应是静态成员),那么帮助程序无论如何都会关闭数据库。
所以我的看法是,方法是:
使用 getWriteableDatabase 从单例包装器打开。 (我使用派生的应用程序类从静态提供应用程序上下文以解决对上下文的需求)。
永远不要直接调用close。
永远不要将生成的数据库存储在任何没有明显范围的对象中,并依赖引用计数来触发隐式 close()。
如果进行文件级处理,请停止所有数据库活动,然后调用 close 以防万一有一个失控的线程,假设您编写了正确的事务,因此失控的线程将失败,并且关闭的数据库至少会有正确的事务而不是而不是部分事务的文件级副本。
我知道响应迟了,但在 android 中执行 sqlite 查询的最佳方法是通过自定义内容提供程序。这样,UI 与数据库类(扩展 SQLiteOpenHelper 类的类)分离。查询也在后台线程(游标加载器)中执行。