Jakie są najlepsze praktyki dotyczące SQLite na Androida?

694

Jakie byłyby najlepsze praktyki podczas wykonywania zapytań w bazie danych SQLite w aplikacji na Androida?

Czy uruchamianie wstawek, usuwanie i wybieranie zapytań z doInBackground AsyncTask jest bezpieczne? Czy powinienem użyć wątku interfejsu użytkownika? Przypuszczam, że zapytania do bazy danych mogą być „ciężkie” i nie powinny używać wątku interfejsu użytkownika, ponieważ mogą blokować aplikację - w wyniku czego aplikacja nie odpowiada (ANR).

Jeśli mam kilka zadań AsyncTasks, czy powinny one współużytkować połączenie, czy też powinny je otwierać?

Czy są jakieś najlepsze praktyki dla tych scenariuszy?

Vidar Vestnes
źródło
10
Cokolwiek zrobisz, pamiętaj o oczyszczeniu danych wejściowych, jeśli twój dostawca treści (lub interfejs SQLite) jest publicznie skierowany!
Kristopher Micinski
37
Zdecydowanie NIE powinieneś robić dostępu do bazy danych z wątku interfejsu użytkownika, mogę ci tyle powiedzieć.
Edward Falk
@EdwardFalk Dlaczego nie? Z pewnością istnieją przypadki użycia, w których można to zrobić?
Michael,
4
Jeśli wykonasz dowolne operacje we / wy, dostęp do sieci itp. Z wątku interfejsu użytkownika, całe urządzenie zawiesza się do momentu zakończenia operacji. Jeśli zakończy się w ciągu 1/20 sekundy, to dobrze. Jeśli to potrwa dłużej, masz złą obsługę.
Edward Falk,

Odpowiedzi:

631

Wstawki, aktualizacje, usunięcia i odczyty są ogólnie OK z wielu wątków, ale odpowiedź Brada jest nieprawidłowa. Musisz uważać na sposób tworzenia połączeń i korzystania z nich. Są sytuacje, w których wywołania aktualizacji zakończą się niepowodzeniem, nawet jeśli baza danych nie zostanie uszkodzona.

Podstawowa odpowiedź.

Obiekt SqliteOpenHelper utrzymuje jedno połączenie z bazą danych. Wygląda na to, że oferuje połączenie do odczytu i zapisu, ale tak naprawdę nie jest. Zadzwoń tylko do odczytu, a otrzymasz połączenie do bazy danych zapisu niezależnie od tego.

Tak więc jedna instancja pomocnika, jedno połączenie db. Nawet jeśli używasz go z wielu wątków, jedno połączenie na raz. Obiekt SqliteDatabase używa blokad java, aby utrzymać dostęp do szeregów. Jeśli więc 100 wątków ma jedną instancję db, wywołania do rzeczywistej bazy danych na dysku są serializowane.

Tak więc jeden pomocnik, jedno połączenie db, które jest serializowane w kodzie Java. Jeden wątek, 1000 wątków, jeśli korzystasz z jednej współużytkowanej instancji pomocnika, cały kod dostępu db jest szeregowy. A życie jest dobre (ish).

Jeśli spróbujesz pisać do bazy danych z rzeczywistych odrębnych połączeń w tym samym czasie, jedno zakończy się niepowodzeniem. Nie będzie czekać, aż zrobi się pierwszy, a potem napisze. Po prostu nie napisze twojej zmiany. Co gorsza, jeśli nie wywołasz odpowiedniej wersji wstawiania / aktualizacji na SQLiteDatabase, nie otrzymasz wyjątku. Po prostu dostaniesz wiadomość w swoim LogCat i to wszystko.

A więc wiele wątków? Użyj jednego pomocnika. Kropka. Jeśli WIESZ, że będzie pisał tylko jeden wątek, MOŻESZ być w stanie korzystać z wielu połączeń, a twoje odczyty będą szybsze, ale kupujący się strzeże. Nie testowałem zbyt wiele.

Oto post na blogu ze znacznie bardziej szczegółowymi informacjami i przykładową aplikacją.

Gray i ja właśnie opracowujemy narzędzie ORM oparte na jego Ormlite, które działa natywnie z implementacjami baz danych Androida i postępuje zgodnie z bezpieczną strukturą tworzenia / wywoływania opisaną w poście na blogu. To powinno być wkrótce. Spójrz.


W międzyczasie jest kolejny post na blogu:

Sprawdź także rozwidlenie o 2 punkt0 wcześniej wspomnianego przykładu blokowania:

Kevin Galligan
źródło
2
Nawiasem mówiąc, wsparcie Androida Ormlite można znaleźć na stronie ormlite.sourceforge.net/sqlite_java_android_orm.html . Istnieją przykładowe projekty, dokumentacja i słoiki.
Grey,
1
Drugi na bok. Kod ormlite ma klasy pomocnicze, których można użyć do zarządzania instancjami dbhelper. Możesz użyć rzeczy ormlite, ale nie jest to wymagane. Możesz użyć klas pomocniczych tylko do zarządzania połączeniami.
Kevin Galligan,
31
Kāgii, dziękuję za szczegółowe wyjaśnienie. Czy możesz wyjaśnić jedną rzecz - Rozumiem, że powinieneś mieć JEDEN pomocnika, ale czy powinieneś także mieć tylko jedno połączenie (tj. Jeden obiekt SqliteDatabase)? Innymi słowy, jak często należy wywoływać getWritableDatabase? I co równie ważne, kiedy wywołujesz close ()?
Artem
3
Zaktualizowałem kod. Oryginał został utracony po zmianie hostów blogów, ale dodałem trochę uproszczonego przykładowego kodu, który zademonstruje problem. Jak zarządzasz pojedynczym połączeniem? Początkowo miałem znacznie bardziej skomplikowane rozwiązanie, ale później to zmieniłem. Spójrz tutaj: touchlab.co/uncategorized/single-sqlite-connection
Kevin Galligan
czy trzeba pozbyć się połączenia i gdzie to zrobić?
tugce
189

Jednoczesny dostęp do bazy danych

Ten sam artykuł na moim blogu (lubię formatować więcej)

Napisałem mały artykuł opisujący, jak zapewnić bezpieczny dostęp do wątku bazy danych Androida.


Zakładając, że masz własny SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Teraz chcesz zapisać dane w bazie danych w osobnych wątkach.

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

Otrzymasz następujący komunikat w logcat i jedna ze zmian nie zostanie zapisana.

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

Dzieje się tak, ponieważ za każdym razem, gdy tworzysz nowy obiekt SQLiteOpenHelper , faktycznie tworzysz nowe połączenie z bazą danych. Jeśli spróbujesz pisać do bazy danych z rzeczywistych odrębnych połączeń w tym samym czasie, jedno zakończy się niepowodzeniem. (z odpowiedzi powyżej)

Aby korzystać z bazy danych z wieloma wątkami, musimy upewnić się, że korzystamy z jednego połączenia z bazą danych.

Stwórzmy menedżera bazy danych klasy singleton, który przechowa i zwróci pojedynczy obiekt 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();
    }

}

Zaktualizowany kod, który zapisuje dane do bazy danych w osobnych wątkach, będzie wyglądał następująco.

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

Spowoduje to kolejną awarię.

java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase

Skoro jesteśmy przy użyciu tylko jednego połączenia z bazą danych, metoda getDatabase () zwrócić tę samą instancję SQLiteDatabase obiektu dla thread1 i thread2 . Co się dzieje, Thread1 może zamknąć bazę danych, podczas gdy Thread2 nadal z niej korzysta. Właśnie dlatego mamy awarię IllegalStateException .

Musimy upewnić się, że nikt nie korzysta z bazy danych, a dopiero potem ją zamknąć. Niektórzy ludzie na stackoveflow zalecają, aby nigdy nie zamykać SQLiteDatabase . Spowoduje to wyświetlenie następującego komunikatu logcat.

Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed

Próbka robocza

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

        }
    }

}

Użyj go w następujący sposób.

SQLiteDatabase database = DatabaseManager.getInstance().openDatabase();
database.insert(...);
// database.close(); Don't close it directly!
DatabaseManager.getInstance().closeDatabase(); // correct way

Za każdym razem trzeba bazę danych należy zadzwonić openDatabase () metodę DatabaseManager klasie. Wewnątrz tej metody mamy licznik wskazujący, ile razy baza danych jest otwierana. Jeśli jest równy jeden, oznacza to, że musimy utworzyć nowe połączenie z bazą danych, jeśli nie, połączenie z bazą danych jest już utworzone.

To samo dzieje się w metodzie closeDatabase () . Za każdym razem, gdy wywołujemy tę metodę, licznik jest zmniejszany, za każdym razem, gdy osiąga zero, zamykamy połączenie z bazą danych.


Teraz powinieneś być w stanie korzystać z bazy danych i mieć pewność, że jest ona bezpieczna dla wątków.

Dmytro Danylyk
źródło
10
Jestem jedną z tych osób, które sugerują, że nigdy nie zamykaj bazy danych. Błąd „Znaleziono przeciek” pojawia się tylko wtedy, gdy otworzysz bazę danych, nie zamykaj jej, a następnie spróbuj ponownie otworzyć. Jeśli używasz tylko jednego otwartego pomocnika i nigdy nie zamykasz bazy danych, nie pojawia się ten błąd. Jeśli znajdziesz inaczej, daj mi znać (z kodem). Mam gdzieś dłuższy post na ten temat, ale nie mogę go znaleźć. Pytanie, na które odpowiedział tutaj commonsware, który trochę nas obezwładnia w dziale punktów SO: stackoverflow.com/questions/7211941/...
Kevin Galligan
4
Więcej myśli. # 1, stworzę pomocnika w twoim menedżerze. Pytanie o problemy, aby mieć go na zewnątrz. Nowy programista może zadzwonić do pomocnika bezpośrednio z jakiegoś szalonego powodu. Ponadto, jeśli będziesz potrzebować metody init, wyrzuć wyjątek, jeśli instancja już istnieje. Aplikacje obsługujące wiele baz danych oczywiście przestaną działać. # 2, dlaczego pole mDatabase? Jest dostępny od pomocnika. # 3, jako pierwszy krok w kierunku „nigdy nie zamykaj”, co dzieje się z db, gdy aplikacja ulega awarii i nie jest „zamknięta”? Wskazówka, nic. Jest w porządku, ponieważ SQLite jest super stabilny. To był pierwszy krok w ustaleniu, dlaczego nie musisz go zamykać.
Kevin Galligan
1
Czy jest jakiś powód, dla którego używasz publicznej metody inicjowania, aby wywoływać przed uzyskaniem instancji? Dlaczego nie mieć prywatnego konstruktora o nazwie if(instance==null)? Nie pozostawiasz innego wyboru niż inicjowanie połączenia za każdym razem; skąd inaczej miałbyś wiedzieć, czy został on zainicjowany w innych aplikacjach itp.?
ChiefTwoPencils
1
initializeInstance()ma parametr typu SQLiteOpenHelper, ale w komentarzu wspomniałeś o użyciu DatabaseManager.initializeInstance(getApplicationContext());. Co się dzieje? Jak to może działać?
faizal
2
@DmytroDanylyk „DatabaseManager to bezpieczny wątek singleton, więc można go używać wszędzie”, co nie jest prawdą w odpowiedzi na pytanie virsir. Obiekty nie są współdzielonymi procesami krzyżowymi. Twój
menedżer
17
  • Użyj a Threadlub AsyncTaskdo długotrwałych operacji (50ms +). Przetestuj aplikację, aby zobaczyć, gdzie to jest. Większość operacji (prawdopodobnie) nie wymaga wątku, ponieważ większość operacji (prawdopodobnie) obejmuje tylko kilka wierszy. Użyj wątku do operacji masowych.
  • Udostępnij jedną SQLiteDatabaseinstancję dla każdego DB na dysku między wątkami i zaimplementuj system liczący, aby śledzić otwarte połączenia.

Czy są jakieś najlepsze praktyki dla tych scenariuszy?

Udostępnij statyczne pole wszystkim klasom. Kiedyś utrzymywałem singleton do tego i innych rzeczy, które należy udostępnić. Należy również użyć schematu zliczania (zwykle używającego AtomicInteger), aby upewnić się, że baza danych nigdy nie zostanie wcześniej zamknięta lub pozostawiona otwarta.

Moje rozwiązanie:

Aby uzyskać najnowszą wersję, zobacz https://github.com/JakarCo/databasemanager, ale postaram się również aktualizować kod tutaj. Jeśli chcesz zrozumieć moje rozwiązanie, spójrz na kod i przeczytaj moje notatki. Moje notatki są zwykle bardzo pomocne.

  1. skopiuj / wklej kod do nowego pliku o nazwie DatabaseManager. (lub pobierz go z github)
  2. rozszerz DatabaseManageri wdrażaj onCreatei onUpgradetak jak zwykle. Możesz utworzyć wiele podklas tej samej DatabaseManagerklasy, aby mieć różne bazy danych na dysku.
  3. Utwórz instancję swojej podklasy i wywołaj, getDb()aby użyć SQLiteDatabaseklasy.
  4. Zadzwoń close()do każdej podklasy, którą utworzyłeś

Kod do skopiowania / wklejenia :

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;

import java.util.concurrent.ConcurrentHashMap;

/** Extend this class and use it as an SQLiteOpenHelper class
 *
 * DO NOT distribute, sell, or present this code as your own. 
 * for any distributing/selling, or whatever, see the info at the link below
 *
 * Distribution, attribution, legal stuff,
 * See https://github.com/JakarCo/databasemanager
 * 
 * If you ever need help with this code, contact me at [email protected] (or [email protected] )
 * 
 * Do not sell this. but use it as much as you want. There are no implied or express warranties with this code. 
 *
 * This is a simple database manager class which makes threading/synchronization super easy.
 *
 * Extend this class and use it like an SQLiteOpenHelper, but use it as follows:
 *  Instantiate this class once in each thread that uses the database. 
 *  Make sure to call {@link #close()} on every opened instance of this class
 *  If it is closed, then call {@link #open()} before using again.
 * 
 * Call {@link #getDb()} to get an instance of the underlying SQLiteDatabse class (which is synchronized)
 *
 * I also implement this system (well, it's very similar) in my <a href="http://androidslitelibrary.com">Android SQLite Libray</a> at http://androidslitelibrary.com
 * 
 *
 */
abstract public class DatabaseManager {

    /**See SQLiteOpenHelper documentation
    */
    abstract public void onCreate(SQLiteDatabase db);
    /**See SQLiteOpenHelper documentation
     */
    abstract public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion);
    /**Optional.
     * *
     */
    public void onOpen(SQLiteDatabase db){}
    /**Optional.
     * 
     */
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
    /**Optional
     * 
     */
    public void onConfigure(SQLiteDatabase db){}



    /** The SQLiteOpenHelper class is not actually used by your application.
     *
     */
    static private class DBSQLiteOpenHelper extends SQLiteOpenHelper {

        DatabaseManager databaseManager;
        private AtomicInteger counter = new AtomicInteger(0);

        public DBSQLiteOpenHelper(Context context, String name, int version, DatabaseManager databaseManager) {
            super(context, name, null, version);
            this.databaseManager = databaseManager;
        }

        public void addConnection(){
            counter.incrementAndGet();
        }
        public void removeConnection(){
            counter.decrementAndGet();
        }
        public int getCounter() {
            return counter.get();
        }
        @Override
        public void onCreate(SQLiteDatabase db) {
            databaseManager.onCreate(db);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onUpgrade(db, oldVersion, newVersion);
        }

        @Override
        public void onOpen(SQLiteDatabase db) {
            databaseManager.onOpen(db);
        }

        @Override
        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            databaseManager.onDowngrade(db, oldVersion, newVersion);
        }

        @Override
        public void onConfigure(SQLiteDatabase db) {
            databaseManager.onConfigure(db);
        }
    }

    private static final ConcurrentHashMap<String,DBSQLiteOpenHelper> dbMap = new ConcurrentHashMap<String, DBSQLiteOpenHelper>();

    private static final Object lockObject = new Object();


    private DBSQLiteOpenHelper sqLiteOpenHelper;
    private SQLiteDatabase db;
    private Context context;

    /** Instantiate a new DB Helper. 
     * <br> SQLiteOpenHelpers are statically cached so they (and their internally cached SQLiteDatabases) will be reused for concurrency
     *
     * @param context Any {@link android.content.Context} belonging to your package.
     * @param name The database name. This may be anything you like. Adding a file extension is not required and any file extension you would like to use is fine.
     * @param version the database version.
     */
    public DatabaseManager(Context context, String name, int version) {
        String dbPath = context.getApplicationContext().getDatabasePath(name).getAbsolutePath();
        synchronized (lockObject) {
            sqLiteOpenHelper = dbMap.get(dbPath);
            if (sqLiteOpenHelper==null) {
                sqLiteOpenHelper = new DBSQLiteOpenHelper(context, name, version, this);
                dbMap.put(dbPath,sqLiteOpenHelper);
            }
            //SQLiteOpenHelper class caches the SQLiteDatabase, so this will be the same SQLiteDatabase object every time
            db = sqLiteOpenHelper.getWritableDatabase();
        }
        this.context = context.getApplicationContext();
    }
    /**Get the writable SQLiteDatabase
     */
    public SQLiteDatabase getDb(){
        return db;
    }

    /** Check if the underlying SQLiteDatabase is open
     *
     * @return whether the DB is open or not
     */
    public boolean isOpen(){
        return (db!=null&&db.isOpen());
    }


    /** Lowers the DB counter by 1 for any {@link DatabaseManager}s referencing the same DB on disk
     *  <br />If the new counter is 0, then the database will be closed.
     *  <br /><br />This needs to be called before application exit.
     * <br />If the counter is 0, then the underlying SQLiteDatabase is <b>null</b> until another DatabaseManager is instantiated or you call {@link #open()}
     *
     * @return true if the underlying {@link android.database.sqlite.SQLiteDatabase} is closed (counter is 0), and false otherwise (counter > 0)
     */
    public boolean close(){
        sqLiteOpenHelper.removeConnection();
        if (sqLiteOpenHelper.getCounter()==0){
            synchronized (lockObject){
                if (db.inTransaction())db.endTransaction();
                if (db.isOpen())db.close();
                db = null;
            }
            return true;
        }
        return false;
    }
    /** Increments the internal db counter by one and opens the db if needed
    *
    */
    public void open(){
        sqLiteOpenHelper.addConnection();
        if (db==null||!db.isOpen()){
                synchronized (lockObject){
                    db = sqLiteOpenHelper.getWritableDatabase();
                }
        } 
    }
}
Trzcina
źródło
1
Co się stanie, gdy nazwiesz „zamknij”, a następnie spróbujesz ponownie użyć klasy? czy to się zawiesi? czy automatycznie się ponownie zainicjuje, aby móc ponownie korzystać z bazy danych?
deweloper Androida
1
@androiddeveloper, Jeśli zadzwonisz close, musisz zadzwonić openponownie przed użyciem tego samego wystąpienia klasy LUB możesz utworzyć nowe wystąpienie. Ponieważ w closekodzie ustawiłem db=null, nie będziesz mógł użyć wartości zwracanej z getDb(ponieważ byłoby zerowe), więc dostaniesz, NullPointerExceptionjeśli zrobiłbyś coś takiegomyInstance.close(); myInstance.getDb().query(...);
Reed
Dlaczego nie połączyć getDb()i open()do jednej metody?
Alex Burdusel,
@Burdu, połączenie zapewniające dodatkową kontrolę nad licznikiem bazy danych i złym projektem. Jednak zdecydowanie nie jest to najlepszy sposób. Zaktualizuję to za kilka dni.
Reed
@Burdu, właśnie go zaktualizowałem. Możesz pobrać nowy kod stąd . Nie przetestowałem tego, więc daj mi znać, czy powinienem zatwierdzić zmiany.
Reed
11

Baza danych jest bardzo elastyczna z wielowątkowością. Moje aplikacje jednocześnie uderzają w swoje bazy danych z wielu różnych wątków i wszystko w porządku. W niektórych przypadkach mam wiele procesów uderzających jednocześnie w DB i to też działa dobrze.

Twoje zadania asynchroniczne - korzystaj z tego samego połączenia, kiedy możesz, ale jeśli musisz, możesz uzyskać dostęp do bazy danych z różnych zadań.

Brad Hein
źródło
Ponadto, czy masz czytników i pisarzy w różnych połączeniach, czy powinni oni dzielić jedno połączenie? Dzięki.
Gray,
@Gray - poprawnie, powinienem wyraźnie o tym wspomnieć. Jeśli chodzi o połączenia, użyłbym tego samego połączenia w jak największym stopniu, ale ponieważ blokowanie jest obsługiwane na poziomie systemu plików, możesz otworzyć je wiele razy w kodzie, ale użyłbym jednego połączenia tak często, jak to możliwe. Android sqlite DB jest bardzo elastyczny i wybaczający.
Brad Hein,
3
@Gray, chciałem tylko opublikować zaktualizowane informacje dla osób, które mogą korzystać z tej metody. Dokumentacja mówi: Ta metoda nic teraz nie robi. Nie używaj.
Pijusn
3
Znalazłem tę metodę, która zawiodła okropnie, przełączamy się na ContentProvider, aby uzyskać dostęp z wielu aplikacji. Będziemy musieli zastosować pewną współbieżność naszych metod, ale to powinno rozwiązać wszelkie problemy z procesami, które jednocześnie uzyskują dostęp do danych.
JPM,
2
Wiem, że to stare, ale nieprawidłowe. Może się okazać , że dostęp do DB z różnych SQLiteDatabaseobiektów na różnych AsyncTasks / Threads może się udać , ale czasami prowadzi to do błędów, dlatego SQLiteDatabase (linia 1297) używa Locks
Reed
7

Odpowiedź Dmytra działa dobrze w moim przypadku. Myślę, że lepiej jest zadeklarować funkcję jako zsynchronizowaną. przynajmniej w moim przypadku wywołałoby wyjątek zerowy wskaźnik, w przeciwnym razie np. getWritableDatabase nie zostałby jeszcze zwrócony w jednym wątku, a openDatabse wywołał w międzyczasie inny wątek.

public synchronized SQLiteDatabase openDatabase() {
    if(mOpenCounter.incrementAndGet() == 1) {
        // Opening new database
        mDatabase = mDatabaseHelper.getWritableDatabase();
    }
    return mDatabase;
}
gonglong
źródło
mDatabaseHelper.getWritableDatabase (); Nie utworzy to nowego obiektu bazy danych
odległy
5

po kilku godzinach borykania się z tym stwierdziłem, że możesz użyć tylko jednego obiektu pomocnika db na wykonanie db. Na przykład,

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

zgodnie z:

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

tworzenie nowego DBAdaptera za każdym razem, gdy iteruje się pętla, było jedynym sposobem, w jaki mogłem przenieść moje łańcuchy do bazy danych poprzez moją klasę pomocniczą.

dell116
źródło
4

Rozumiem API SQLiteDatabase, ponieważ w przypadku aplikacji wielowątkowej nie możesz sobie pozwolić na posiadanie więcej niż 1 obiektu SQLiteDatabase wskazującego na jedną bazę danych.

Obiekt zdecydowanie może zostać utworzony, ale wstawienia / aktualizacje kończą się niepowodzeniem, jeśli różne wątki / procesy (również) zaczną używać różnych obiektów SQLiteDatabase (np. Jak używamy w JDBC Connection).

Jedynym rozwiązaniem tutaj jest trzymanie się 1 obiektów SQLiteDatabase i za każdym razem, gdy startTransaction () jest używany w więcej niż 1 wątku, Android zarządza blokowaniem różnych wątków i pozwala tylko 1 wątkowi na raz mieć wyłączny dostęp do aktualizacji.

Możesz także wykonać „Odczyty” z bazy danych i użyć tego samego obiektu SQLiteDatabase w innym wątku (podczas gdy inny wątek pisze) i nigdy nie byłoby uszkodzenia bazy danych, tzn. „Odczyt wątku” nie odczytałby danych z bazy danych, dopóki „ write thread ”zatwierdza dane, chociaż oba używają tego samego obiektu SQLiteDatabase.

Różni się to od tego, jak obiekt połączenia znajduje się w JDBC, gdzie jeśli przekażesz (użyjesz tego samego) obiekt połączenia między wątkami odczytu i zapisu, prawdopodobnie wydrukowalibyśmy również nieprzypisane dane.

W mojej aplikacji korporacyjnej próbuję używać kontroli warunkowych, aby wątek interfejsu użytkownika nigdy nie musiał czekać, podczas gdy wątek BG przechowuje obiekt SQLiteDatabase (wyłącznie). Próbuję przewidzieć działania interfejsu użytkownika i odroczyć uruchamianie wątku BG na „x” sekund. Można także utrzymywać PriorityQueue, aby zarządzać rozdawaniem obiektów połączenia SQLiteDatabase, aby wątek interfejsu użytkownika pobierał go jako pierwszy.

Swaroop
źródło
A co wkładasz w PriorityQueue - detektory (które chcą uzyskać obiekt bazy danych) lub zapytania SQL?
Pijusn
Nie korzystałem z kolejki priorytetowej, ale w zasadzie wątki „dzwoniącego”.
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. Nie zawsze tak jest, jeśli zaczniesz „czytać wątek” zaraz po „zapisywać wątek”, możesz nie otrzymać nowo zaktualizowanych danych (wstawionych lub zaktualizowanych w wątku zapisu). Odczyt wątku może odczytać dane przed rozpoczęciem zapisu wątku. Dzieje się tak, ponieważ operacja zapisu początkowo włącza blokadę zastrzeżoną zamiast blokady wyłącznej.
Amit Vikram Singh,
4

Możesz spróbować zastosować nowe podejście architektoniczne ogłoszone na Google I / O 2017.

Zawiera także nową bibliotekę ORM o nazwie Pokój

Zawiera trzy główne komponenty: @Entity, @exe i @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.
}

Userexe.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();
}
Zimbo Rodger
źródło
Nie sugeruję miejsca w bazie danych z wieloma relacjami N-N-N, ponieważ nie radzi sobie dobrze z tym podejściem i musisz napisać dużo kodu, aby znaleźć obejście tych relacji.
AlexPad
3

Mając pewne problemy, myślę, że zrozumiałem, dlaczego się mylę.

Napisałem klasę opakowującą bazę danych, która zawierała funkcję o close()nazwie helper close, której lustro open()nosi nazwę getWriteableDatabase, a następnie przeprowadziłem migrację do ContentProvider. Model dla ContentProvidernie używa, SQLiteDatabase.close()co moim zdaniem jest dużą wskazówką, ponieważ używa kodugetWriteableDatabase W niektórych przypadkach nadal uzyskiwałem bezpośredni dostęp (zapytania sprawdzania poprawności ekranu w głównej części, więc przeprowadziłem migrację do modelu getWriteableDatabase / rawQuery.

Używam singletonu, a w złej dokumentacji jest nieco złowieszczy komentarz

Zamknij dowolny otwarty obiekt bazy danych

(moje pogrubienie).

Miałem więc sporadyczne awarie, w których korzystałem z wątków w tle, aby uzyskać dostęp do bazy danych i działały one w tym samym czasie, co pierwszy plan.

Więc myślę close() wymusza zamknięcie bazy danych niezależnie od innych wątków zawierających referencje - więc close()samo w sobie nie jest po prostu cofaniem dopasowania, getWriteableDatabaseale wymusza zamknięcie dowolnego otwartych żądań. W większości przypadków nie stanowi to problemu, ponieważ kod jest jednowątkowy, ale w przypadkach wielowątkowych zawsze istnieje szansa na otwarcie i zamknięcie synchronizacji.

Po przeczytaniu komentarzy w innym miejscu, które wyjaśniają, że instancja kodu SqLiteDatabaseHelper się liczy, wtedy jedyną potrzebną chwilą zamknięcia jest sytuacja, w której chcesz wykonać kopię zapasową, a także wymusić zamknięcie wszystkich połączeń i wymusić SqLite na zapisz wszystkie zbuforowane rzeczy, które mogą się kręcić - innymi słowy zatrzymaj całą aktywność bazy danych aplikacji, zamknij na wypadek, gdyby Pomocnik stracił ścieżkę, wykonaj dowolną aktywność na poziomie plików (tworzenie kopii zapasowych / przywracanie), a następnie rozpocznij od nowa.

Chociaż wydaje się, że dobrym pomysłem jest próba zamknięcia w kontrolowany sposób, w rzeczywistości Android zastrzega sobie prawo do usunięcia maszyny wirtualnej, więc każde zamknięcie zmniejsza ryzyko, że aktualizacje w pamięci podręcznej nie zostaną zapisane, ale nie można tego zagwarantować, jeśli urządzenie jest zestresowany, a jeśli poprawnie zwolniłeś kursory i odniesienia do baz danych (które nie powinny być elementami statycznymi), pomocnik i tak zamknie bazę danych.

Uważam więc, że podejście jest następujące:

Użyj getWriteableDatabase, aby otworzyć z opakowania singleton. (Użyłem pochodnej klasy aplikacji, aby dostarczyć kontekst aplikacji ze statycznego, aby rozwiązać potrzebę kontekstu).

Nigdy nie dzwonić bezpośrednio.

Nigdy nie przechowuj wynikowej bazy danych w żadnym obiekcie, który nie ma oczywistego zakresu i polegaj na liczeniu referencji, aby wywołać niejawne zamknięcie ().

Jeśli wykonujesz obsługę na poziomie plików, zatrzymaj całą aktywność bazy danych, a następnie wywołaj zamknięcie na wypadek, gdyby istniał niekontrolowany wątek przy założeniu, że zapisujesz prawidłowe transakcje, aby nie działał wątek, a zamknięta baza danych miała przynajmniej odpowiednie transakcje niż potencjalnie kopia częściowej transakcji na poziomie pliku.

Ian Spencer
źródło
0

Wiem, że odpowiedź jest spóźniona, ale najlepszym sposobem na wykonywanie zapytań sqlite w Androidzie jest niestandardowy dostawca treści. W ten sposób interfejs użytkownika jest oddzielony od klasy bazy danych (klasy rozszerzającej klasę SQLiteOpenHelper). Ponadto zapytania są wykonywane w wątku w tle (moduł ładujący kursor).

Theo
źródło