Android上的SQLite的最佳做法是什么?
当在Android应用程序中执行对SQLite数据库的查询时,什么会被认为是最佳实践?
从AsyncTask的doInBackground运行插入,删除和select查询是否安全? 或者我应该使用UI线程? 我想数据库查询可以是“沉重的”,不应该使用UI线程,因为它可以locking应用程序 – 导致应用程序不响应 (ANR)。
如果我有几个AsyncTasks,他们应该共享一个连接还是应该打开一个连接?
有没有这些情况下的最佳做法?
插入,更新,删除和读取通常可以从多个线程,但布拉德的答案是不正确的。 您必须小心如何创build连接并使用它们。 有些情况下,即使数据库没有损坏,更新调用也会失败。
基本的答案。
SqliteOpenHelper对象拥有一个数据库连接。 它似乎为您提供了一个读写连接,但它确实没有。 调用只读,你会得到写数据库连接不pipe。
所以,一个辅助实例,一个数据库连接。 即使你从多个线程使用它,一次一个连接。 SqliteDatabase对象使用java锁来保持访问序列化。 因此,如果100个线程有一个数据库实例,则对实际的磁盘数据库的调用将被序列化。
所以,一个助手,一个数据库连接,这是在java代码序列化。 一个线程,1000个线程,如果你使用它们之间共享的一个助手实例,你所有的数据库访问代码是串行的。 生命是好的(ish)。
如果您尝试从实际不同的连接同时写入数据库,则会失败。 它不会等到第一个完成,然后写。 它不会写你的改变。 更糟的是,如果你没有在SQLiteDatabase上调用正确版本的insert / update,你将不会得到exception。 你会在你的LogCat中得到一个消息,就是这样。
那么,multithreading? 使用一个帮手。 期。 如果你知道只有一个线程将被写入,你可能会使用多个连接,你的读取会更快,但买方要小心。 我没有testing那么多。
这里有一个更详细的博客文章和一个示例应用程序。
- Android Sqlitelocking (更新链接6/18/2012)
- Android数据库locking – 碰撞 -在GitHub上由touchlab实例
Gray和我实际上正在包装一个基于Ormlite的ORM工具,该工具本身与Android数据库实现一起工作,并遵循我在博客文章中描述的安全创build/调用结构。 这应该很快出来。 看一看。
同时,还有一个后续的博客文章:
- 单个SQLite连接
还要检查前面提到的locking示例的2点0的叉:
- 在GitHub上, Android-Database-Locking-Collisions-以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)
发生这种情况是因为每次创build新的SQLiteOpenHelper对象时,实际上都在build立新的数据库连接。 如果您尝试从实际不同的连接同时写入数据库,则会失败。 (从上面的答案)
要在多个线程中使用数据库,我们需要确保我们正在使用一个数据库连接。
让我们做单身类数据库pipe理器 ,它将保存并返回单个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可能会closures数据库,而Thread2仍在使用它。 这就是为什么我们有IllegalStateException崩溃。
我们需要确保没有人使用数据库,只有closures它。 build议stackoverflow的一些人不要closures你的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()方法。 在这个方法里面,我们有一个计数器,表示数据库打开了多less次。 如果它等于1,则表示我们需要创build新的数据库连接,如果不是,则数据库连接已经创build。
closeDatabase()方法也一样。 每次我们调用这个方法,计数器都会减less,每当它到零,我们就closures数据库连接。
现在你应该能够使用你的数据库,并确保它是线程安全的。
- 使用
Thread
或AsyncTask
进行长时间运行(50ms +)。 testing你的应用程序,看看它在哪里。 大多数操作(可能)不需要线程,因为大多数操作(可能)只涉及几行。 使用线程批量操作。 - 在线程之间为磁盘上的每个DB共享一个
SQLiteDatabase
实例,并实现一个计数系统来跟踪打开的连接。
有没有这些情况下的最佳做法?
在所有类之间共享一个静态字段。 我曾经为这个和其他需要共享的东西保持单身。 一个计数scheme(通常使用AtomicInteger)也应该用来确保你永远不要提前closures数据库或者把它打开。
我的解决scheme
对于最新的版本,请参阅https://github.com/JakarCo/databasemanager,但我会尽力保持代码在这里。; 如果您想了解我的解决scheme,请查看代码并阅读我的笔记。 我的笔记通常很有帮助。
- 将代码复制/粘贴到名为
DatabaseManager
的新文件中。 (或从github下载) - 扩展
DatabaseManager
并像通常那样实现onCreate
和onUpgrade
。 您可以创build一个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(); } } } }
数据库使用multithreading非常灵活。 我的应用程序从许多不同的线程同时击中他们的数据库,它没问题。 在某些情况下,我有多个进程同时击中数据库,这也很好。
您的asynchronous任务 – 尽可能使用相同的连接,但是如果必须,则可以从不同的任务访问数据库。
Dmytro的答案适用于我的情况。 我认为最好把函数声明为synchronized。 至less在我的情况下,它会调用空指针exception,否则,例如,getWritableDatabase尚未在一个线程中返回,在另一个线程中调用openDatabse。
public synchronized SQLiteDatabase openDatabase() { if(mOpenCounter.incrementAndGet() == 1) { // Opening new database mDatabase = mDatabaseHelper.getWritableDatabase(); } return mDatabase; }
我对SQLiteDatabase API的理解是,如果你有一个multithreading的应用程序,你不能有超过一个SQLiteDatabase对象指向一个单一的数据库。
该对象肯定可以创build,但如果不同的线程/进程(太)开始使用不同的SQLiteDatabase对象(比如我们在JDBC连接中使用),插入/更新失败。
这里唯一的解决scheme是坚持使用1个SQLiteDatabase对象,并且每当在一个以上的线程中使用startTransaction()时,Androidpipe理跨不同线程的locking,并且一次只允许一个线程拥有独占更新访问权限。
你也可以从数据库中“读取”,并在另一个线程中使用相同的SQLiteDatabase对象(而另一个线程写入),并且永远不会有数据库损坏,即“读取线程”不会从数据库读取数据,直到“写入线程“提交数据虽然都使用相同的SQLiteDatabase对象。
这与连接对象在JDBC中的连接对象是不同的,如果你在读写线程之间传递(使用相同的)连接对象,那么我们可能会打印未提交的数据。
在我的企业应用程序中,我尝试使用条件检查,以便UI线程永不需要等待,而BG线程保存SQLiteDatabase对象(专用)。 我尝试预测UI操作,并推迟BG线程运行“x”秒。 也可以维护PriorityQueue来pipe理SQLiteDatabase连接对象的处理,以便UI线程首先获取它。
经过几个小时的努力,我发现你只能使用一个数据库帮助对象每分贝执行。 例如,
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();
每次循环迭代时创build一个新的DBAdapter是唯一可以通过我的helper类将我的string存入数据库的方法。
遇到了一些问题,我想我已经明白了为什么我会出错。
我写了一个数据库包装类,它包含一个close()
,它叫做helper close作为open()
一个镜像,它叫做getWriteableDatabase,然后迁移到ContentProvider
。 ContentProvider
的模型不使用SQLiteDatabase.close()
,我认为这是一个很大的线索,因为代码使用getWriteableDatabase
在一些情况下,我仍然在做直接访问(屏幕validation查询主,所以我迁移到一个getWriteableDatabase / rawQuery模型。
我使用一个单身人士,并在closures文件中有一些稍微不祥的评论
closures任何打开的数据库对象
(我的大胆)。
所以我有间歇性的崩溃,我使用后台线程来访问数据库,他们在前台同时运行。
所以我认为close()
强制closures数据库,而不pipe其他任何线程持有引用 – 所以close()
本身不是简单地撤销匹配的getWriteableDatabase
而是强制closures任何打开的请求。 大多数情况下这不是问题,因为代码是单线程的,但是在multithreading的情况下总会有打开和closures不同步的机会。
阅读其他地方的注释,说明SqLiteDatabaseHelper代码实例是计数的,那么只有当你想要closures的时候,你想要做的情况是你想做一个备份副本,你想要强制closures所有的连接,并强制SqLite写下任何可能caching的caching内容 – 换句话说,停止所有的应用程序数据库活动,closures以防助手失去踪迹,执行任何文件级活动(备份/恢复),然后重新开始。
虽然这听起来像是一个好主意,试图以受控的方式进行closures,但事实是,Android保留垃圾的权利,因此任何closures都会降低caching更新未被写入的风险,但是如果设备是强调,如果你已经正确地释放了你的游标和对数据库的引用(不应该是静态成员),那么这个helper将会closures数据库。
所以我认为这个方法是:
使用getWriteableDatabase从单例包装器打开。 (我使用派生的应用程序类来提供静态的应用程序上下文以解决对上下文的需求)。
切勿直接致电closures。
切勿将结果数据库存储在任何不具有明显范围的对象中,并依靠引用计数触发隐式close()。
如果要进行文件级别的处理,就把所有的数据库活动暂停一下,然后调用close,以防假设你编写了正确的事务,以至于有一个失控的线程,所以失控的线程将会失败,而且封闭的数据库至less会有正确的事务比可能是部分事务的文件级拷贝。
您可以尝试应用2017年Google I / O上发布的新架构方法。
它还包括称为Room的新ORM库
它包含三个主要组件:@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(); }