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

Vid*_*nes 681 database sqlite android

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

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

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

这些方案是否有最佳实践?

Kev*_*gan 626

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

基本答案.

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

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

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

如果您尝试同时从实际的不同连接写入数据库,则会失败.它不会等到第一个完成然后写.它根本不会写你的改变.更糟的是,如果你不叫插入/更新的正确版本的SQLiteDatabase,你不会得到一个例外.您只需在LogCat中收到一条消息即可.

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

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

格雷和我实际上结束了ORM工具,根据脱下Ormlite,这与Android数据库实现原生支持,并遵循安全创建/调用结构我在博客中描述的.那应该很快就会出来.看一看.


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

还可以通过前面提到的锁定示例的2point0来检查fork :

  • Kāgii,谢谢您的详细解释.你能澄清一件事 - 我明白你应该有一个帮手,但你是否也只有一个连接(即一个SqliteDatabase对象)?换句话说,您应该多久调用一次getWritableDatabase?同样重要的是什么时候调用close()? (31认同)
  • 我更新了代码.当我切换博客主机时原来丢失了,但是我添加了一些精简的示例代码来证明这个问题.另外,您如何管理单一连接?我最初有一个更复杂的解决方案,但我已经修改过了.看看这里:http://www.touchlab.co/uncategorized/single-sqlite-connection/ (3认同)
  • 另外,Ormlite的android支持可以在http://ormlite.sourceforge.net/sqlite_java_android_orm.html找到.有示例项目,文档和jar. (2认同)

Dmy*_*lyk 187

并发数据库访问

我的博客上的文章相同(我更喜欢格式化)

我写了一篇小文章,描述了如何安全地访问你的android数据库线程.


假设您有自己的SQLiteOpenHelper.

public class DatabaseHelper extends SQLiteOpenHelper { ... }
Run Code Online (Sandbox Code Playgroud)

现在,您希望在单独的线程中将数据写入数据库.

 // 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();
Run Code Online (Sandbox Code Playgroud)

您将在logcat中收到以下消息,并且不会写入您的某个更改.

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
Run Code Online (Sandbox Code Playgroud)

这种情况正在发生,因为每次创建新的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();
    }

}
Run Code Online (Sandbox Code Playgroud)

在单独的线程中将数据写入数据库的更新代码将如下所示.

 // 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();
Run Code Online (Sandbox Code Playgroud)

这会给你带来另一次崩溃.

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
Run Code Online (Sandbox Code Playgroud)

由于我们只使用一个数据库连接,方法getDatabase()返回同一个实例SQLiteDatabase的对象线程1线程2.发生了什么,Thread1可能会关闭数据库,而Thread2仍在使用它.这就是为什么我们有IllegalStateException崩溃.

我们需要确保没有人使用数据库,然后关闭它.stackoveflow上的一些人建议永远不要关闭SQLiteDatabase.这将导致以下logcat消息.

Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
Run Code Online (Sandbox Code Playgroud)

工作样本

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

        }
    }

}
Run Code Online (Sandbox Code Playgroud)

使用方法如下.

SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way
Run Code Online (Sandbox Code Playgroud)

每次需要数据库时,都应该调用DatabaseManager类的openDatabase()方法.在这个方法中,我们有一个计数器,它指示数据库的打开次数.如果它等于1,则意味着我们需要创建新的数据库连接,否则,已经创建了数据库连接.

closeDatabase()方法中也是如此.每次调用此方法时,计数器都会减少,只要它变为零,我们就会关闭数据库连接.


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

  • 我是那些暗示你永远不会关闭数据库的人之一.如果打开数据库,则只会出现"泄漏发现"错误,请勿关闭它,然后再次尝试打开.如果您只使用一个打开的帮助程序,并且*从不*关闭数据库,则不会出现该错误.如果您发现其他情况,请告诉我(带代码).我在某个地方有一篇较长的帖子,但找不到它.这里的问题由commonsware回答,在SO点部门中我们两个都是如此:http://stackoverflow.com/questions/7211941/never-close-android-sqlite-connection (10认同)
  • getWritableDatabase,如果创建了新的db,则默认情况下引用为1.所以,你可能会让数据库永远不会关闭,因为无论这个数据库是缓存还是新创建,acquireReference都会加1.所以我怀疑这个答案. (4认同)
  • 更多的想法.#1,我会在你的经理里面创建帮手.问外面有问题.新开发人员可能会出于某种疯狂的原因直接调用帮助程序.此外,如果您将需要init方法,则在实例已存在时抛出异常.多数据库应用程序显然会失败.#2,为什么mDatabase字段?它可以从帮手那里获得.#3,作为迈向"永不关闭"的第一步,当你的应用程序崩溃并且没有"关闭"时,你的数据库会发生什么?提示,没什么.它很好,因为SQLite非常稳定.这是第1步,找出*为什么*你不需要关闭它. (4认同)
  • @DmytroDanylyk"DatabaseManager是线程安全的单例,所以它可以在任何地方使用",这对于回应virsir的问题是不正确的.对象不是共享的跨进程.您的DatabaseManager将在不同的进程中没有状态,如同步适配器(android:process =":sync") (2认同)

Ree*_*eed 17

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

这些方案是否有最佳实践?

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

我的解决方案

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

  1. 将代码复制/粘贴到名为的新文件中DatabaseManager.(或从github下载)
  2. 扩展DatabaseManager和实施onCreate,onUpgrade像往常一样.您可以创建一个DatabaseManager类的多个子类,以便在磁盘上具有不同的数据库.
  3. 实例化您的子类并调用getDb()以使用SQLiteDatabase该类.
  4. 调用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();
                }
        } 
    }
}
Run Code Online (Sandbox Code Playgroud)


Bra*_*ein 11

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

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

  • @Gray,只是想为可能正在使用此方法的人发布更新信息.文档说:*此方法现在什么都不做.不使用.* (3认同)
  • 我发现这种方法失败可怕,我们正在切换到ContentProvider来从多个应用程序访问.我们必须对我们的方法进行一些并发,但这应该解决所有同时访问数据的进程的任何问题. (3认同)
  • 我知道这是旧的,但这是不正确的.它可能**可以在不同的`AsyncTask /`Thread`s上从不同的`SQLiteDatabase`对象访问DB,但有时会导致错误,这就是为什么[SQLiteDatabase](http:// grepcode. com/file/repository.grepcode.com/java/ext/com.google.android/android/1.5_r4/android/database/sqlite/SQLiteDatabase.java#SQLiteDatabase.unlock%28%29)(第1297行)使用`Lock单曲 (2认同)

gon*_*ong 7

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

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


Swa*_*oop 5

我对 SQLiteDatabase API 的理解是,如果您有一个多线程应用程序,您无法承受超过 1 个指向单个数据库的 SQLiteDatabase 对象。

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

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

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

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

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


del*_*116 5

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

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();
}
Run Code Online (Sandbox Code Playgroud)

适用于:

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();
Run Code Online (Sandbox Code Playgroud)

每次循环迭代时都要创建一个新的DBAdapter,这是我可以通过助手类将字符串输入数据库的唯一方法。


Zim*_*ger 5

您可以尝试应用在 Google I/O 2017 上宣布的新架构方法。

它还包括名为Room 的新 ORM 库

它包含三个主要组件:@Entity、@Dao 和@Database

用户.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.
}
Run Code Online (Sandbox Code Playgroud)

用户道

@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);
}
Run Code Online (Sandbox Code Playgroud)

应用数据库.java

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
  public abstract UserDao userDao();
}
Run Code Online (Sandbox Code Playgroud)