Apa yang akan dianggap praktik terbaik saat menjalankan kueri pada basis data SQLite dalam aplikasi Android?
Apakah aman menjalankan sisipan, menghapus, dan memilih kueri dari doInBackground AsyncTask? Atau haruskah saya menggunakan Thread UI? Saya menduga bahwa kueri basis data bisa "berat" dan tidak boleh menggunakan utas UI karena dapat mengunci aplikasi - menghasilkan Aplikasi Tidak Menanggapi (ANR).
Jika saya memiliki beberapa AsyncTasks, haruskah mereka berbagi koneksi atau haruskah mereka membuka koneksi masing-masing?
Adakah praktik terbaik untuk skenario ini?
Jawaban:
Sisipan, pembaruan, penghapusan, dan pembacaan pada umumnya OK dari banyak utas, tetapi jawaban Brad tidak benar. Anda harus berhati-hati dengan cara Anda membuat koneksi dan menggunakannya. Ada situasi di mana panggilan pembaruan Anda akan gagal, bahkan jika database Anda tidak rusak.
Jawaban dasarnya.
Objek SqliteOpenHelper berpegang pada satu koneksi database. Tampaknya menawarkan Anda koneksi baca dan tulis, tetapi sebenarnya tidak. Panggil read-only, dan Anda akan mendapatkan koneksi database tulis apa pun.
Jadi, satu contoh pembantu, satu koneksi db. Bahkan jika Anda menggunakannya dari beberapa utas, satu koneksi pada satu waktu. Objek SqliteDatabase menggunakan kunci java untuk menjaga akses serial. Jadi, jika 100 utas memiliki instance db, panggilan ke database aktual pada disk adalah serial.
Jadi, satu helper, satu koneksi db, yang diserialisasi dalam kode java. Satu utas, 1000 utas, jika Anda menggunakan satu contoh pembantu yang dibagikan di antara mereka, semua kode akses db Anda bersifat serial. Dan hidup itu baik (ish).
Jika Anda mencoba menulis ke database dari koneksi berbeda yang sebenarnya pada saat yang sama, orang akan gagal. Itu tidak akan menunggu sampai yang pertama selesai dan kemudian menulis. Itu tidak akan menulis perubahan Anda. Lebih buruk lagi, jika Anda tidak memanggil versi yang benar dari insert / update pada SQLiteDatabase, Anda tidak akan mendapatkan pengecualian. Anda hanya akan mendapatkan pesan di LogCat Anda, dan itu saja.
Jadi, banyak utas? Gunakan satu pembantu. Titik. Jika Anda TAHU hanya satu utas yang akan menulis, Anda DAPAT menggunakan beberapa koneksi, dan bacaan Anda akan lebih cepat, tetapi pembeli berhati-hatilah. Saya belum menguji sebanyak itu.
Berikut adalah posting blog dengan jauh lebih detail dan contoh aplikasi.
Gray dan saya sebenarnya sedang membungkus alat ORM, berdasarkan Ormlite-nya, yang bekerja secara native dengan implementasi basis data Android, dan mengikuti struktur pembuatan / panggilan aman yang saya jelaskan di posting blog. Itu harus segera keluar. Lihatlah.
Sementara itu, ada posting blog tindak lanjut:
Juga checkout garpu dengan 2point0 dari contoh penguncian yang disebutkan sebelumnya:
sumber
Akses Database Bersamaan
Artikel yang sama di blog saya (saya lebih suka memformat)
Saya menulis artikel kecil yang menjelaskan cara membuat akses ke utas basis data android Anda aman.
Dengan asumsi Anda memiliki SQLiteOpenHelper Anda sendiri .
Sekarang Anda ingin menulis data ke basis data di utas terpisah.
Anda akan mendapatkan pesan berikut di logcat Anda dan salah satu perubahan Anda tidak akan ditulis.
Ini terjadi karena setiap kali Anda membuat objek SQLiteOpenHelper baru, Anda sebenarnya membuat koneksi database baru. Jika Anda mencoba menulis ke database dari koneksi berbeda yang sebenarnya pada saat yang sama, orang akan gagal. (dari jawaban di atas)
Untuk menggunakan basis data dengan banyak utas, kami perlu memastikan bahwa kami menggunakan satu koneksi basis data.
Mari kita membuat Database Manager kelas tunggal yang akan menahan dan mengembalikan objek SQLiteOpenHelper tunggal .
Kode yang diperbarui yang menulis data ke basis data dalam utas terpisah akan terlihat seperti ini.
Ini akan membuat Anda crash lagi.
Karena kita hanya menggunakan satu koneksi basis data, metode getDatabase () mengembalikan instance objek SQLiteDatabase yang sama untuk Thread1 dan Thread2 . Apa yang terjadi, Thread1 dapat menutup basis data, sementara Thread2 masih menggunakannya. Itu sebabnya kami mengalami crash IllegalStateException .
Kita perlu memastikan tidak ada yang menggunakan database dan baru kemudian menutupnya. Beberapa orang di stackoveflow disarankan untuk tidak pernah menutup SQLiteDatabase Anda . Ini akan menghasilkan pesan logcat berikut.
Sampel kerja
Gunakan sebagai berikut.
Setiap kali Anda membutuhkan database, Anda harus memanggil metode openDatabase () dari kelas DatabaseManager . Di dalam metode ini, kami memiliki penghitung, yang menunjukkan berapa kali database dibuka. Jika sama dengan satu, itu berarti kita perlu membuat koneksi database baru, jika tidak, koneksi database sudah dibuat.
Hal yang sama terjadi pada metode closeDatabase () . Setiap kali kita memanggil metode ini, penghitung berkurang, setiap kali menjadi nol, kita menutup koneksi basis data.
Sekarang Anda harus dapat menggunakan database Anda dan pastikan itu aman.
sumber
if(instance==null)
? Anda tidak punya pilihan selain memanggil inisialisasi setiap kali; bagaimana lagi yang Anda tahu jika sudah diinisialisasi atau tidak dalam aplikasi lain dll?initializeInstance()
memiliki parameter tipeSQLiteOpenHelper
, tetapi dalam komentar Anda yang Anda sebutkan untuk digunakanDatabaseManager.initializeInstance(getApplicationContext());
. Apa yang sedang terjadi? Bagaimana ini bisa berhasil?Thread
atauAsyncTask
untuk operasi jangka panjang (50 ms +). Uji aplikasi Anda untuk melihat di mana itu. Sebagian besar operasi (mungkin) tidak memerlukan utas, karena sebagian besar operasi (mungkin) hanya melibatkan beberapa baris. Gunakan utas untuk operasi massal.SQLiteDatabase
contoh untuk setiap DB pada disk di antara utas dan menerapkan sistem penghitungan untuk melacak koneksi terbuka.Bagikan bidang statis antara semua kelas Anda. Saya biasa menjaga seorang singleton untuk itu dan hal-hal lain yang perlu dibagikan. Skema penghitungan (umumnya menggunakan AtomicInteger) juga harus digunakan untuk memastikan Anda tidak pernah menutup database lebih awal atau membiarkannya terbuka.
Untuk versi terbaru, lihat https://github.com/JakarCo/databasemanager tetapi saya juga akan mencoba memperbarui kode di sini. Jika Anda ingin memahami solusi saya, lihat kodenya dan baca catatan saya. Catatan saya biasanya cukup membantu.
DatabaseManager
. (atau unduh dari github)DatabaseManager
dan mengimplementasikanonCreate
danonUpgrade
seperti yang biasa Anda lakukan. Anda dapat membuat beberapa subclass dari satuDatabaseManager
kelas untuk memiliki basis data yang berbeda pada disk.getDb()
untuk menggunakanSQLiteDatabase
kelas.close()
untuk setiap subkelas yang Anda instantiatedKode untuk menyalin / menempel :
sumber
close
, Anda harus meneleponopen
lagi sebelum menggunakan instance kelas yang sama ATAU Anda dapat membuat instance baru. Karena dalamclose
kode, saya aturdb=null
, Anda tidak akan dapat menggunakan nilai pengembalian darigetDb
(karena itu akan menjadi nol), jadi Anda akan mendapatkanNullPointerException
jika Anda melakukan sesuatu sepertimyInstance.close(); myInstance.getDb().query(...);
getDb()
danopen()
menjadi satu metode?Basis data sangat fleksibel dengan multi-threading. Aplikasi saya menekan DB mereka dari berbagai utas secara bersamaan dan tidak apa-apa. Dalam beberapa kasus saya memiliki beberapa proses yang memukul DB secara bersamaan dan itu berfungsi dengan baik juga.
Tugas async Anda - gunakan koneksi yang sama ketika Anda bisa, tetapi jika harus, OK untuk mengakses DB dari tugas yang berbeda.
sumber
SQLiteDatabase
objek pada berbedaAsyncTask
s /Thread
s, tetapi kadang-kadang akan mengakibatkan kesalahan yang mengapa SQLiteDatabase (baris 1297) menggunakanLock
sJawaban Dmytro bekerja dengan baik untuk kasus saya. Saya pikir lebih baik untuk mendeklarasikan fungsi sebagai disinkronkan. setidaknya untuk kasus saya, itu akan meminta pengecualian null pointer sebaliknya, misalnya getWritableDatabase belum dikembalikan dalam satu utas dan openDatabse disebut di utas lain sementara itu.
sumber
setelah berjuang dengan ini selama beberapa jam, saya telah menemukan bahwa Anda hanya dapat menggunakan satu objek pembantu db per eksekusi db. Sebagai contoh,
sebagaimana seharusnya:
membuat DBAdapter baru setiap kali loop berulang adalah satu-satunya cara saya bisa mendapatkan string saya ke dalam database melalui kelas pembantu saya.
sumber
Pemahaman saya tentang SQLiteDatabase APIs adalah jika Anda memiliki aplikasi multi-threaded, Anda tidak dapat memiliki lebih dari 1 objek SQLiteDatabase yang menunjuk ke satu database.
Objek pasti dapat dibuat tetapi sisipan / pembaruan gagal jika utas / proses yang berbeda (juga) mulai menggunakan objek SQLiteDatabase yang berbeda (seperti bagaimana kita menggunakan dalam Koneksi JDBC).
Satu-satunya solusi di sini adalah tetap menggunakan 1 objek SQLiteDatabase dan setiap kali startTransaction () digunakan di lebih dari 1 utas, Android mengelola penguncian di berbagai utas dan memungkinkan hanya 1 utas sekaligus untuk memiliki akses pembaruan eksklusif.
Anda juga dapat melakukan "Membaca" dari database dan menggunakan objek SQLiteDatabase yang sama di utas yang berbeda (sementara utas lain menulis) dan tidak akan pernah ada kerusakan basis data yaitu "baca utas" tidak akan membaca data dari database sampai " tulis utas "melakukan data meskipun keduanya menggunakan objek SQLiteDatabase yang sama.
Ini berbeda dari bagaimana objek koneksi berada di JDBC di mana jika Anda menyebarkan (menggunakan yang sama) objek koneksi antara utas baca dan tulis maka kami kemungkinan akan mencetak data yang juga tidak dikomit.
Dalam aplikasi perusahaan saya, saya mencoba menggunakan pemeriksaan bersyarat agar UI Thread tidak perlu menunggu, sementara BG thread memegang objek SQLiteDatabase (secara eksklusif). Saya mencoba memprediksi Tindakan UI dan menunda utas BG agar berjalan selama 'x' detik. Anda juga dapat mempertahankan PriorityQueue untuk mengelola membagikan objek Koneksi SQLiteDatabase sehingga Thread UI mendapatkannya terlebih dahulu.
sumber
"read thread" wouldn't read the data from the database till the "write thread" commits the data although both use the same SQLiteDatabase object
,. Ini tidak selalu benar, jika Anda mulai "membaca utas" tepat setelah "tulis utas" Anda mungkin tidak mendapatkan data yang baru diperbarui (dimasukkan atau dimutakhirkan dalam utas tulis). Baca utas mungkin membaca data sebelum memulai utas tulis. Ini terjadi karena operasi penulisan pada awalnya mengaktifkan kunci yang dilindungi bukan kunci eksklusif.Anda dapat mencoba menerapkan pendekatan arsitektur baru yang diumumkan di Google I / O 2017.
Ini juga termasuk perpustakaan ORM baru yang disebut Room
Ini berisi tiga komponen utama: @ Entity, @POR dan @ Database
User.java
UserDao.java
AppDatabase.java
sumber
Setelah mengalami beberapa masalah, saya pikir saya telah mengerti mengapa saya salah.
Saya telah menulis kelas pembungkus basis data yang termasuk
close()
yang disebut pembantu dekat sebagai cerminopen()
yang disebut getWriteableDatabase dan kemudian bermigrasi ke aContentProvider
. Model untukContentProvider
tidak menggunakanSQLiteDatabase.close()
yang saya pikir adalah petunjuk besar karena kode tidak digunakangetWriteableDatabase
Dalam beberapa kasus saya masih melakukan akses langsung (permintaan validasi layar di utama jadi saya bermigrasi ke model getWriteableDatabase / rawQuery.Saya menggunakan singleton dan ada komentar yang agak tidak menyenangkan dalam dokumentasi tutup
(huruf tebal saya).
Jadi saya mengalami crash terputus-putus di mana saya menggunakan utas latar untuk mengakses database dan mereka berjalan pada saat yang sama dengan latar depan.
Jadi saya pikir
close()
kekuatan database untuk menutup terlepas dari benang lain memegang referensi - sehinggaclose()
itu sendiri tidak hanya mengurai pencocokangetWriteableDatabase
tapi kekuatan menutup setiap permintaan terbuka. Sebagian besar waktu ini bukan masalah karena kodenya adalah threading tunggal, tetapi dalam kasus multi-utas selalu ada peluang untuk membuka dan menutup sinkronisasi.Setelah membaca komentar di tempat lain yang menjelaskan bahwa instance kode SqLiteDatabaseHelper diperhitungkan, maka satu-satunya saat Anda ingin menutup adalah di mana Anda ingin situasi di mana Anda ingin melakukan salinan cadangan, dan Anda ingin memaksa semua koneksi ditutup dan memaksa SqLite untuk hapus semua barang yang di-cache yang mungkin berkeliaran - dengan kata lain hentikan semua aktivitas basis data aplikasi, tutup kalau-kalau Pembantu kehilangan jejak, lakukan aktivitas tingkat file apa saja (cadangan / pengembalian) kemudian mulai dari awal lagi.
Meskipun kedengarannya seperti ide yang baik untuk mencoba dan menutup dengan cara yang terkontrol, kenyataannya adalah bahwa Android berhak untuk membuang VM Anda sehingga setiap penutupan mengurangi risiko pembaruan yang di-cache tidak ditulis, tetapi tidak dapat dijamin jika perangkat ditekankan, dan jika Anda telah dengan benar membebaskan kursor dan referensi Anda ke basis data (yang seharusnya bukan anggota statis), maka helper akan tetap menutup database.
Jadi pendapat saya adalah bahwa pendekatannya adalah:
Gunakan getWriteableDatabase untuk membuka dari bungkus tunggal. (Saya menggunakan kelas aplikasi turunan untuk menyediakan konteks aplikasi dari statis untuk menyelesaikan kebutuhan akan konteks).
Jangan pernah langsung menelepon tutup.
Jangan pernah menyimpan database yang dihasilkan di objek apa pun yang tidak memiliki cakupan yang jelas dan mengandalkan penghitungan referensi untuk memicu penutupan implisit ().
Jika melakukan penanganan level file, hentikan semua aktivitas basis data, lalu panggil tutup jika-kalau ada utas pelarian dengan asumsi bahwa Anda menulis transaksi yang tepat sehingga utas pelarian akan gagal dan database yang ditutup setidaknya akan memiliki transaksi yang tepat. daripada berpotensi salinan tingkat file transaksi parsial.
sumber
Saya tahu bahwa responsnya terlambat, tetapi cara terbaik untuk menjalankan kueri sqlite di android adalah melalui penyedia konten khusus. Dengan cara itu UI dipisahkan dengan kelas database (kelas yang memperluas kelas SQLiteOpenHelper). Kueri juga dieksekusi di utas latar belakang (Cursor Loader).
sumber