¿Cómo puedo evitar problemas de concurrencia al usar SQLite en Android?

Resuelto Vidar Vestnes asked hace 54 años • 10 respuestas

¿Cuáles se considerarían las mejores prácticas al ejecutar consultas en una base de datos SQLite dentro de una aplicación de Android?

¿Es seguro ejecutar inserciones, eliminaciones y consultas de selección desde doInBackground de AsyncTask? ¿O debería utilizar el hilo de la interfaz de usuario? Supongo que las consultas a la base de datos pueden ser "pesadas" y no deberían utilizar el subproceso de la interfaz de usuario, ya que puede bloquear la aplicación, lo que da como resultado una aplicación que no responde (ANR).

Si tengo varias AsyncTasks, ¿deberían compartir una conexión o deberían abrir una conexión cada una?

¿Existen mejores prácticas para estos escenarios?

Vidar Vestnes avatar Jan 01 '70 08:01 Vidar Vestnes
Aceptado

Las inserciones, actualizaciones, eliminaciones y lecturas generalmente están bien en varios hilos, pero la respuesta de Brad no es correcta. Tienes que tener cuidado con cómo creas tus conexiones y las utilizas. Hay situaciones en las que las llamadas de actualización fallarán, incluso si la base de datos no se daña.

La respuesta básica.

El objeto SqliteOpenHelper mantiene una conexión de base de datos. Parece ofrecerle una conexión de lectura y escritura, pero en realidad no es así. Llame al modo de solo lectura y obtendrá la conexión de base de datos de escritura independientemente.

Entonces, una instancia auxiliar, una conexión de base de datos. Incluso si lo usas desde múltiples hilos, una conexión a la vez. El objeto SqliteDatabase utiliza bloqueos de Java para mantener el acceso serializado. Entonces, si 100 subprocesos tienen una instancia de base de datos, las llamadas a la base de datos real en el disco se serializan.

Entonces, un ayudante, una conexión de base de datos, que está serializada en código Java. Un hilo, 1000 hilos, si usa una instancia auxiliar compartida entre ellos, todo su código de acceso a la base de datos es en serie. Y la vida es buena (más o menos).

Si intenta escribir en la base de datos desde conexiones distintas al mismo tiempo, una fallará. No esperará hasta que termine el primero y luego escribirá. Simplemente no escribirá su cambio. Peor aún, si no llama a la versión correcta de insertar/actualizar en SQLiteDatabase, no obtendrá una excepción. Recibirás un mensaje en tu LogCat y eso será todo.

Entonces, ¿varios hilos? Utilice un ayudante. Período. Si SABE que solo se escribirá un hilo, PUEDE poder usar múltiples conexiones y sus lecturas serán más rápidas, pero tenga cuidado. No he probado tanto.

Aquí hay una publicación de blog con muchos más detalles y una aplicación de ejemplo.

  • Bloqueo de Android Sqlite (enlace actualizado el 18/06/2012)
  • Ejemplo de colisiones de bloqueo de base de datos de Android de touchlab en GitHub

De hecho, Gray y yo estamos terminando una herramienta ORM, basada en su Ormlite, que funciona de forma nativa con implementaciones de bases de datos de Android y sigue la estructura de creación/llamada segura que describo en la publicación del blog. Eso debería salir muy pronto. Echar un vistazo.


Mientras tanto, hay una publicación de blog de seguimiento:

  • Conexión SQLite única

Consulte también la bifurcación de 2point0 del ejemplo de bloqueo mencionado anteriormente:

  • Ejemplo de colisiones de bloqueo de base de datos de Android de 2point0 en GitHub
Kevin Galligan avatar Sep 11 '2010 05:09 Kevin Galligan

Acceso simultáneo a la base de datos

Mismo artículo en mi blog (me gusta más formatear)

Escribí un pequeño artículo que describe cómo hacer que el acceso a los subprocesos de su base de datos de Android sea seguro.


Suponiendo que tenga su propio SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... }

Ahora desea escribir datos en la base de datos en subprocesos separados.

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

Recibirá el siguiente mensaje en su logcat y uno de sus cambios no se escribirá.

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

Esto sucede porque cada vez que crea un nuevo objeto SQLiteOpenHelper , en realidad está realizando una nueva conexión a la base de datos. Si intenta escribir en la base de datos desde conexiones distintas al mismo tiempo, una fallará. (de la respuesta anterior)

Para usar una base de datos con múltiples subprocesos, debemos asegurarnos de que estamos usando una conexión de base de datos.

Creemos un Administrador de base de datos de clase singleton que contendrá y devolverá un único objeto 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();
    }

}

El código actualizado que escribe datos en la base de datos en subprocesos separados se verá así.

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

Esto te traerá otro accidente.

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

Dado que utilizamos solo una conexión de base de datos, el método getDatabase() devuelve la misma instancia del objeto SQLiteDatabase para Thread1 y Thread2 . Lo que está sucediendo es que Thread1 puede cerrar la base de datos, mientras Thread2 todavía la está usando. Es por eso que tenemos el bloqueo de IllegalStateException .

Necesitamos asegurarnos de que nadie esté usando la base de datos y solo entonces cerrarla. Algunas personas en stackoveflow recomendaron no cerrar nunca su base de datos SQLite . Esto dará como resultado el siguiente mensaje de logcat.

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

muestra de trabajo

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

        }
    }

}

Úselo de la siguiente manera.

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

Cada vez que necesite una base de datos, debe llamar al método openDatabase() de la clase DatabaseManager . Dentro de este método, tenemos un contador que indica cuántas veces se abre la base de datos. Si es igual a uno, significa que necesitamos crear una nueva conexión a la base de datos; si no, la conexión a la base de datos ya está creada.

Lo mismo sucede en el método closeDatabase() . Cada vez que llamamos a este método, el contador disminuye; cada vez que llega a cero, cerramos la conexión a la base de datos.


Ahora debería poder utilizar su base de datos y asegurarse de que sea segura para subprocesos.

Dmytro Danylyk avatar Nov 15 '2013 08:11 Dmytro Danylyk