Android上SQLite的最佳做法是什么?

时间:2010-03-22 15:13:33

标签: android database sqlite

在Android应用程序中对SQLite数据库执行查询时,最佳做法是什么?

从AsyncTask的doInBackground运行插入,删除和选择查询是否安全?或者我应该使用UI线程?我想数据库查询可能“很重”,不应该使用UI线程,因为它可以锁定应用程序 - 导致Application Not Responding(ANR)。

如果我有多个AsyncTasks,他们应该共享连接还是应该分别打开连接?

这些方案是否有最佳做法?

10 个答案:

答案 0 :(得分:617)

多个线程的插入,更新,删除和读取通常都可以,但Brad的answer不正确。您必须小心如何创建连接并使用它们。在某些情况下,即使您的数据库没有损坏,您的更新调用也会失败。

基本答案。

SqliteOpenHelper对象保留一个数据库连接。它似乎为您提供读写连接,但它确实没有。调用只读,无论如何都将获得写数据库连接。

所以,一个帮助器实例,一个数据库连接。即使您从多个线程使用它,一次一个连接。 SqliteDatabase对象使用java锁来保持序列化访问。因此,如果100个线程有一个数据库实例,则对实际磁盘数据库的调用将被序列化。

所以,一个帮助器,一个db连接,在java代码中序列化。一个线程,1000个线程,如果您使用它们之间共享的一个帮助程序实例,则所有数据库访问代码都是串行的。生活是美好的(ish)。

如果您尝试同时从实际的不同连接写入数据库,则会失败。它不会等到第一个完成然后写。它根本不会写你的改变。更糟糕的是,如果您没有在SQLiteDatabase上调用正确版本的插入/更新,则不会出现异常。您只需在LogCat中收到一条消息即可。

那么,多线程?使用一个帮手。期。如果你知道只有一个线程会写,你可以使用多个连接,你的读取会更快,但买家要小心。我没有测试那么多。

这是一篇博客文章,其中包含更多详细信息和示例应用程序。

格雷和我实际上正在整理一个基于他的Ormlite的ORM工具,该工具本身适用于Android数据库实现,并遵循我在博客文章中描述的安全创建/调用结构。那应该很快就会出来。看一看。


与此同时,还有一篇跟进博客文章:

同样通过前面提到的锁定示例的 2point0 检查分叉:

答案 1 :(得分:184)

并发数据库访问

Same article on my blog(I like formatting more)

我写了一篇小文章,描述了如何安全地访问你的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() Thread1 Thread2返回 SQLiteDatabase 对象的相同实例。发生了什么, 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()方法也是如此。每次调用此方法时,计数器都会减少,只要它变为零,我们就会关闭数据库连接。


现在您应该能够使用您的数据库,并确保它是线程安全的。

答案 2 :(得分:17)

  • 使用ThreadAsyncTask进行长时间运行(50ms +)。测试您的应用以查看它的位置。大多数操作(可能)不需要线程,因为大多数操作(可能)只涉及几行。使用线程进行批量操作。
  • 在线程之间为磁盘上的每个数据库共享一个SQLiteDatabase实例,并实现计数系统以跟踪打开的连接。
  

这些方案是否有最佳做法?

在所有课程之间共享静态字段。我过去常常为这个和其他需要共享的东西保留单身。还应使用计数方案(通常使用AtomicInteger)来确保您不要提前关闭数据库或将其保持打开状态。

  

我的解决方案:

对于最新版本,请参阅https://github.com/JakarCo/databasemanager,但我也会尝试在此处更新代码。如果您想了解我的解决方案,请查看代码并阅读我的笔记。我的笔记通常很有帮助。

  1. 将代码复制/粘贴到名为DatabaseManager的新文件中。 (或从github下载)
  2. 扩展DatabaseManager并像往常一样实施onCreateonUpgrade。您可以创建一个DatabaseManager类的多个子类,以便在磁盘上拥有不同的数据库。
  3. 实例化您的子类并致电getDb()以使用SQLiteDatabase类。
  4. 为您实例化的每个子类调用close()
  5. 复制/粘贴的代码:

    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();
                    }
            } 
        }
    }
    

答案 3 :(得分:11)

数据库非常灵活,具有多线程功能。我的应用程序同时从许多不同的线程中击中了他们的数据库,它确实很好。在某些情况下,我有多个进程同时命中数据库,并且工作正常。

您的异步任务 - 尽可能使用相同的连接,但如果必须,可以从不同的任务访问数据库。

答案 4 :(得分:7)

Dmytro的回答对我的案子很好。 我认为将函数声明为synchronized是更好的。至少在我的情况下,它会调用空指针异常,例如, getWritableDatabase尚未在一个线程中返回,而openDatabse在另一个线程中同时调用。

public synchronized SQLiteDatabase openDatabase() {
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }

答案 5 :(得分:5)

经过几个小时的努力,我发现每个数据库执行只能使用一个db帮助对象。例如,

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是我通过助手类将字符串放入数据库的唯一方法。

答案 6 :(得分:4)

我对SQLiteDatabase API的理解是,如果你有一个多线程应用程序,你就不能拥有多于一个指向单个数据库的SQLiteDatabase对象。

绝对可以创建对象,但如果不同的线程/进程(也)开始使用不同的SQLiteDatabase对象(比如我们在JDBC Connection中的使用方式),则插入/更新会失败。

这里唯一的解决方案是坚持使用1个SQLiteDatabase对象,并且每当在多个线程中使用startTransaction()时,Android就会跨不同的线程管理锁定,并且一次只允许1个线程拥有独占的更新访问权限。

你也可以从数据库中“读取”并在不同的线程中使用相同的SQLiteDatabase对象(而另一个线程写入)并且永远不会有数据库损坏,即“读取线程”不会从数据库中读取数据直到“写入线程”提交数据,尽管两者都使用相同的SQLiteDatabase对象。

这与JDBC中的连接对象的不同之处在于,如果您传递(使用相同的)读写线程之间的连接对象,那么我们可能也会打印未提交的数据。

在我的企业应用程序中,我尝试使用条件检查,以便UI线程永远不必等待,而BG线程保持SQLiteDatabase对象(仅限)。我尝试预测UI动作并推迟BG线程运行'x'秒。还可以维护PriorityQueue来管理分发SQLiteDatabase Connection对象,以便UI Thread首先获取它。

答案 7 :(得分:4)

您可以尝试在Google I / O 2017中应用新的架构方法anounced

它还包括名为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();
}

答案 8 :(得分:3)

遇到一些问题,我想我已经理解了为什么我出错了。

我编写了一个数据库包装类,其中包含一个close(),它将helper关闭为open()的镜像,调用getWriteableDatabase,然后迁移到ContentProviderContentProvider的模型不使用SQLiteDatabase.close(),我认为这是一个很大的线索,因为代码确实使用getWriteableDatabase在某些情况下,我仍然在进行直接访问(主要的屏幕验证查询)我迁移到getWriteableDatabase / rawQuery模型。

我使用单身人士,关闭文档

中有一些稍微不祥的评论
  

关闭任何打开的数据库对象

(我的粗体)。

所以我遇到间歇性崩溃,我使用后台线程来访问数据库,它们与前台同时运行。

所以我认为close()强制数据库关闭而不管其他任何线程是否存在引用 - 所以close()本身并不是简单地撤消匹配的getWriteableDatabase而是强制关闭 任何 打开请求。大多数情况下,这不是问题,因为代码是单线程,但在多线程情况下,总是有可能打开和关闭不同步。

在其他地方阅读了解释SqLiteDatabaseHelper代码实例的计数,那么您希望关闭的唯一时间就是您想要进行备份副本的情况,并且您希望强制关闭所有连接并且强制SqLite写出任何可能正在游荡的缓存内容 - 换句话说,停止所有应用程序数据库活动,关闭以防Helper丢失跟踪,执行任何文件级别活动(备份/恢复)然后重新开始。

虽然尝试以受控方式关闭听起来是个好主意,但事实是Android保留了删除VM的权利,因此任何关闭都可以降低缓存更新未被写入的风险,但无法保证如果设备受到压力,并且您已正确释放了游标和对数据库的引用(不应该是静态成员),那么帮助程序无论如何都将关闭数据库。

所以我认为方法是:

使用getWriteableDatabase从单件包装器打开。 (我使用派生的应用程序类从静态提供应用程序上下文来解决对上下文的需求)。

永远不要直接致电。

永远不要将结果数据库存储在任何没有明显范围的对象中,并依赖引用计数来触发隐式close()。

如果进行文件级别处理,将所有数据库活动停止,然后调用close,以防万一有一个失控的线程假设您编写了正确的事务,因此失控线程将失败并且关闭的数据库将至少具有正确的交易,而不是潜在的部分交易的文件级副本。

答案 9 :(得分:0)

我知道响应迟了,但在android中执行sqlite查询的最佳方法是通过自定义内容提供程序。通过这种方式,UI与数据库类(扩展SQLiteOpenHelper类的类)分离。查询也在后台线程(Cursor Loader)中执行。