What are the best practices for SQLite on Android?
在Android应用程序中对SQLite数据库执行查询时,什么是最佳实践?
从异步任务的doinbackground中运行插入、删除和选择查询是否安全?还是应该使用UI线程?我认为数据库查询可能是"重的",不应该使用UI线程,因为它可以锁定应用程序,从而导致应用程序没有响应(ANR)。
如果我有几个异步任务,它们应该共享一个连接还是应该分别打开一个连接?
对于这些场景,是否有任何最佳实践?
插入、更新、删除和读取多个线程通常都是正常的,但是brad的回答不正确。您必须小心如何创建连接并使用它们。在某些情况下,即使数据库没有损坏,更新调用也会失败。
基本答案。
sqliteOpenHelper对象保留一个数据库连接。它似乎为您提供了一个读写连接,但实际上没有。调用只读,您将获得写数据库连接,而不管如何。
所以,一个助手实例,一个数据库连接。即使您从多个线程使用它,一次一个连接。SQLeDATABAASE对象使用Java锁来保持访问序列化。因此,如果100个线程有一个DB实例,那么对实际磁盘数据库的调用将被序列化。
因此,一个助手,一个DB连接,在Java代码中被序列化。一个线程,1000个线程,如果使用它们之间共享的一个助手实例,那么所有的DB访问代码都是串行的。生活是美好的。
如果您试图同时从实际的不同连接写入数据库,其中一个将失败。它不会等到第一次写完再写。它不会写你的零钱。更糟的是,如果不在sqliteDatabase上调用正确版本的insert/update,则不会得到异常。你只会在日志里收到一条信息,就是这样。
那么,多线程?使用一个助手。时期。如果你知道只有一个线程在写,你可以使用多个连接,你的读取速度会更快,但买家要小心。我没有做过那么多的测试。
这里有一篇更详细的博客文章和一个示例应用程序。
- android sqlite锁定(更新链接6/18/2012)
- GitHub上TouchLab的Android数据库锁定冲突示例
我和格雷实际上正在包装一个基于他Ormlite的ORM工具,它与Android数据库实现本地工作,并遵循我在博客文章中描述的安全创建/调用结构。那应该很快就出来了。看一看。
同时,还有一篇后续博客文章:
- 单sqlite连接
另外,按前面提到的锁定示例的2点0检查拨叉:
- GitHub上2点0的Android数据库锁定冲突示例
并发数据库访问
我博客上的同一篇文章(我更喜欢格式化)
我写了一篇小文章,描述了如何保证Android数据库线程的安全。
假设您有自己的sqliteOpenHelper。
1 | public class DatabaseHelper extends SQLiteOpenHelper { ... } |
现在,您希望在单独的线程中将数据写入数据库。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 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(); |
您将在日志cat中收到以下消息,其中一个更改将不会被写入。
1 | android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5) |
这是因为每次创建新的sqliteOpenHelper对象时,实际上都在创建新的数据库连接。如果您试图同时从实际的不同连接写入数据库,其中一个将失败。(来自上述答案)
要将数据库与多个线程一起使用,我们需要确保使用一个数据库连接。
让我们做一个singleton类数据库管理器,它将保存并返回单个sqliteOpenHelper对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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(); } } |
在不同线程中将数据写入数据库的更新代码将如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 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(); |
这会给你带来另一场车祸。
1 | java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase |
因为我们只使用一个数据库连接,所以getdatabase()方法为thread1和thread2返回相同的sqliteDatabase对象实例。发生了什么,thread1可能会关闭数据库,而thread2仍在使用它。这就是为什么我们有非法状态例外崩溃。
我们需要确保没有人使用数据库,然后关闭它。StackoveFlow上的一些人建议永远不要关闭您的sqliteDatabase。这将导致以下logcat消息。
1 2 | Leak found Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed |
工作样本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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(); } } } |
使用方法如下。
1 2 3 4 | SQLiteDatabase database = DatabaseManager.getInstance().openDatabase(); database.insert(...); // database.close(); Don't close it directly! DatabaseManager.getInstance().closeDatabase(); // correct way |
每次需要数据库时,都应该调用databaseManager类的opendatabase()方法。在这个方法中,我们有一个计数器,它指示打开数据库的次数。如果它等于1,则意味着我们需要创建新的数据库连接,否则,数据库连接已经创建。
closedatabase()方法中也会发生同样的情况。每次调用此方法时,计数器都会减少,每当计数器变为零时,我们将关闭数据库连接。
现在您应该能够使用您的数据库并确保它是线程安全的。
- 对于长时间运行的操作(50ms以上),使用
Thread 或AsyncTask 。测试你的应用程序,看看它在哪里。大多数操作(可能)不需要线程,因为大多数操作(可能)只涉及几行。使用线程进行批量操作。 - 在线程之间为磁盘上的每个数据库共享一个
SQLiteDatabase 实例,并实现计数系统以跟踪打开的连接。
Are there any best practices for these scenarios?
在所有类之间共享一个静态字段。我过去常常为这个和其他需要分享的事情留着一个单人间。计数方案(通常使用atomicinteger)也应该用于确保永远不要过早关闭数据库或将其保持打开状态。
My solution:
有关最新版本的信息,请参阅https://github.com/jakarco/databasemanager,但我也会尽量使代码保持最新。如果您想理解我的解决方案,请看代码并阅读我的注释。我的笔记通常很有用。
要复制/粘贴的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | 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 [email protected] (or [email protected] ) * * 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 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. * 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 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(); } } } } |
数据库具有非常灵活的多线程功能。我的应用程序同时从多个不同的线程命中了它们的DBS,而且效果很好。在某些情况下,我有多个进程同时命中数据库,这也很好。
您的异步任务-尽可能使用相同的连接,但如果必须,可以从不同的任务访问DB。
德米特洛的回答对我的案件很有效。我认为最好将函数声明为synchronized。至少在我的情况下,它会调用空指针异常,否则,例如getwritabledatabase尚未在一个线程中返回,而opendatabse同时在另一个线程中调用。
1 2 3 4 5 6 7 | public synchronized SQLiteDatabase openDatabase() { if(mOpenCounter.incrementAndGet() == 1) { // Opening new database mDatabase = mDatabaseHelper.getWritableDatabase(); } return mDatabase; } |
您可以尝试在2017年的谷歌I/O上应用新的架构方法。
它还包括新的ORM库,称为Room。
它包含三个主要组件:@entity、@dao和@database
Java语言
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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. } |
UsdoDo.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @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
1 2 3 4 | @Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } |
在为此挣扎了几个小时之后,我发现每个数据库执行只能使用一个db helper对象。例如,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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(); } |
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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的理解是,如果您有一个多线程的应用程序,那么您就不能拥有多个指向单个数据库的sqliteDatabase对象。
当然可以创建该对象,但是如果不同的线程/进程也开始使用不同的sqliteDatabase对象(比如我们在JDBC连接中的使用方式),则插入/更新将失败。
这里唯一的解决方案是坚持使用1个sqliteDatabase对象,并且每当在多个线程中使用startTransaction()时,Android会管理不同线程之间的锁定,并且一次只允许1个线程具有独占的更新访问权。
另外,您可以从数据库中执行"读取",并在不同的线程中使用相同的sqliteDatabase对象(而另一个线程写入),并且不会出现数据库损坏,即"读取线程"在"写入线程"提交数据之前不会从数据库中读取数据,尽管两者都使用相同的sqliteDatabase对象。
这与JDBC中的连接对象不同,如果您在读写线程之间传递(使用相同的)连接对象,那么我们也可能打印未提交的数据。
在我的企业应用程序中,我尝试使用条件检查,以便UI线程不必等待,而bg线程持有sqliteDatabase对象(独占)。我尝试预测用户界面操作,并将bg线程的运行延迟"x"秒。此外,还可以维护PriorityQueue来管理分发sqliteDatabase连接对象,以便UI线程首先获取它。
有过一些问题,我想我已经明白为什么我会出错。
我编写了一个数据库包装类,其中包括一个
我用的是单件的,在近距离的文档中有一些不祥的评论。
Close any open database object
(我大胆)。
所以我有间歇性崩溃,我使用后台线程访问数据库,它们与前台同时运行。
因此,我认为
在阅读了其他解释sqliteDatabaseHelper代码实例计数的注释之后,您唯一想要关闭的时间就是您想要执行备份副本的情况,并且您想要强制关闭所有连接,并强制sqlite写下任何可能在徘徊的缓存内容-换句话说,st操作所有应用程序数据库活动,关闭以防帮助程序丢失跟踪,执行任何文件级活动(备份/还原),然后重新启动。
虽然尝试以受控方式关闭听起来是一个好主意,但现实情况是,Android保留对虚拟机进行垃圾处理的权利,因此任何关闭都会降低未写入缓存更新的风险,但如果设备受到压力,并且您已正确释放了光标和对数据库的引用(这是不应是静态成员),则帮助程序仍将关闭数据库。
所以我的看法是:
使用GetWriteableDatabase从单个包装打开。(我使用派生的应用程序类从静态提供应用程序上下文以解决对上下文的需求)。
不要直接呼叫关闭。
不要将结果数据库存储在任何没有明显作用域的对象中,并依赖引用计数来触发隐式close()。
如果执行文件级处理,请停止所有数据库活动,然后调用close,以防出现失控线程,前提是您编写了正确的事务,这样失控线程将失败,并且关闭的数据库将至少具有正确的事务,而不是可能具有部分事务的文件级副本。
我知道响应很晚,但是在Android中执行sqlite查询的最佳方法是通过自定义内容提供程序。这样,用户界面就与数据库类(扩展sqliteOpenHelper类的类)分离了。查询也在后台线程(光标加载程序)中执行。