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

694

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

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

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

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

Видар Вестнес
источник
10
Что бы вы ни делали, не забывайте дезинфицировать свои входные данные, если ваш контент-провайдер (или интерфейс SQLite) находится в открытом доступе!
Кристофер Мичински
37
Вы определенно не должны делать доступ к БД из потока пользовательского интерфейса, я могу вам сказать это очень много.
Эдвард Фальк
@EdwardFalk Почему бы и нет? Наверняка есть случаи использования, где это допустимо?
Майкл
4
Если вы выполняете какие-либо операции ввода-вывода, доступа к сети и т. Д. Из потока пользовательского интерфейса, все устройство останавливается до завершения операции. Если это завершится за 1/20 секунды, то хорошо. Если это займет больше времени, у вас плохой пользовательский опыт.
Эдвард Фальк

Ответы:

631

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

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

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

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

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

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

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

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

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


А пока есть пост в блоге:

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

Кевин Галлиган
источник
2
Кроме того, поддержку Android для Ormlite можно найти по адресу ormlite.sourceforge.net/sqlite_java_android_orm.html . Есть примеры проектов, документация и банки.
Серый
1
Второй в стороне. В коде ormlite есть вспомогательные классы, которые можно использовать для управления экземплярами dbhelper. Вы можете использовать материал ormlite, но это не обязательно. Вы можете использовать вспомогательные классы только для управления соединением.
Кевин Галлиган
31
Кагий, спасибо за подробное объяснение. Не могли бы вы пояснить одну вещь - я понимаю, что у вас должен быть ОДИН помощник, но вы должны также иметь только одно соединение (т.е. один объект SqliteDatabase)? Другими словами, как часто вы должны вызывать getWritableDatabase? И не менее важно, когда вы вызываете close ()?
Артем
3
Я обновил код. Оригинал был утерян, когда я сменил блог-хостеров, но я добавил уменьшенный пример кода, который продемонстрирует проблему. Кроме того, как вы управляете одним соединением? Сначала у меня было гораздо более сложное решение, но с тех пор я исправил это. Взгляните сюда: touchlab.co/uncategorized/single-sqlite-connection
Кевин Галлиган
есть ли необходимость утилизировать соединение и где это сделать?
августа
189

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

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

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

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


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

Дмитрий Данилык
источник
10
Я один из тех людей, которые предлагают вам никогда не закрывать БД. Вы получите ошибку «Leak found» только если откроете БД, не закроете ее, а затем попытаетесь открыть снова. Если вы используете только один открытый помощник и никогда не закрываете базу данных, вы не получите эту ошибку. Если вы найдете другое, пожалуйста, дайте мне знать (с кодом). У меня где-то был длинный пост об этом, но я не могу его найти. Вопрос, на который отвечает commonsware, который как бы обыгрывает нас обоих в отделе SO points: stackoverflow.com/questions/7211941/…
Кевин Галлиган
4
Больше мыслей. # 1, я бы создал помощника внутри вашего менеджера. Запрашиваемая проблемы, чтобы иметь его на улице. Новый разработчик может вызвать помощника по какой-то безумной причине. Кроме того, если вам требуется метод init, создайте исключение, если экземпляр уже существует. Приложения Multi-DB, очевидно, потерпят неудачу как есть. # 2, почему поле mDatabase? Его можно получить у помощника. # 3, как ваш первый шаг к «никогда не закрывать», что происходит с вашей БД, когда ваше приложение падает и не «закрывается»? Намек, ничего. Это хорошо, потому что SQLite супер стабильный. Это был первый шаг в выяснении, почему вам не нужно закрывать его.
Кевин Галлиган
1
Любая причина, почему вы используете публичный метод инициализации для вызова до получения экземпляра? Почему бы не иметь частный конструктор, который называется if(instance==null)? Вы не оставляете выбора, кроме как каждый раз вызывать инициализацию; как еще узнать, инициализирован ли он или нет в других приложениях и т. д.?
ChiefTwoPencils
1
initializeInstance()имеет параметр типа SQLiteOpenHelper, но в своем комментарии вы упомянули об использовании DatabaseManager.initializeInstance(getApplicationContext());. Что здесь происходит? Как это может работать?
Файзал
2
@DmytroDanylyk «DatabaseManager - это потокобезопасный синглтон, поэтому его можно использовать где угодно», что неверно в ответ на вопрос virsir. Объекты не являются общими кросс-процессами. Ваш DatabaseManager не будет иметь состояния в другом процессе, как в адаптере синхронизации (android: process = ": sync")
whizzle
17
  • Используйте 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();
                }
        } 
    }
}
камыш
источник
1
Что происходит, когда вы звоните «закрыть», а затем пытаетесь повторно использовать класс? это потерпит крах? или он автоматически переинициализирует себя, чтобы иметь возможность снова использовать БД?
Android-разработчик
1
@androiddeveloper, Если вы звоните close, вы должны позвонить openснова, прежде чем использовать тот же экземпляр класса ИЛИ вы можете создать новый экземпляр. Поскольку в closeкоде, который я установил db=null, вы не сможете использовать возвращаемое значение из getDb(поскольку оно будет нулевым), так что вы получите, NullPointerExceptionесли бы вы сделали что-то вродеmyInstance.close(); myInstance.getDb().query(...);
Reed
Почему бы не объединить getDb()и open()в один метод?
Алекс Бурдусел
@Burdu, сочетание дополнительного контроля счетчика базы данных и плохого дизайна. Хотя это определенно не лучший способ сделать это. Я обновлю это через несколько дней.
Рид
@ Бурду, я только что обновил. Вы можете получить новый код отсюда . Я не проверял это, поэтому, пожалуйста, дайте мне знать, если я должен зафиксировать изменения.
Рид
11

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

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

Брэд Хейн
источник
Кроме того, у вас есть читатели и писатели в разных связях, или они должны иметь одну связь? Спасибо.
Серый,
@ Серый - правильно, я должен был упомянуть об этом явно. Что касается соединений, я бы максимально использовал одно и то же соединение, но поскольку блокировка обрабатывается на уровне файловой системы, вы можете открыть его несколько раз в коде, но я бы использовал как можно больше одного соединения. Android sqlite DB очень гибок и простителен.
Брэд Хейн
3
@Gray, просто хотел опубликовать обновленную информацию для людей, которые могут использовать этот метод. Документация гласит: этот метод сейчас ничего не делает. Не используйте.
Пиюсн
3
Я обнаружил, что этот метод ужасно терпит неудачу, мы переключаемся на ContentProvider для доступа из нескольких приложений. Нам нужно будет выполнить некоторые параллельные операции с нашими методами, но это должно устранить любые проблемы с процессами, которые обращаются к данным одновременно.
JPM
2
Я знаю, что это старый, но это неверно. Это может работать нормально для доступа к БД из разных SQLiteDatabaseобъектов на разных AsyncTaskс / Threadс, но иногда это приводит к ошибкам, поэтому SQLiteDatabase (строка 1297) использует Locks
Reed
7

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

public synchronized SQLiteDatabase openDatabase() {
    if(mOpenCounter.incrementAndGet() == 1) {
        // Opening new database
        mDatabase = mDatabaseHelper.getWritableDatabase();
    }
    return mDatabase;
}
gonglong
источник
mDatabaseHelper.getWritableDatabase (); Это не будет создавать новый объект базы данных
отдаленный
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();
}

в отношении:

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

dell116
источник
4

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

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

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

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

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

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

Swaroop
источник
И что вы помещаете в этот PriorityQueue - слушатели (которые хотят получить объект базы данных) или SQL-запросы?
Пиюсн
Я не использую подход с приоритетной очередью, но, по сути, «вызывающие» потоки.
Swaroop
@Swaroop: PCMIIW, "read thread" wouldn't read the data from the database till the "write thread" commits the data although both use the same SQLiteDatabase object. Это не всегда так, если вы начнете «читать поток» сразу после «записи потока», вы можете не получить недавно обновленные данные (вставленные или обновленные в записи потока). Читать поток может прочитать данные до начала записи потока. Это происходит потому, что операция записи изначально включает зарезервированную блокировку вместо эксклюзивной блокировки.
Амит Викрам Сингх
4

Вы можете попробовать применить новый архитектурный подход, анонсированный на 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();
}
Зимбо Роджер
источник
Я не предлагаю Room to Database с несколькими N-N-отношениями, потому что он плохо обрабатывает этот подход, и вам нужно написать много кода, чтобы найти обходной путь для этих отношений.
AlexPad
3

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

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

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

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

(мой смелый).

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

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

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

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

Поэтому я считаю, что подход заключается в следующем:

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

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

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

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

Ян Спенсер
источник
0

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

Тео
источник