Каковы лучшие практики для SQLite на Android? - PullRequest
667 голосов
/ 22 марта 2010

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

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

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

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

Ответы [ 10 ]

619 голосов
/ 11 сентября 2010

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

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

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

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

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

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

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

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

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


Тем временем, есть еще одно сообщение в блоге:

Также проверьте вилку с помощью 2point0 из вышеупомянутого примера блокировки:

185 голосов
/ 15 ноября 2013

одновременный доступ к базе данных

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

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

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

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

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


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

17 голосов
/ 30 октября 2014
  • Используйте 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 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();
                }
        } 
    }
}
11 голосов
/ 22 марта 2010

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

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

7 голосов
/ 07 января 2014

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

public synchronized SQLiteDatabase openDatabase() {
        if(mOpenCounter.incrementAndGet() == 1) {
            // Opening new database
            mDatabase = mDatabaseHelper.getWritableDatabase();
        }
        return mDatabase;
    }
5 голосов
/ 29 июня 2011

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

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

4 голосов
/ 23 мая 2017

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

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

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

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

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

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

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

3 голосов
/ 04 марта 2017

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

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

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

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

(моя смелость).

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

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

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

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

Итак, я считаю, что подход:

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

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

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

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

0 голосов
/ 24 ноября 2017

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...