Каковы наилучшие рекомендации для SQLite на Android?

Что будет считаться лучшей практикой при выполнении запросов к базе данных SQLite в приложении Android?

безопасно ли запускать вставки, удаления и выбирать запросы из doInBackground AsyncTask? Или я должен использовать поток UI? Я полагаю, что запросы базы данных могут быть "тяжелыми" и не должны использовать поток пользовательского интерфейса, поскольку он может заблокировать приложение - в результате Приложение Не Отвечает (ANR).

Если у меня есть несколько AsyncTasks, должны ли они поделитесь соединением или они должны открыть соединение каждый?

существуют ли какие-либо рекомендации для этих сценариев?

10 ответов


вставки, обновления, удаления и чтения, как правило, в порядке из нескольких потоков, но Брэд ответ неверно. Вы должны быть осторожны с тем, как вы создаете свои связи и использовать их. Бывают ситуации, когда ваши вызовы обновлений завершатся неудачей, даже если ваша база данных не будет повреждена.

основной ответ.

объект SqliteOpenHelper удерживает одно соединение с базой данных. Он, кажется, предлагает вам читать и писать связь, но на самом деле нет. Вызовите только для чтения, и вы получите соединение с базой данных записи независимо.

Итак, один вспомогательный экземпляр, одно соединение с БД. Даже если вы используете его из нескольких потоков, одно соединение одновременно. Объект SqliteDatabase использует блокировки java для сохранения сериализации доступа. Таким образом, если 100 потоков имеют один экземпляр БД, вызовы фактической базы данных на диске сериализуются.

Итак, один помощник, одно соединение db, которое сериализуется в коде java. Один поток, 1000 потоков, если вы используете один вспомогательный экземпляр, разделяемый между ними, весь код доступа к БД является последовательным. И жизнь хороша (иш).

Если вы попытаетесь записать в базу данных из фактических различных соединений в то же время, один не удастся. Это не будет ждать, пока первый будет сделан, а затем написать. Он просто не напишет ваше изменение. Хуже того, если вы не вызываете правильную версию insert/update на SQLiteDatabase, вы не получите исключения. Вы просто получите сообщение в своем LogCat, и это будет все.

Итак, несколько потоков? Используйте одного помощника. Период. Если вы знаете, что только один поток будет писать, вы можете использовать несколько соединений, и ваши чтения будут быстрее, но покупатель будьте осторожны. Я еще не проверял.

вот сообщение в блоге с гораздо более подробной информацией и примером приложения.

Грей и я фактически оборачиваем инструмент ORM, основанный на его Ormlite, который изначально работает с реализациями баз данных Android и следует безопасной структуре создания/вызова, которую я описываю в блоге. Это должно скоро выйти. Взглянуть.


тем временем, есть последующий блог сообщение:

также проверьте вилку по 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)

это происходит потому, что каждый раз, когда вы создаете новый SQLiteOpenHelper объект вы фактически создаете новое соединение с базой данных. Если вы попытаетесь записать в базу данных из реальных различных соединений одновременно,это не удастся. (из ответа выше)

чтобы использовать базу данных с несколькими потоками, нам нужно убедиться, что мы используем одно соединение с базой данных.

давайте сделаем синглтон класс Менеджер Баз Данных, который будет держать и вернуть один SQLiteOpenHelper


  • использовать Thread или AsyncTask для длительных операций (50 мс+). Проверьте приложение, чтобы увидеть, где это. Большинство операций (вероятно) не требуют потока, потому что большинство операций (вероятно) включают только несколько строк. Используйте поток для массовых операций.
  • поделиться одним SQLiteDatabase экземпляр для каждой БД на диске между потоками и реализовать систему подсчета для отслеживания открытых соединений.

есть ли какие-либо лучшие практики для этих сценарии?

поделитесь статическим полем между всеми вашими классами. Раньше я держал синглтон для этого и других вещей, которые нужно разделить. Схема подсчета (обычно с использованием 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 [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 <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();
                }
        } 
    }
}

база данных очень гибкая с multi-threading. Мои приложения одновременно попадают в свои DBs из разных потоков, и все в порядке. В некоторых случаях у меня есть несколько процессов, одновременно попадающих в БД, и это тоже отлично работает.

ваши асинхронные задачи-используйте одно и то же соединение, когда можете, но если вам нужно, его OK для доступа к БД из разных задач.


ответ Дмитрия отлично работает для моего случая. Я думаю, что лучше объявить функцию синхронизированной. по крайней мере, для моего случая он будет вызывать исключение нулевого указателя в противном случае, например getWritableDatabase еще не возвращен в одном потоке и openDatabse вызывается в другом потоке.

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

мое понимание API SQLiteDatabase заключается в том, что если у вас есть многопоточное приложение, вы не можете позволить себе иметь более 1 объекта SQLiteDatabase, указывающего на одну базу данных.

объект определенно может быть создан, но вставки / обновления терпят неудачу, если разные потоки / процессы (тоже) начинают использовать разные объекты SQLiteDatabase (например, как мы используем в соединении JDBC).

единственное решение здесь-придерживаться 1 SQLiteDatabase объектов и всякий раз, когда startTransaction () используется в более чем 1 потоке, Android управляет блокировкой в разных потоках и позволяет только 1 потоку одновременно иметь эксклюзивный доступ к обновлению.

также вы можете сделать "считывает" из базы данных и использовать тот же ошибки в другом потоке (в то время как другой поток записывает) и не будет коррупции базы данных.е "читать поток" не будет считывать данные из базы данных, пока "нить написать" совершает данные, хотя оба используют тот же Ошибки.

Это отличается от того, как объект соединения находится в JDBC, где, если вы передаете (используете то же самое) объект соединения между потоками чтения и записи, мы, вероятно, также будем печатать незафиксированные данные.

в моем корпоративном приложении я пытаюсь использовать условные проверки, чтобы потоку пользовательского интерфейса никогда не приходилось ждать, в то время как поток BG содержит объект SQLiteDatabase (исключительно). Я пытаюсь предсказать действия пользовательского интерфейса и отложить запуск потока BG для "x" секунд. Также можно поддерживать PriorityQueue для управления раздачей объектов подключения SQLiteDatabase, чтобы поток пользовательского интерфейса получал его первым.


после борьбы с этим в течение нескольких часов я обнаружил, что вы можете использовать только один вспомогательный объект db для выполнения 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();
}

как apposed к:

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 каждый раз, когда цикл повторяется, было единственным способом получить мои строки в базу данных через мой вспомогательный класс.


имея некоторые проблемы, я думаю, что понял, почему я ошибался.

Я написал класс оболочки базы данных, который включал close() который вызвал помощника близко как зеркало open() который вызвал getWriteableDatabase, а затем переместился в ContentProvider. Модель ContentProvider не использовать SQLiteDatabase.close() который, я думаю, является большой подсказкой, поскольку код использует getWriteableDatabase в некоторых случаях я все еще делал прямой доступ (запросы проверки экрана в основном, поэтому я перенесен в модель getWriteableDatabase/rawQuery.

Я использую Синглтон, и есть немного зловещий комментарий в закрытой документации

закрыть любой открыть объект базы данных

(жирный шрифт мой).

поэтому у меня были прерывистые сбои, когда я использую фоновые потоки для доступа к базе данных, и они работают одновременно с передним планом.

так я думаю close() заставляет базу данных закрываться независимо от любых других потоков, содержащих ссылки-so close() сам по себе не просто отменить соответствие getWriteableDatabase но сила закрытия любой открытые запросы. В большинстве случаев это не проблема, поскольку код является однопоточным, но в многопоточных случаях всегда есть вероятность открытия и закрытия из синхронизации.

прочитав комментарии в другом месте, которое объясняет, что экземпляр кода SqLiteDatabaseHelper подсчитывает, то единственный раз, когда вы хотите закрыть - это когда вы хотите ситуацию, когда вы хотите сделать резервную копию, и вы хотите заставить все соединения быть закрытыми и заставить SqLite списать все кэшированные вещи, которые могут болтаться - другими словами, остановить всю активность базы данных приложений, закрыть на случай, если помощник потерял след, сделайте любую активность уровня файла (резервное копирование/восстановление), а затем начните все сначала.

хотя это звучит как хорошая идея, чтобы попытаться закрыть контролируемым образом, реальность такова, что Android оставляет за собой право выбросить вашу виртуальную машину, поэтому любое закрытие снижает риск того, что кэшированные обновления не будут записаны, но это не может быть гарантировано, если устройство напряжено, и если вы правильно освободили курсоры и ссылки на базы данных (которые не должны быть статическими членами), то помощник все равно закроет базу данных.

поэтому я считаю, что подход таков:

используйте getWriteableDatabase для открытия из одноэлементной оболочки. (Я использовал производное приложение класс для предоставления контекста приложения из статического, чтобы устранить необходимость в контексте).

никогда напрямую вызывать метод close.

никогда не храните результирующую базу данных в любом объекте, который не имеет очевидной области и полагается на подсчет ссылок для запуска неявного close ().

если вы выполняете обработку уровня файла, остановите все действия базы данных, а затем вызовите close на случай, если есть убегающий поток при условии, что вы пишете правильно транзакции таким образом, беглый поток завершится неудачей, и закрытая база данных будет иметь по крайней мере правильные транзакции, а не потенциально копию частичной транзакции на уровне файла.


вы можете попробовать применить новый подход к архитектуре объявила в Google I / O 2017.

Он также включает в себя новую библиотеку 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.
}

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

Я знаю, что ответ запаздывает, но лучший способ выполнить запросы sqlite в android-через пользовательский поставщик контента. Таким образом, пользовательский интерфейс отделяется от класса базы данных(класс, который расширяет класс SQLiteOpenHelper). Также запросы выполняются в фоновом потоке (Cursor Loader).