当前位置:Gxlcms > 数据库问题 > 小白到大神,你需要了解的 sqlite 最佳实践

小白到大神,你需要了解的 sqlite 最佳实践

时间:2021-07-01 10:21:17 帮助过:29人阅读

本文微信公众号「AndroidTraveler」首发。

背景

本文是对一篇英文文档的翻译,原文请见文末链接。


并发数据库访问

假设你实现了自己的 SQLiteOpenHelper。

  1. <code>public class DatabaseHelper extends SQLiteOpenHelper { ... }</code>

现在你想要在多个线程中对数据库写入数据。

  1. <code> // Thread 1
  2. Context context = getApplicationContext();
  3. DatabaseHelper helper = new DatabaseHelper(context);
  4. SQLiteDatabase database = helper.getWritableDatabase();
  5. database.insert(…);
  6. database.close();
  7. // Thread 2
  8. Context context = getApplicationContext();
  9. DatabaseHelper helper = new DatabaseHelper(context);
  10. SQLiteDatabase database = helper.getWritableDatabase();
  11. database.insert(…);
  12. database.close();</code>

你将会在你的 logcat 中发现下面信息,并且你的其中一个改变不会写入数据库:

  1. <code>android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)</code>

产生这个错误的原因是因为,每次你创建新的 SQLiteOpenHelper 对象,实际上你创建了新的数据库连接。如果你尝试从不同的连接同时对数据库写入数据,其中一个会失败。

为了在多线程使用数据库,我们要确保只使用一个数据库连接。

让我们构造单例类 DatabaseManager,它会持有并返回单个 SQLiteOpenHelper 对象。

  1. <code>public class DatabaseManager {
  2. private static DatabaseManager instance;
  3. private static SQLiteOpenHelper mDatabaseHelper;
  4. public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
  5. if (instance == null) {
  6. instance = new DatabaseManager();
  7. mDatabaseHelper = helper;
  8. }
  9. }
  10. public static synchronized DatabaseManager getInstance() {
  11. if (instance == null) {
  12. throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
  13. " is not initialized, call initialize(..) method first.");
  14. }
  15. return instance;
  16. }
  17. public synchronized SQLiteDatabase getDatabase() {
  18. return mDatabaseHelper.getWritableDatabase();
  19. }
  20. }</code>

在多个线程中对数据库写入数据,修改后的代码如下所示。

  1. <code>// In your application class
  2. DatabaseManager.initializeInstance(new DatabaseHelper());
  3. // Thread 1
  4. DatabaseManager manager = DatabaseManager.getInstance();
  5. SQLiteDatabase database = manager.getDatabase()
  6. database.insert(…);
  7. database.close();
  8. // Thread 2
  9. DatabaseManager manager = DatabaseManager.getInstance();
  10. SQLiteDatabase database = manager.getDatabase()
  11. database.insert(…);
  12. database.close();</code>

这会带来另一个奔溃。

  1. <code>java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase</code>

由于我们只使用了一个数据库连接,Thread1Thread2getDatabase() 方法都会返回同一个 SQLiteDatabase 对象实例。可能发生的场景是 Thread1 关闭了数据库,然而 Thread2 还在使用它。这也就是为什么我们会有 IllegalStateException 的奔溃的原因。

我们需要确保没有人正在使用数据库,这个时候我们才可以关闭它。stackoveflow 上有人推荐永远不要关闭你的 SQLiteDatabase。这会让你看到下面的 logcat 信息。所以我一点也不认为这是一个好的想法。

  1. <code>Leak found
  2. Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed</code>

实战例子

一种可能的解决方案是使用计数器跟踪打开/关闭的数据库连接。

  1. <code>public class DatabaseManager {
  2. private AtomicInteger mOpenCounter = new AtomicInteger();
  3. private static DatabaseManager instance;
  4. private static SQLiteOpenHelper mDatabaseHelper;
  5. private SQLiteDatabase mDatabase;
  6. public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
  7. if (instance == null) {
  8. instance = new DatabaseManager();
  9. mDatabaseHelper = helper;
  10. }
  11. }
  12. public static synchronized DatabaseManager getInstance() {
  13. if (instance == null) {
  14. throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
  15. " is not initialized, call initializeInstance(..) method first.");
  16. }
  17. return instance;
  18. }
  19. public synchronized SQLiteDatabase openDatabase() {
  20. if(mOpenCounter.incrementAndGet() == 1) {
  21. // Opening new database
  22. mDatabase = mDatabaseHelper.getWritableDatabase();
  23. }
  24. return mDatabase;
  25. }
  26. public synchronized void closeDatabase() {
  27. if(mOpenCounter.decrementAndGet() == 0) {
  28. // Closing database
  29. mDatabase.close();
  30. }
  31. }
  32. }</code>

然后如下所示来使用。

  1. <code>SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
  2. database.insert(...);
  3. // database.close(); Don't close it directly!
  4. DatabaseManager.getInstance().closeDatabase(); // correct way</code>

每当你需要使用数据库的时候你应该调用 DatabaseManager 类的 openDatabase() 方法。在这个方法里面,我们有一个计数器,用来表明数据库打开的次数。如果计数为 1,意味着我们需要创建新的数据库连接,否则,数据库连接已经建立。

对于 closeDatabase() 方法来说也是一样的。每次我们调用这个方法的时候,计数器在减少,当减为 0 的时候,我们关闭数据库连接。

现在你能够使用你的数据库并且确保是线程安全的。


原文:Concurrent database access

由于本人翻译水平有限,如果你有更好的翻译文案,欢迎在 GitHub 提 PR。
这边建了一个仓库,欢迎提 PR 投稿一些好的文章翻译。
并发数据库访问

技术图片

小白到大神,你需要了解的 sqlite 最佳实践

标签:场景   应该   ESS   zed   最佳实践   correct   leak   sql   否则   

人气教程排行