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?
Odpowiedzi:
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:
źródło
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 .
Teraz chcesz zapisać dane w bazie danych w osobnych wątkach.
Otrzymasz następujący komunikat w logcat i jedna ze zmian nie zostanie zapisana.
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 .
Zaktualizowany kod, który zapisuje dane do bazy danych w osobnych wątkach, będzie wyglądał następująco.
Spowoduje to kolejną awarię.
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.
Próbka robocza
Użyj go w następujący sposób.
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.
źródło
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.?initializeInstance()
ma parametr typuSQLiteOpenHelper
, ale w komentarzu wspomniałeś o użyciuDatabaseManager.initializeInstance(getApplicationContext());
. Co się dzieje? Jak to może działać?Thread
lubAsyncTask
do 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.SQLiteDatabase
instancję dla każdego DB na dysku między wątkami i zaimplementuj system liczący, aby śledzić otwarte połączenia.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.
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.
DatabaseManager
. (lub pobierz go z github)DatabaseManager
i wdrażajonCreate
ionUpgrade
tak jak zwykle. Możesz utworzyć wiele podklas tej samejDatabaseManager
klasy, aby mieć różne bazy danych na dysku.getDb()
aby użyćSQLiteDatabase
klasy.close()
do każdej podklasy, którą utworzyłeśKod do skopiowania / wklejenia :
źródło
close
, musisz zadzwonićopen
ponownie przed użyciem tego samego wystąpienia klasy LUB możesz utworzyć nowe wystąpienie. Ponieważ wclose
kodzie ustawiłemdb=null
, nie będziesz mógł użyć wartości zwracanej zgetDb
(ponieważ byłoby zerowe), więc dostaniesz,NullPointerException
jeśli zrobiłbyś coś takiegomyInstance.close(); myInstance.getDb().query(...);
getDb()
iopen()
do jednej metody?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ń.
źródło
SQLiteDatabase
obiektów na różnychAsyncTask
s /Thread
s może się udać , ale czasami prowadzi to do błędów, dlatego SQLiteDatabase (linia 1297) używaLock
sOdpowiedź 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.
źródło
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,
zgodnie z:
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ą.
źródło
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.
źródło
"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.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
Userexe.java
AppDatabase.java
źródło
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 lustroopen()
nosi nazwę getWriteableDatabase, a następnie przeprowadziłem migrację doContentProvider
. Model dlaContentProvider
nie 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
(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ęcclose()
samo w sobie nie jest po prostu cofaniem dopasowania,getWriteableDatabase
ale 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.
źródło
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).
źródło