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

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

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

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

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

Ответ 1

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

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

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

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

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

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

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

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

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


Тем временем в блоге появляется следующее сообщение:

Также проверьте вилку на 2point0 предыдущего примера блокировки:

Ответ 2

Параллельный доступ к базе данных

Та же статья в моем блоге (мне больше нравится форматирование)

Я написал небольшую статью, в которой описывается, как сделать доступ к вашей базе данных 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, вы фактически устанавливаете новое соединение с базой данных. Если вы попытаетесь одновременно выполнить запись в базу данных из разных соединений, произойдет сбой. (из ответа выше)

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

Давайте создадим одноэлементный класс Database Manager, который будет содержать и возвращать один объект 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() возвращает один и тот же экземпляр объекта SQLiteDatabase для Thread1 и Thread2. Что происходит, Thread1 может закрыть базу данных, в то время как Thread2 все еще использует ее. Вот почему у нас происходит сбой IllegalStateException.

Мы должны убедиться, что никто не использует базу данных, и только потом закрывать ее. Некоторые люди в stackoveflow рекомендуют никогда не закрывать вашу базу данных SQLite. Это приведет к следующему сообщению 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

Каждый раз, когда вам нужна база данных, вы должны вызывать метод openDatabase() класса DatabaseManager. Внутри этого метода у нас есть счетчик, который показывает, сколько раз открывается база данных. Если он равен единице, это означает, что нам нужно создать новое соединение с базой данных, если нет, соединение с базой данных уже создано.

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


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

Ответ 3

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

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

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

Мое решение:

Для самой последней версии см. https://github.com/JakarCo/databasemanager, но я постараюсь также обновить код. Если вы хотите понять мое решение, посмотрите на код и прочитайте мои заметки. Мои заметки обычно очень полезны.

  • скопируйте/вставьте код в новый файл с именем DatabaseManager. (или загрузить его из github)
  • расширяем DatabaseManager и реализуем onCreate и onUpgrade, как обычно. Вы можете создать несколько подклассов одного класса DatabaseManager, чтобы иметь разные базы данных на диске.
  • Создайте свой подкласс и вызовите getDb(), чтобы использовать класс SQLiteDatabase.
  • Вызов 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 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();
                }
        } 
    }
}

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

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

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

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

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

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

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

Ответ 7

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

в соответствии с:

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

Ответ 8

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

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

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

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

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

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

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

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

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

Итак, мой подход заключается в том, что подход:

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

Нельзя напрямую закрыть.

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

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

Ответ 9

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

Он также включает новую библиотеку ORM, называемую Room

Он содержит три основных компонента: @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();
}

Ответ 10

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