Які найкращі практики для SQLite на Android?


694

Що б вважалося найкращим методом під час виконання запитів у базі даних SQLite в додатку Android?

Чи безпечно запускати вставки, видаляти та вибирати запити з doInBackground AsyncTask? Або я повинен використовувати нитку інтерфейсу користувача? Я припускаю, що запити до бази даних можуть бути "важкими" і не повинні використовувати потік інтерфейсу, оскільки він може заблокувати додаток - в результаті чого додаток не відповідає (ANR).

Якщо у мене є декілька AsyncTasks, вони повинні мати спільне з'єднання або вони повинні відкривати з'єднання кожен?

Чи є найкращі практики для цих сценаріїв?


10
Що б ви не робили, пам’ятайте про санітарні дані, якщо ваш постачальник вмісту (або інтерфейс SQLite) публічно стикається!
Крістофер Мічінський

37
Ви, безумовно, НЕ повинні робити db-доступу з потоку користувальницького інтерфейсу, я можу вам сказати, що дуже багато.
Едвард Фолк

@EdwardFalk Чому б і ні? Напевно, є випадки використання, коли це правильно робити?
Майкл

4
Якщо ви робите якісь введення / виведення, доступ до мережі тощо тощо з потоку інтерфейсу, весь пристрій заморожується до завершення операції. Якщо це завершиться за 1/20 секунди, то добре. Якщо це займе більше часу, у вас поганий досвід користувача.
Едвард Фолк

Відповіді:


631

Вставки, оновлення, видалення та читання , як правило , в порядку з декількох потоків, але Бреда відповідь не є правильним. Ви повинні бути обережними з тим, як створюєте свої зв’язки та використовуєте їх. Бувають ситуації, коли ваші дзвінки за оновлення не вдається, навіть якщо ваша база даних не буде пошкоджена.

Основна відповідь.

Об'єкт SqliteOpenHelper підтримує одне підключення до бази даних. Здається, пропонують вам зв’язок читання та запису, але насправді це не так. Зателефонуйте лише для читання, і ви отримаєте з'єднання з базою даних записів незалежно.

Отже, один екземпляр помічника, одне db-з'єднання. Навіть якщо ви використовуєте його з декількох потоків, по одному з'єднанню. Об'єкт SqliteDatabase використовує блоки Java, щоб зберігати серіалізований доступ. Отже, якщо 100 потоків мають один екземпляр db, виклики до фактичної бази даних на диску серіалізуються.

Отже, один помічник, один db-зв’язок, який серіалізується в коді Java. Один потік, 1000 потоків, якщо ви використовуєте один помічник екземпляра, який спільний між ними, весь ваш код доступу db є послідовним. І життя добре (іш).

Якщо ви спробуєте одночасно записати в базу даних фактичні розрізнені з'єднання, це не вдасться. Він не чекатиме, коли буде зроблено перше, а потім напишіть. Він просто не напише вашу зміну. Гірше, якщо ви не зателефонуєте на правильну версію вставки / оновлення на базі даних SQLiteDatabase, ви не отримаєте винятку. Ви просто отримаєте повідомлення у своєму LogCat, і це буде все.

Отже, кілька ниток? Використовуйте одного помічника. Період. Якщо ви знаєте, що буде записано лише один потік, ви МОЖЕТЕ мати можливість використовувати декілька підключень, і ваші читання будуть швидшими, але будьте обачні. Я не тестував так багато.

Ось допис у блозі з більш детальною інформацією та прикладом програми.

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


Тим часом є додаткова публікація блогу:

Також огляньте вилку на 2 точки0 згаданого раніше прикладу блокування:


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

Ви отримаєте наступне повідомлення у вашій реєстраційній системі, і одна із змін не буде записана.

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


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


10
Я з тих людей, які пропонують вам ніколи не закривати db. Помилка "Знайдено витоку" ви отримуєте лише в тому випадку, якщо відкриєте db, не закривайте його, а потім спробуйте відкрити знову. Якщо ви використовуєте лише один відкритий помічник і ніколи не закриваєте db, ви не отримаєте цієї помилки. Якщо ви виявите інше, будь ласка, повідомте мене (з кодом). У мене був десь довший пост про це десь, але не можу його знайти. На питання тут відповідає комунальне забезпечення, хто з нас випереджає нас обох у відділі SO: stackoverflow.com/questions/7211941/…
Кевін Галліган

4
Більше думок. №1, я створив би помічника у вашого менеджера. Попросити проблем мати його зовні. Новий диявол може викликати помічника безпосередньо з божевільної причини. Крім того, якщо вам потрібно буде використовувати метод init, киньте виняток, якщо екземпляр вже існує. Програми з декількома db, очевидно, не спрацюють як є. №2, чому поле mDatabase? Її можна отримати у помічника. # 3, як ваш перший крок до "ніколи не закривається", що відбувається з вашим db, коли ваш додаток виходить з ладу і не "закривається"? Підказка, нічого. Це добре, тому що SQLite супер стабільний. Це був крок 1, щоб з'ясувати, чому вам не потрібно його закривати.
Кевін Галліган

1
Будь-яка причина, чому ви використовуєте загальнодоступний метод ініціалізації для виклику до отримання екземпляра? Чому б не мати приватного конструктора, який називається if(instance==null)? Ви не залишаєте іншого вибору, як викликати ініціалізацію кожного разу; як інакше ви дізнаєтесь, чи було вона ініціалізована чи ні в інших додатках тощо?
ChiefTwoPencils

1
initializeInstance()має параметр типу SQLiteOpenHelper, але у своєму коментарі ви згадали про використання DatabaseManager.initializeInstance(getApplicationContext());. Що відбувається? Як це можливо працює?
фазал

2
@DmytroDanylyk "DatabaseManager - це безпечний потік синглів, тому його можна використовувати будь-де", що не відповідає дійсності на запитання virsir. Об'єкти не є спільними перехресними процесами. У вашого DatabaseManager не буде стану в іншому процесі, як у адаптері синхронізації (android: process = ": sync")
свист

17
  • Використовуйте a Threadабо AsyncTaskдля тривалих операцій (50ms +). Перевірте свою програму, щоб побачити, де це. Більшість операцій (ймовірно) не потребують потоку, оскільки більшість операцій (ймовірно) включає лише кілька рядків. Використовуйте нитку для об'ємних операцій.
  • Поділіться один 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
Що відбувається, коли ви телефонуєте "закрити", а потім намагаєтесь повторно використовувати клас? це впаде? чи він автоматично повторно ініціалізує себе, щоб мати можливість знову використовувати БД?
андроїд розробник

1
@androiddeveloper. Якщо ви телефонуєте close, вам доведеться зателефонувати openще раз, перш ніж використовувати той самий екземпляр класу АБО ви можете створити новий екземпляр. Оскільки в closeкоді я встановив db=null, ви не зможете використовувати повернене значення з getDb(оскільки це буде нульовим), тож ви отримаєте, NullPointerExceptionякби зробили щось на кшталтmyInstance.close(); myInstance.getDb().query(...);
Рід

Чому б не поєднати getDb()і не open()в єдиний метод?
Алекс Бурдузель

@ Бурду, поєднання надання додаткового контролю над лічильником бази даних та поганим дизайном. Це, безумовно, не найкращий спосіб. Я оновлю його через кілька днів.
Рід

@Burdu, я щойно оновив його. Ви можете отримати новий код тут . Я цього не перевіряв, тому, будь ласка, повідомте мене, чи варто вносити зміни.
Рід

11

База даних дуже гнучка з багатопотоковою резьбою. Мої програми одночасно вражають свої БД з багатьох різних потоків, і це чудово. У деяких випадках у мене одночасно потрапляють декілька процесів, і це працює чудово.

Ваші завдання з асинхронізації - використовуйте те саме з'єднання, коли можете, але якщо вам доведеться, це OK, щоб отримати доступ до БД з різних завдань.


Також у вас є читачі та письменники, які перебувають у різних зв’язках, або вони мають спільне з'єднання? Дякую.
Сірий

@Gray - правильно, я мав би це прямо сказати. Що стосується з'єднань, я би максимально використовував одне і те ж з’єднання, але оскільки блокування обробляється на рівні файлової системи, ви можете відкрити його кілька разів у коді, але я би використав одне з'єднання якомога більше. Android sqlite DB є дуже гнучким і прощальним.
Бред Хайн

3
@Gray, просто хотів опублікувати оновлену інформацію для людей, які, можливо, використовують цей метод. Документація говорить: Цей метод зараз нічого не робить. Не використовувати.
Піжусн

3
Я виявив, що цей метод жахливо не працює, ми переходимо до ContentProvider для доступу з декількох додатків. Нам доведеться домогтися певної сукупності наших методів, але це повинно вирішити будь-які проблеми з процесами, які одночасно отримують доступ до даних.
JPM

2
Я знаю, що це старе, але це неправильно. Це може спрацювати з доступом до БД з різних SQLiteDatabaseоб’єктів на різних AsyncTasks / Threads, але іноді це призведе до помилок, через що SQLiteDatabase (рядок 1297) використовує Locks
Reed

7

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

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

mDatabaseHelper.getWritableDatabase (); Це не буде створювати новий об'єкт бази даних
віддалений

5

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


4

Я розумію, що API API SQLiteDatabase полягає в тому, що якщо у вас є багатопотокове додаток, ви не можете дозволити собі мати більше 1 об'єкта SQLiteDatabase, що вказує на єдину базу даних.

Об'єкт, безумовно, може бути створений, але вставки / оновлення виходять з ладу, якщо різні потоки / процеси (теж) починають використовувати різні об'єкти SQLiteDatabase (наприклад, як ми використовуємо в JDBC Connection).

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

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

Це відрізняється від того, яким є об’єкт з'єднання в JDBC, де, якщо ви перейдете (використовуєте те саме) об'єкт з'єднання між потоками читання та запису, то, ймовірно, ми також надрукуємо непередані дані.

У моєму додатку для підприємства я намагаюся використовувати умовні перевірки, щоб потоку інтерфейсу ніколи не довелося чекати, в той час як BG-потік містить об'єкт SQLiteDatabase (виключно). Я намагаюся спрогнозувати дії інтерфейсу та відстрочити показ потоку BG на "х" секунд. Також можна підтримувати PriorityQueue, щоб керувати передачею об'єктів SQLiteDatabase Connection, щоб потік інтерфейсу отримував його першим.


І що ви вкладаєте в цю PriorityQueue - слухачів (які хочуть отримати об’єкт бази даних) або SQL запитів?
Pijusn

Я не використовував підхід черги пріоритетів, але по суті "нитки", що викликає.
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, що називається Кімната

Він містить три основні компоненти: @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();
}

Я не пропоную номер із базою даних з декількома N-N-зв’язками, оскільки він не справляється з таким підходом і вам потрібно написати багато коду, щоб знайти вирішення цих відносин.
AlexPad

3

Маючи деякі проблеми, я думаю, я зрозумів, чому я пішов не так.

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

Я використовую синглтон, і в тісній документації є трохи зловісний коментар

Закрийте будь-який об’єкт відкритої бази даних

(моя сміливість).

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

Тому я думаю, що close()змушує базу даних закриватися незалежно від будь-яких інших потоків, що містять посилання - тому close()сама по собі не просто скасовує відповідність, getWriteableDatabaseа й примушує закривати будь-які відкриті запити. У більшості випадків це не є проблемою, оскільки код є однократним потоком, але у багатопотокових випадках завжди є шанс відкрити та закрити синхронізацію.

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

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

Тож я вважаю, що підхід такий:

Використовуйте базу даних getWriteableDatabase, щоб відкрити з однотонної обгортки. (Я використовував похідний клас програми для надання контексту програми зі статики для вирішення потреби в контексті).

Ніколи безпосередньо не дзвоніть близько.

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

Якщо ви здійснюєте обробку рівня файлів, припиніть всю активність бази даних, а потім зателефонуйте закрити на випадок, якщо є відверта нитка з припущенням, що ви пишете належні транзакції, так що потоковий потік вийде з ладу, а закрита база даних, принаймні, матиме належні транзакції ніж потенційно копія на рівні файлу часткової транзакції.


0

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.