在Android应用程序中对SQLite数据库执行查询时,什么被认为是最佳实践?
从AsyncTask的doInBackground中运行插入、删除和选择查询是否安全?或者我应该使用UI线程?我认为数据库查询可能是“沉重的”,不应该使用UI线程,因为它可以锁定应用程序-导致应用程序无响应(ANR)。
如果我有几个asynctask,他们应该共享一个连接还是应该各自打开一个连接?
对于这些场景有什么最佳实践吗?
在Android应用程序中对SQLite数据库执行查询时,什么被认为是最佳实践?
从AsyncTask的doInBackground中运行插入、删除和选择查询是否安全?或者我应该使用UI线程?我认为数据库查询可能是“沉重的”,不应该使用UI线程,因为它可以锁定应用程序-导致应用程序无响应(ANR)。
如果我有几个asynctask,他们应该共享一个连接还是应该各自打开一个连接?
对于这些场景有什么最佳实践吗?
当前回答
并发数据库访问
同样的文章在我的博客上(我更喜欢格式化)
我写了一篇小文章,描述了如何让访问你的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时,我们将关闭数据库连接。
现在您应该能够使用数据库并确保它是线程安全的。
其他回答
数据库在多线程方面非常灵活。我的应用程序从许多不同的线程同时击中它们的db,它做得很好。在某些情况下,我有多个进程同时访问数据库,这也很好。
你的异步任务-当你可以使用相同的连接,但如果你必须,它可以从不同的任务访问数据库。
在纠结了几个小时之后,我发现每次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是我通过助手类将字符串放入数据库的唯一方法。
我对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线程首先获得它。
德米特罗的回答很适合我。 我认为最好将函数声明为同步的。至少在我的情况下,它会调用空指针异常,否则,例如getWritableDatabase尚未返回在一个线程和opendatabase在另一个线程同时调用。
public synchronized SQLiteDatabase openDatabase() {
if(mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
并发数据库访问
同样的文章在我的博客上(我更喜欢格式化)
我写了一篇小文章,描述了如何让访问你的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时,我们将关闭数据库连接。
现在您应该能够使用数据库并确保它是线程安全的。