Apa praktik terbaik untuk SQLite di Android?

694

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?

Vidar Vestnes
sumber
10
Apa pun yang Anda lakukan, ingat untuk membersihkan input Anda jika penyedia konten Anda (atau antarmuka SQLite) menghadap publik!
Kristopher Micinski
37
Anda pasti TIDAK boleh melakukan akses db dari utas UI, saya dapat memberi tahu Anda sebanyak itu.
Edward Falk
@ EdwardFalk Kenapa tidak? Tentunya ada kasus penggunaan yang valid untuk melakukan ini?
Michael
4
Jika Anda melakukan I / O, akses jaringan, dll. Dari utas UI, seluruh perangkat membeku sampai operasi selesai. Jika selesai dalam 1/20 detik, maka baik-baik saja. Jika itu membutuhkan waktu lebih lama, Anda memiliki pengalaman pengguna yang buruk.
Edward Falk

Jawaban:

631

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:

Kevin Galligan
sumber
2
Selain itu, dukungan Android Ormlite dapat ditemukan di ormlite.sourceforge.net/sqlite_java_android_orm.html . Ada contoh proyek, dokumentasi, dan toples.
Abu
1
Sebentar lagi. Kode ormlite memiliki kelas pembantu yang dapat digunakan untuk mengelola instance dbhelper. Anda dapat menggunakan barang-barang ormlite, tetapi tidak wajib. Anda dapat menggunakan kelas pembantu hanya untuk melakukan manajemen koneksi.
Kevin Galligan
31
Kāgii, terima kasih atas penjelasan terperincinya. Bisakah Anda mengklarifikasi satu hal - saya mengerti Anda harus memiliki SATU pembantu, tetapi haruskah Anda juga hanya memiliki satu koneksi (yaitu satu objek SqliteDatabase)? Dengan kata lain, seberapa sering Anda harus memanggil getWritableDatabase? Dan yang sama pentingnya ketika Anda menelepon close ()?
Artem
3
Saya memperbarui kode. Dokumen asli hilang ketika saya mengganti host blog, tetapi saya menambahkan beberapa kode contoh yang dipersingkat yang akan menunjukkan masalah tersebut. Juga, bagaimana Anda mengelola koneksi tunggal? Saya punya solusi yang jauh lebih rumit pada awalnya, tetapi sejak itu saya mengubahnya. Coba lihat di sini: touchlab.co/uncategorized/single-sqlite-connection
Kevin
apakah ada kebutuhan untuk membuang koneksi dan ke mana melakukannya?
tugce
188

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 .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Sekarang Anda ingin menulis data ke basis data di utas terpisah.

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

Anda akan mendapatkan pesan berikut di logcat Anda dan salah satu perubahan Anda tidak akan ditulis.

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

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 .

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

}

Kode yang diperbarui yang menulis data ke basis data dalam utas terpisah akan terlihat seperti ini.

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

Ini akan membuat Anda crash lagi.

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

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.

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

Sampel kerja

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

        }
    }

}

Gunakan sebagai berikut.

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

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.

Dmytro Danylyk
sumber
10
Saya salah satu dari orang-orang yang menyarankan Anda untuk tidak pernah menutup db. Anda hanya mendapatkan kesalahan "Kebocoran ditemukan" jika Anda membuka db, jangan tutup, lalu coba buka lagi. Jika Anda hanya menggunakan satu pembantu terbuka, dan tidak pernah menutup db, Anda tidak mendapatkan kesalahan itu. Jika Anda menemukan sebaliknya, beri tahu saya (dengan kode). Saya memiliki posting yang lebih panjang tentang suatu tempat, tetapi tidak dapat menemukannya. Pertanyaan dijawab oleh commonsware di sini, siapa yang mengalahkan kami di departemen SO points: stackoverflow.com/questions/7211941/…
Kevin Galligan
4
Lebih banyak pemikiran. # 1, saya akan membuat pembantu di dalam manajer Anda. Meminta masalah untuk memilikinya di luar. Dev baru mungkin memanggil pembantu secara langsung untuk beberapa alasan gila. Juga, jika Anda memerlukan metode init, berikan pengecualian jika instance sudah ada. Aplikasi multi-db jelas akan gagal apa adanya. # 2, mengapa bidang mDatabase? Ini tersedia dari helper. # 3, sebagai langkah pertama Anda menuju "tidak pernah tutup", apa yang terjadi pada db Anda ketika aplikasi Anda mogok dan tidak "ditutup"? Petunjuk, tidak ada. Tidak masalah, karena SQLite sangat stabil. Itu adalah langkah 1 dalam mencari tahu mengapa Anda tidak perlu menutupnya.
Kevin Galligan
1
Adakah alasan mengapa Anda menggunakan metode inisialisasi publik untuk menelepon sebelum mendapatkan instance? Mengapa tidak memiliki konstruktor pribadi yang disebut 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?
ChiefTwoPencils
1
initializeInstance()memiliki parameter tipe SQLiteOpenHelper, tetapi dalam komentar Anda yang Anda sebutkan untuk digunakan DatabaseManager.initializeInstance(getApplicationContext());. Apa yang sedang terjadi? Bagaimana ini bisa berhasil?
faizal
2
@DmytroDanylyk "DatabaseManager adalah singleton aman thread, sehingga dapat digunakan di mana saja" itu tidak benar dalam menanggapi pertanyaan virsir. Objek tidak berbagi proses lintas. DatabaseManager Anda tidak akan memiliki status dalam proses yang berbeda seperti pada adaptor sinkronisasi (android: process = ": sync")
whizzle
17
  • Gunakan a Threadatau AsyncTaskuntuk 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.
  • Bagikan satu SQLiteDatabasecontoh untuk setiap DB pada disk di antara utas dan menerapkan sistem penghitungan untuk melacak koneksi terbuka.

Adakah praktik terbaik untuk skenario ini?

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.

Solusi saya:

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.

  1. salin / tempel kode ke file baru bernama DatabaseManager. (atau unduh dari github)
  2. memperluas DatabaseManagerdan mengimplementasikan onCreatedan onUpgradeseperti yang biasa Anda lakukan. Anda dapat membuat beberapa subclass dari satu DatabaseManagerkelas untuk memiliki basis data yang berbeda pada disk.
  3. Instantiate subclass Anda dan panggilan getDb()untuk menggunakan SQLiteDatabasekelas.
  4. Panggil close()untuk setiap subkelas yang Anda instantiated

Kode untuk menyalin / menempel :

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();
                }
        } 
    }
}
Reed
sumber
1
Apa yang terjadi ketika Anda memanggil "tutup" dan kemudian mencoba menggunakan kembali kelas? apakah itu akan crash? atau apakah akan secara otomatis menginisialisasi ulang untuk dapat menggunakan DB lagi?
pengembang android
1
@androiddeveloper, Jika Anda menelepon close, Anda harus menelepon openlagi sebelum menggunakan instance kelas yang sama ATAU Anda dapat membuat instance baru. Karena dalam closekode, saya atur db=null, Anda tidak akan dapat menggunakan nilai pengembalian dari getDb(karena itu akan menjadi nol), jadi Anda akan mendapatkan NullPointerExceptionjika Anda melakukan sesuatu sepertimyInstance.close(); myInstance.getDb().query(...);
Reed
Mengapa tidak menggabungkan getDb()dan open()menjadi satu metode?
Alex Burdusel
@ Burdu, Campuran memberikan kontrol ekstra atas penghitung database dan desain yang buruk. Ini jelas bukan cara terbaik untuk melakukannya. Saya akan memperbaruinya dalam beberapa hari.
Reed
@ Burdu, saya baru saja memperbaruinya. Anda bisa mendapatkan kode baru dari sini . Saya belum mengujinya, jadi tolong beri tahu saya jika saya harus melakukan perubahan.
Reed
11

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.

Brad Hein
sumber
Juga, apakah Anda memiliki pembaca dan penulis dalam koneksi yang berbeda atau haruskah mereka berbagi satu koneksi? Terima kasih.
Gray
@ Grey - benar, saya seharusnya menyebutkannya secara eksplisit. Sejauh koneksi, saya akan menggunakan koneksi yang sama sebanyak mungkin, tetapi karena penguncian ditangani pada tingkat filesystem, Anda dapat membukanya beberapa kali dalam kode, tetapi saya akan menggunakan koneksi tunggal sebanyak mungkin. Android sqlite DB sangat fleksibel dan pemaaf.
Brad Hein
3
@ Abu-abu, hanya ingin memposting informasi yang diperbarui untuk orang-orang yang mungkin menggunakan metode ini. Dokumentasi mengatakan: Metode ini sekarang tidak melakukan apa-apa. Jangan gunakan.
Pijusn
3
Saya menemukan metode ini gagal total, kami beralih ke ContentProvider untuk mengakses dari beberapa aplikasi. Kami harus melakukan beberapa konkurensi pada metode kami, tetapi itu harus memperbaiki masalah dengan proses semua mengakses data pada saat yang sama.
JPM
2
Saya tahu ini sudah tua, tetapi itu tidak benar. Ini mungkin bekerja baik-baik saja untuk mengakses DB dari berbagai SQLiteDatabaseobjek pada berbeda AsyncTasks / Threads, tetapi kadang-kadang akan mengakibatkan kesalahan yang mengapa SQLiteDatabase (baris 1297) menggunakan Locks
Reed
7

Jawaban 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.

public synchronized SQLiteDatabase openDatabase() {
    if(mOpenCounter.incrementAndGet() == 1) {
        // Opening new database
        mDatabase = mDatabaseHelper.getWritableDatabase();
    }
    return mDatabase;
}
Gonglong
sumber
mDatabaseHelper.getWritableDatabase (); Ini tidak akan membuat objek database baru
luar
5

setelah berjuang dengan ini selama beberapa jam, saya telah menemukan bahwa Anda hanya dapat menggunakan satu objek pembantu db per eksekusi db. Sebagai contoh,

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

sebagaimana seharusnya:

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

membuat DBAdapter baru setiap kali loop berulang adalah satu-satunya cara saya bisa mendapatkan string saya ke dalam database melalui kelas pembantu saya.

dell116
sumber
4

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.

Swaroop
sumber
Dan apa yang Anda masukkan ke dalam PriorityQueue - pendengar (yang ingin mendapatkan objek basis data) atau query SQL?
Pijusn
Saya belum menggunakan pendekatan antrian prioritas, tetapi pada dasarnya utas "penelepon".
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,. 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.
Amit Vikram Singh
4

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

@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();
}
Zimbo Rodger
sumber
Saya tidak menyarankan Kamar ke Database dengan beberapa hubungan N-ke-N, karena tidak menangani pendekatan ini dengan baik dan Anda harus menulis banyak kode untuk menemukan solusi untuk hubungan ini.
AlexPad
3

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 cermin open()yang disebut getWriteableDatabase dan kemudian bermigrasi ke a ContentProvider. Model untuk ContentProvidertidak menggunakan SQLiteDatabase.close()yang saya pikir adalah petunjuk besar karena kode tidak digunakan getWriteableDatabaseDalam 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

Tutup setiap objek database open

(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 - sehingga close()itu sendiri tidak hanya mengurai pencocokan getWriteableDatabasetapi 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.

Ian Spencer
sumber
0

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).

Theo
sumber