在Android应用程序中对SQLite数据库执行查询时,最佳做法是什么?
从AsyncTask的doInBackground运行插入,删除和选择查询是否安全?或者我应该使用UI线程?我认为数据库查询可能"很重",不应该使用UI线程,因为它可以锁定应用程序 - 导致应用程序无响应(ANR).
如果我有几个AsyncTasks,他们应该共享一个连接还是应该分别打开一个连接?
这些方案是否有最佳实践?
插入,更新,删除和读取通常可以从多个线程中获得,但Brad的答案不正确.您必须小心如何创建连接并使用它们.在某些情况下,即使您的数据库没有损坏,您的更新调用也会失败.
基本答案.
SqliteOpenHelper对象保留一个数据库连接.它似乎为您提供读写连接,但它确实没有.调用只读,无论如何都将获得写数据库连接.
所以,一个帮助器实例,一个数据库连接.即使您从多个线程使用它,一次一个连接.SqliteDatabase对象使用java锁来保持序列化访问.因此,如果100个线程有一个数据库实例,则对实际磁盘数据库的调用将被序列化.
所以,一个帮助器,一个db连接,在java代码中序列化.一个线程,1000个线程,如果您使用它们之间共享的一个帮助程序实例,则所有数据库访问代码都是串行的.生活是美好的(ish).
如果您尝试同时从实际的不同连接写入数据库,则会失败.它不会等到第一个完成然后写.它根本不会写你的改变.更糟的是,如果你不叫插入/更新的正确版本的SQLiteDatabase,你不会得到一个例外.您只需在LogCat中收到一条消息即可.
那么,多线程?使用一个帮手.期.如果你知道只有一个线程会写,你可以使用多个连接,并读取你会更快,但买家当心.我没有测试那么多.
这是一篇博客文章,其中包含更多详细信息和示例应用.
Android Sqlite锁定(更新链接6/18/2012)
Android-Database-Locking-Collisions-在GitHub上的touchlab示例
格雷和我实际上结束了ORM工具,根据脱下Ormlite,这与Android数据库实现原生支持,并遵循安全创建/调用结构我在博客中描述的.那应该很快就会出来.看一看.
与此同时,还有一篇跟进博客文章:
单个SQLite连接
还可以通过前面提到的锁定示例的2point0来检查fork :
Android-Database-Locking-Collisions- GitHub上的2point0示例
我的博客上的文章相同(我更喜欢格式化)
我写了一篇小文章,描述了如何安全地访问你的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()返回同一个实例SQLiteDatabase的对象线程1和线程2.发生了什么,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()方法中也是如此.每次调用此方法时,计数器都会减少,只要它变为零,我们就会关闭数据库连接.
现在您应该能够使用您的数据库并确保它的线程安全.
使用Thread
或AsyncTask
用于长时间运行(50ms +).测试您的应用以查看它的位置.大多数操作(可能)不需要线程,因为大多数操作(可能)只涉及几行.使用线程进行批量操作.
SQLiteDatabase
在线程之间为磁盘上的每个DB 共享一个实例,并实现计数系统以跟踪打开的连接.
这些方案是否有最佳实践?
在所有类之间共享静态字段.我过去常常为这个和其他需要共享的东西保留单身.计数方案(通常使用AtomicInteger)也应该用于确保您不要提前关闭数据库或保持打开状态.
我的解决方案
有关最新版本,请参阅https://github.com/JakarCo/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 Android SQLite Libray 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 ConcurrentHashMapdbMap = new ConcurrentHashMap (); private static final Object lockObject = new Object(); private DBSQLiteOpenHelper sqLiteOpenHelper; private SQLiteDatabase db; private Context context; /** Instantiate a new DB Helper. *
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 *
If the new counter is 0, then the database will be closed. *
This needs to be called before application exit. *
If the counter is 0, then the underlying SQLiteDatabase is null 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(); } } } }
数据库非常灵活,具有多线程功能.我的应用程序同时从许多不同的线程中击中了他们的数据库,它确实很好.在某些情况下,我有多个进程同时命中数据库,并且工作正常.
您的异步任务 - 尽可能使用相同的连接,但如果必须,可以从不同的任务访问数据库.
Dmytro的答案适用于我的案例.我认为将函数声明为synchronized是更好的.至少在我的情况下,它会调用空指针异常,例如,getWritableDatabase尚未在一个线程中返回,而openDatabse同时在另一个线程中调用.
public synchronized SQLiteDatabase openDatabase() { if(mOpenCounter.incrementAndGet() == 1) { // Opening new database mDatabase = mDatabaseHelper.getWritableDatabase(); } return mDatabase; }
经过几个小时的努力,我发现每个数据库执行只能使用一个数据库帮助对象。例如,
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,这是我可以通过助手类将字符串输入数据库的唯一方法。