شرح - sqlite in android studio




ما أفضل الممارسات لـ SQLite على نظام Android؟ (7)

الوصول المتزامن لقاعدة البيانات

المادة نفسها على مدونتي (أنا أحب تنسيق أكثر)

كتبت المقالة الصغيرة التي تصف كيفية جعل الوصول إلى مؤشر ترابط قاعدة بيانات الروبوت الخاص بك آمنة.

على افتراض لديك SQLiteOpenHelper الخاصة بك.

public class DatabaseHelper extends SQLiteOpenHelper { ... }

الآن تريد كتابة البيانات إلى قاعدة البيانات في سلاسل منفصلة.

 // Thread 1
 Context context = getApplicationContext();
 DatabaseHelper helper = new DatabaseHelper(context);
 SQLiteDatabase database = helper.getWritableDatabase();
 database.insert(…);
 database.close();

 // Thread 2
 Context context = getApplicationContext();
 DatabaseHelper helper = new DatabaseHelper(context);
 SQLiteDatabase database = helper.getWritableDatabase();
 database.insert(…);
 database.close();

ستتلقى الرسالة التالية في logcat ولن تتم كتابة أحد تغييراتك.

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

يحدث هذا لأنه في كل مرة تقوم فيها بإنشاء كائن SQLiteOpenHelper جديد تقوم بالفعل بإنشاء اتصال قاعدة بيانات جديدة. إذا حاولت الكتابة إلى قاعدة البيانات من الاتصالات الفعلية الفعلية في نفس الوقت ، فسوف يفشل المرء. (من الجواب أعلاه)

لاستخدام قاعدة البيانات ذات السلاسل المتعددة ، نحتاج إلى التأكد من أننا نستخدم اتصال قاعدة بيانات واحد.

دعونا نجعل مدير قاعدة البيانات لفئة singleton الذي سيعقد ويعود كائن SQLiteOpenHelper واحد.

public class DatabaseManager {

    private static DatabaseManager instance;
    private static SQLiteOpenHelper mDatabaseHelper;

    public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
        if (instance == null) {
            instance = new DatabaseManager();
            mDatabaseHelper = helper;
        }
    }

    public static synchronized DatabaseManager getInstance() {
        if (instance == null) {
            throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
                    " is not initialized, call initialize(..) method first.");
        }

        return instance;
    }

    public SQLiteDatabase getDatabase() {
        return new mDatabaseHelper.getWritableDatabase();
    }

}

سوف تظهر التعليمات البرمجية المحدثة التي تكتب البيانات إلى قاعدة البيانات في سلاسل عمليات منفصلة مثل هذا.

 // In your application class
 DatabaseManager.initializeInstance(new MySQLiteOpenHelper());
 // Thread 1
 DatabaseManager manager = DatabaseManager.getInstance();
 SQLiteDatabase database = manager.getDatabase()
 database.insert(…);
 database.close();

 // Thread 2
 DatabaseManager manager = DatabaseManager.getInstance();
 SQLiteDatabase database = manager.getDatabase()
 database.insert(…);
 database.close();

هذا سيجلب لك تحطم آخر.

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

بما أننا نستخدم اتصال قاعدة بيانات واحد فقط ، ترجع getDatabase () أسلوب المثيل نفس الكائن SQLiteDatabase لـ Thread1 و Thread2 . ما يحدث ، قد إغلاق Thread1 قاعدة البيانات بينما لا يزال Thread2 استخدامه. لهذا السبب لدينا تعطل IllegalStateException .

نحتاج إلى التأكد من عدم استخدام أحد قاعدة البيانات ثم إغلاقها فقط. أوصى بعض الناس على stackoveflow أبدا لإغلاق SQLiteDatabase الخاص بك. لا يبدو غبي فقط ولكن أيضا شرف لك مع رسالة logcat التالية.

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

عينة العمل

public class DatabaseManager {

    private int mOpenCounter;

    private static DatabaseManager instance;
    private static SQLiteOpenHelper mDatabaseHelper;
    private SQLiteDatabase mDatabase;

    public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
        if (instance == null) {
            instance = new DatabaseManager();
            mDatabaseHelper = helper;
        }
    }

    public static synchronized DatabaseManager getInstance() {
        if (instance == null) {
            throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
                    " is not initialized, call initializeInstance(..) method first.");
        }

        return instance;
    }

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

    public synchronized void closeDatabase() {
        mOpenCounter--;
        if(mOpenCounter == 0) {
            // Closing database
            mDatabase.close();

        }
    }

}

استخدمه على النحو التالي.

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

في كل مرة تحتاج فيها إلى قاعدة بيانات ، يجب استدعاء طريقة openDatabase () الخاصة بفئة DatabaseManager . داخل هذه الطريقة ، لدينا عداد يشير إلى عدد مرات فتح قاعدة البيانات. إذا كان يساوي واحد ، فهذا يعني أننا بحاجة إلى إنشاء اتصال قاعدة بيانات جديد ، إذا لم يكن كذلك ، يتم إنشاء اتصال قاعدة البيانات بالفعل.

يحدث نفس الشيء في أسلوب closeDatabase () . في كل مرة نسميها هذه الطريقة ، يتم تقليل العداد ، عندما يذهب إلى الصفر ، نقوم بإغلاق اتصال قاعدة البيانات.

الآن يجب أن تكون قادراً على استخدام قاعدة البيانات الخاصة بك والتأكد من أنه مؤشر ترابط آمن.

ما الذي يعتبر أفضل الممارسات عند تنفيذ الاستعلامات على قاعدة بيانات SQLite داخل تطبيق Android؟

هل من الآمن تشغيل إدراجات وحذف وتحديد استعلامات من doInBackground في AsyncTask؟ أو يجب علي استخدام مؤشر ترابط واجهة المستخدم؟ أفترض أن طلبات البحث في قاعدة البيانات يمكن أن تكون "ثقيلة" ويجب ألا تستخدم مؤشر ترابط واجهة المستخدم حيث يمكنها قفل التطبيق - مما يؤدي إلى عدم استجابة تطبيق (ANR).

إذا كان لدي العديد من AsyncTasks ، فهل يجب أن يتشاركوا في اتصال أو يجب عليهم فتح اتصال؟

هل هناك أفضل الممارسات لهذه السيناريوهات؟


أعرف أن الرد متأخر ، ولكن أفضل طريقة لتنفيذ استعلامات SQLite في Android هي من خلال مقدم محتوى مخصص. بهذه الطريقة يتم فصل UI مع فئة قاعدة البيانات (الفئة التي تمتد فئة SQLiteOpenHelper). كما يتم تنفيذ الاستعلامات في مؤشر ترابط الخلفية (المؤشر Loader).


بعد أن واجهت بعض المشاكل ، أعتقد أنني فهمت لماذا أخطأت.

كنت قد كتبت فئة مجمّع قاعدة البيانات التي تضمنت close() والتي تسمى المساعد كمرآة open() والتي تسمى getWriteableDatabase ثم تم ترحيلها إلى ContentProvider . لا يستخدم SQLiteDatabase.close() الذي أعتقد أنه عبارة عن فكرة كبيرة حيث أن الشفرة لا تستخدم getWriteableDatabase في بعض الحالات كنت لا أزال أقوم بالوصول المباشر (استعلامات التحقق من الشاشة بشكل رئيسي لذا قمت بالترحيل إلى نموذج getWriteableDatabase / rawQuery .

أنا استخدم singleton وهناك تعليق مشؤوم قليلاً في وثائق وثيقة

قم بإغلاق أي كائن قاعدة البيانات المفتوحة

(صبغتي).

لذلك كان لدي حوادث متقطعة حيث أستخدم خيوط خلفية للوصول إلى قاعدة البيانات وتشغيلها في نفس وقت المقدمة.

لذلك أعتقد أن close() يفرض إغلاق قاعدة البيانات بغض النظر عن أي مؤشرات أخرى تحمل المراجع - حتى close() نفسها ليست ببساطة التراجع عن getWriteableDatabase المطابقة ولكن القوة إغلاق أي طلبات مفتوحة. في معظم الأوقات ، لا يمثل هذا مشكلة لأن الشفرة هي واحدة من مؤشرات الترابط ، ولكن في حالات التعدد المترابط ، هناك دائمًا فرصة لفتح وإغلاق المزامنة.

بعد قراءة التعليقات في مكان آخر يشرح أنه يتم حساب مثيل التعليمة البرمجية SqLiteDatabaseHelper ، فإن الوقت الوحيد الذي تريد فيه الإغلاق هو المكان الذي تريد فيه الوضع الذي تريد إجراء نسخة احتياطية منه ، وتريد فرض إغلاق جميع الاتصالات وإجبار SqLite على قم بكتابة أية أشياء مخبأة في الذاكرة المخبأة قد تكون تتسوق - وبمعنى آخر ، قم بإيقاف كل نشاط قاعدة بيانات التطبيق ، أغلق فقط في حالة فقد المسار ، أو القيام بأي نشاط على مستوى الملف (النسخ الاحتياطي / الاستعادة) ثم البدء من جديد.

على الرغم من أن الأمر يبدو وكأنه فكرة جيدة لمحاولة الإغلاق بطريقة محكومة ، إلا أن الواقع هو أن Android تحتفظ بالحق في سلة المهملات الخاصة بك VM لذا فإن أي إغلاق يؤدي إلى تقليل خطر عدم تحديث التحديثات المخزنة مؤقتًا ، ولكن لا يمكن ضمانه إذا كان الجهاز يتم التأكيد ، وإذا قمت بتحرير المؤشرات والمؤشرات الخاصة بك بشكل صحيح إلى قواعد البيانات (التي لا ينبغي أن تكون أعضاء ثابتة) ، فإن المساعد سوف يغلق قاعدة البيانات على أي حال.

لذا ، فإن هذا هو:

استخدم getWriteableDatabase لفتح من مجمع مفرد. (استخدمت فئة تطبيق مشتقة لتوفير سياق التطبيق من ثابت لحل الحاجة إلى سياق).

أبدا الاتصال مباشرة قريبة.

لا تقم أبداً بتخزين قاعدة البيانات الناتجة في أي كائن ليس له نطاق واضح ويعتمد على حساب المرجع لتشغيل إغلاق ضمني ().

إذا كنت تقوم بمعالجة مستوى الملف ، فقم بإيقاف كل نشاط قاعدة البيانات ثم توقف الاتصال فقط في حالة وجود مؤشر ترابط بعيد عن الافتراض بأنك تكتب المعاملات المناسبة حتى يفشل مؤشر الترابط البعيد وأن قاعدة البيانات المغلقة ستحصل على الأقل على المعاملات المناسبة بدلاً من ذلك من المحتمل نسخة لمستوى الملف من معاملة جزئية.


بعد تكافح مع هذا لبضع ساعات ، لقد وجدت أنه يمكنك فقط استخدام كائن مساعد واحد ديسيبل لكل تنفيذ ديسيبل. فمثلا،

for(int x = 0; x < someMaxValue; x++)
{
    db = new DBAdapter(this);
    try
    {

        db.addRow
        (
                NamesStringArray[i].toString(), 
                StartTimeStringArray[i].toString(),
                EndTimeStringArray[i].toString()
        );

    }
    catch (Exception e)
    {
        Log.e("Add Error", e.toString());
        e.printStackTrace();
    }
    db.close();
}

كما كان من المفترض أن:

db = new DBAdapter(this);
for(int x = 0; x < someMaxValue; x++)
{

    try
    {
        // ask the database manager to add a row given the two strings
        db.addRow
        (
                NamesStringArray[i].toString(), 
                StartTimeStringArray[i].toString(),
                EndTimeStringArray[i].toString()
        );

    }
    catch (Exception e)
    {
        Log.e("Add Error", e.toString());
        e.printStackTrace();
    }

}
db.close();

إنشاء DBAdapter جديد في كل مرة كانت تكرار الحلقة الطريقة الوحيدة التي يمكنني الحصول على سلاسل بلدي في قاعدة بيانات من خلال الفئة مساعدتي.


قاعدة البيانات مرنة للغاية مع خيوط المعالجة المتعددة. ضرباتي تطبيقات DBs من العديد من المواضيع المختلفة في وقت واحد ويفعل ما يرام. في بعض الحالات ، أواجه عمليات متعددة تصل إلى DB في وقت واحد وهذا يعمل بشكل جيد أيضًا.

مهامك غير المتزامنة - استخدم نفس الاتصال عندما تستطيع ، ولكن إذا كنت مضطرًا لذلك ، يمكنك الوصول إلى DB من مهام مختلفة.


ما أفهمه من APIs SQLiteDatabase هو أنه في حالة وجود تطبيق متعدد الخيوط ، لا يمكنك تحمل أكثر من كائن SQLiteDatabase واحد يشير إلى قاعدة بيانات واحدة.

يمكن إنشاء الكائن بكل تأكيد ولكن يفشل الإدراج / التحديثات إذا بدأت مؤشرات الترابط / العمليات المختلفة (أيضًا) في استخدام كائنات SQLiteDatabase مختلفة (مثل كيفية استخدامنا في اتصال JDBC).

الحل الوحيد هنا هو التمسك بكائنات SQLiteDatabase واحدة وحيثما يتم استخدام startTransaction () في أكثر من مؤشر ترابط واحد ، يقوم Android بإدارة القفل عبر سلاسل مختلفة ويسمح فقط بموضوع واحد في كل مرة بالوصول إلى التحديث الحصري.

كما يمكنك القيام "بالقراءة" من قاعدة البيانات واستخدام نفس كائن SQLiteDatabase في مؤشر ترابط مختلف (أثناء كتابة مؤشر ترابط آخر) ولن يكون هناك أي تلف في قاعدة البيانات أي "قراءة الموضوع" لن يقرأ البيانات من قاعدة البيانات حتى " كتابة مؤشر الترابط "تلتزم البيانات على الرغم من استخدام كلا نفس كائن SQLiteDatabase.

يختلف هذا عن كيفية اتصال كائن في JDBC حيث إذا قمت بالمرور (استخدام نفس) كائن الاتصال بين سلاسل القراءة والكتابة ، فمن المحتمل أن نقوم بطباعة البيانات غير الملتزم بها أيضًا.

في تطبيق المؤسسة الخاصة بي ، أحاول استخدام تدقيق شرطي حتى لا يضطر مؤشر ترابط واجهة المستخدم إلى الانتظار ، بينما يحتفظ مؤشر ترابط BG بكائن SQLiteDatabase (بشكل خاص). أحاول توقع إجراءات واجهة المستخدم وتأجيل خيط BG من التشغيل لمدة "x" ثانية. كما يمكن للمرء أن يحافظ على PriorityQueue لإدارة تسليم كائنات اتصال SQLiteDatabase بحيث يحصل مؤشر ترابط واجهة المستخدم عليه أولاً.


  • استخدم مؤشر Thread أو AsyncTask للعمليات التي تعمل لفترة طويلة (50ms +). اختبر التطبيق لمعرفة مكانه. معظم العمليات (ربما) لا تتطلب خيط ، لأن معظم العمليات (ربما) تنطوي فقط على عدد قليل من الصفوف. استخدم مؤشر ترابط لعمليات السائبة.
  • مشاركة مثيل SQLiteDatabase واحد لكل DB على القرص بين مؤشرات الترابط وتنفيذ نظام حساب لمتابعة الاتصالات المفتوحة.

هل هناك أفضل الممارسات لهذه السيناريوهات؟

مشاركة مجال ثابت بين جميع الفصول الدراسية الخاصة بك. اعتدت على الحفاظ على المفرد حول ذلك وغيرها من الأشياء التي تحتاج إلى أن تكون مشتركة. يجب أيضًا استخدام نظام حساب (يستخدم عادة AtomicInteger) للتأكد من عدم إغلاق قاعدة البيانات في وقت مبكر أو تركها مفتوحة.

بلدي الحل:

للحصول على الإصدار الأحدث ، اطلع على https://github.com/JakarCo/databasemanager ولكنني سأحاول تحديث الشفرة هنا أيضًا. إذا كنت تريد فهم حلّي ، فابحث عن الشفرة واقرأ ملاحظاتي. ملاحظاتي هي عادة مفيدة جدا.

  1. نسخ / لصق التعليمة البرمجية في ملف جديد باسم DatabaseManager . (أو تنزيله من جيثب)
  2. قم بتوسيع DatabaseManager وتطبيق onCreate و onUpgrade كما تفعل عادة. يمكنك إنشاء فئات فرعية متعددة لفئة DatabaseManager أجل الحصول على قواعد بيانات مختلفة على القرص.
  3. Instantiate الفئة subclass الخاصة بك واستدعاء getDb() لاستخدام فئة SQLiteDatabase .
  4. close() المكالمة close() لكل فئة فرعية قمت بتكوينها

رمز نسخ / لصق :

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

import java.util.concurrent.ConcurrentHashMap;

/** Extend this class and use it as an SQLiteOpenHelper class
 *
 * DO NOT distribute, sell, or present this code as your own. 
 * for any distributing/selling, or whatever, see the info at the link below
 *
 * Distribution, attribution, legal stuff,
 * See https://github.com/JakarCo/databasemanager
 * 
 * If you ever need help with this code, contact me at [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();
                }
        } 
    }
}




sqlite3