¿Cuáles son las mejores prácticas para SQLite en Android?

¿Qué 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, eliminar y seleccionar consultas de doInBackground de una AsyncTask? ¿O debería usar el hilo UI? Supongo que las consultas a la base de datos pueden ser “pesadas” y no deberían usar el hilo de la interfaz de usuario, ya que pueden 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?

¿Hay alguna mejor práctica para estos escenarios?

Las inserciones, actualizaciones, eliminaciones y lecturas generalmente son correctas desde varios hilos, pero la respuesta de Brad no es correcta. Tienes que tener cuidado con la forma de crear tus conexiones y usarlas. Hay situaciones en las que fallarán sus llamadas de actualización, incluso si su base de datos no se corrompe.

La respuesta básica.

El objeto SqliteOpenHelper se mantiene en una conexión de base de datos. Parece ofrecerle una conexión de lectura y escritura, pero realmente no lo hace. Llame al de solo lectura y obtendrá la conexión de la base de datos de escritura independientemente.

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

Entonces, un ayudante, una conexión db, que se serializa en código java. Un hilo, 1000 hilos, si usa una instancia auxiliar compartida entre ellos, todo su código de acceso db es serial. Y la vida es buena (ish).

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

Entonces, ¿múltiples hilos? Usa un ayudante Período. Si usted SABE que solo un hilo estará escribiendo, PUEDE poder usar conexiones múltiples, y sus lecturas serán más rápidas, pero tenga cuidado con el comprador. No he probado tanto.

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

  • Android Sqlite Locking (Enlace actualizado 18/06/2012)
  • Android-Database-Locking-Collisions-Ejemplo de touchlab en GitHub

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


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

  • Conexión única de SQLite

También revise la horquilla por 2point0 del ejemplo de locking mencionado anteriormente:

  • Android-Database-Locking-Collisions-Ejemplo por 2point0 en GitHub

Acceso concurrente a la base de datos

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

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


Suponiendo que tiene su propio SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... } 

Ahora desea escribir datos en la base de datos en hilos 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 será escrito.

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

Esto está sucediendo porque cada vez que creas un nuevo objeto SQLiteOpenHelper estás haciendo una nueva conexión a la base de datos. Si intenta escribir en la base de datos desde conexiones distintas reales al mismo tiempo, uno fallará. (de la respuesta anterior)

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

Hagamos un Database Manager de clase singleton que contendrá y devolverá un solo 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 hilos 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 choque.

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

Dado que estamos utilizando 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, Thread1 puede cerrar la base de datos, mientras que Thread2 todavía lo está usando. Es por eso que tenemos un crash de IllegalStateException .

Necesitamos asegurarnos de que nadie esté usando la base de datos y solo entonces cerrarla. Algunas personas en stackoveflow recomiendan nunca cerrar su SQLiteDatabase . No solo suena estúpido sino que también te honra con 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 ocurre en el método closeDatabase () . Cada vez que llamamos a este método, el contador disminuye, cada vez que va a cero, estamos cerrando la conexión a la base de datos.


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

  • Use un Thread o AsyncTask para AsyncTask de larga duración (50 ms +). Prueba tu aplicación para ver dónde está. La mayoría de las operaciones (probablemente) no requieren un hilo, porque la mayoría de las operaciones (probablemente) solo implican algunas filas. Use un hilo para operaciones masivas.
  • Comparta una instancia SQLiteDatabase para cada DB en el disco entre subprocesos e implemente un sistema de conteo para realizar un seguimiento de las conexiones abiertas.

¿Hay alguna mejor práctica para estos escenarios?

Comparte un campo estático entre todas tus clases. Solía ​​mantener un singleton para eso y para otras cosas que necesitan ser compartidas. También se debe usar un esquema de conteo (generalmente usando AtomicInteger) para asegurarse de nunca cerrar la base de datos anticipadamente o dejarla abierta.

Mi solución:

Para la versión más reciente, consulte https://github.com/JakarCo/databasemanager, pero intentaré actualizar el código aquí también. Si quieres entender mi solución, mira el código y lee mis notas. Mis notas usualmente son bastante útiles.

  1. copie / pegue el código en un nuevo archivo llamado DatabaseManager . (o descárguelo de github)
  2. extienda DatabaseManager e implemente onCreate y onUpgrade como lo haría normalmente. Puede crear múltiples subclases de una clase de DatabaseManager para tener diferentes bases de datos en el disco.
  3. Crea una instancia de tu subclase y llama a getDb() para usar la clase SQLiteDatabase .
  4. Llamar close() para cada subclase que instanciaste

El código para copiar / pegar :

 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 support@androidsqlitelibrary.com (or support@jakar.co ) * * 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 Android SQLite Libray 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 dbMap = new ConcurrentHashMap(); private static final Object lockObject = new Object(); private DBSQLiteOpenHelper sqLiteOpenHelper; private SQLiteDatabase db; private Context context; /** Instantiate a new DB Helper. * 
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 *
If the new counter is 0, then the database will be closed. *

This needs to be called before application exit. *
If the counter is 0, then the underlying SQLiteDatabase is null 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(); } } } }

La base de datos es muy flexible con multi-threading. Mis aplicaciones llegan a sus bases de datos desde muchos hilos diferentes simultáneamente y funciona bien. En algunos casos, tengo múltiples procesos golpeando el DB simultáneamente y eso también funciona bien.

Tus tareas asíncronas: utiliza la misma conexión cuando puedas, pero si tienes que hacerlo, está bien acceder al DB desde diferentes tareas.

La respuesta de Dmytro funciona bien para mi caso. Creo que es mejor declarar la función como sincronizada. al menos para mi caso, invocaría la excepción del puntero nulo de otra manera, por ejemplo, getWritableDatabase aún no se devolvió en un hilo y openDatabse llamado en otro hilo mientras tanto.

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

Mi comprensión de las API de SQLiteDatabase es que, en caso de que tenga una aplicación de múltiples hilos, no puede permitirse tener más de un objeto SQLiteDatabase apuntando a una sola base de datos.

El objeto definitivamente se puede crear, pero las inserciones / actualizaciones fallan si diferentes hilos / procesos (también) comienzan a usar diferentes objetos SQLiteDatabase (como la forma en que usamos en la Conexión JDBC).

La única solución aquí es seguir con 1 objetos SQLiteDatabase y cada vez que se usa startTransaction () en más de 1 subproceso, Android gestiona el locking en diferentes subprocesos y permite solo 1 subproceso a la vez tener acceso de actualización exclusivo.

También puede hacer “lecturas” desde la base de datos y usar el mismo objeto SQLiteDatabase en un hilo diferente (mientras que otro hilo escribe) y nunca habría corrupción de base de datos, es decir, “hilo de lectura” no leería los datos de la base de datos hasta el ” write thread “confirma los datos aunque ambos usan el mismo objeto SQLiteDatabase.

Esto es diferente de cómo está el objeto de conexión en JDBC, donde si pasa (usa el mismo) el objeto de conexión entre los hilos de lectura y escritura, entonces probablemente también estaríamos imprimiendo datos no confirmados.

En mi aplicación empresarial, trato de usar comprobaciones condicionales para que el subproceso UI nunca tenga que esperar, mientras que el subproceso BG contiene el objeto SQLiteDatabase (exclusivamente). Intento predecir Acciones de UI y aplazar la ejecución del hilo BG durante ‘x’ segundos. También se puede mantener PriorityQueue para gestionar la distribución de objetos de conexión SQLiteDatabase para que el subproceso UI lo obtenga primero.

después de luchar con esto por un par de horas, descubrí que solo puedes usar un objeto db helper por ejecución de db. Por ejemplo,

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

como apposed a:

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

crear un nuevo DBAdapter cada vez que el ciclo itera era la única forma en que podía obtener mis cadenas en una base de datos a través de mi clase de ayuda.

Después de haber tenido algunos problemas, creo haber entendido por qué me he equivocado.

Escribí una clase contenedora de base de datos que incluía un close() que llamaba al helper close como un espejo de open() que llamaba getWriteableDatabase y luego migraba a ContentProvider . El modelo para ContentProvider no usa SQLiteDatabase.close() que creo que es una gran pista ya que el código usa getWriteableDatabase En algunos casos todavía estaba haciendo acceso directo (consultas de validación de pantalla en general, así que migré a un modelo getWriteableDatabase / rawQuery .

Yo uso un singleton y está el comentario ligeramente ominoso en la documentación cercana

Cierre cualquier objeto de base de datos abierto

(mi negrita).

Así que he tenido lockings intermitentes donde uso hilos de fondo para acceder a la base de datos y se ejecutan al mismo tiempo que en primer plano.

Así que creo que close() obliga a la base de datos a cerrarse independientemente de cualquier otro hilo que tenga referencias, así que close() no solo está deshaciendo la base getWriteableDatabase sino que obliga a cerrar cualquier solicitud abierta. La mayoría de las veces esto no es un problema, ya que el código es de subprocesamiento único, pero en los casos de subprocesos múltiples siempre existe la posibilidad de abrir y cerrar la sincronización.

Tras haber leído comentarios en otros lugares que explican que la instancia del código SqLiteDatabaseHelper cuenta, entonces la única vez que desea un cierre es donde desea la situación en la que desea hacer una copia de seguridad, y desea forzar el cierre de todas las conexiones y forzar a SqLite a escriba cualquier cosa almacenada en caché que pueda estar merodeando, es decir, detenga toda la actividad de la base de datos de la aplicación, cierre en caso de que el Ayudante haya perdido la ruta, realice cualquier actividad de nivel de archivo (copia de seguridad / restauración) y vuelva a comenzar.

Aunque parece una buena idea intentar cerrar de forma controlada, la realidad es que Android se reserva el derecho de destruir su máquina virtual, por lo que cualquier cierre reduce el riesgo de que no se escriban las actualizaciones en caché, pero no se puede garantizar si el dispositivo está estresado, y si ha liberado correctamente sus cursores y referencias a las bases de datos (que no deberían ser miembros estáticos), el ayudante habrá cerrado la base de datos de todos modos.

Entonces mi opinión es que el enfoque es:

Use getWriteableDatabase para abrir desde un contenedor singleton. (Utilicé una clase de aplicación derivada para proporcionar el contexto de la aplicación desde una estática para resolver la necesidad de un contexto).

Nunca llame directamente cerca.

Nunca almacene la base de datos resultante en ningún objeto que no tenga un scope obvio y confíe en el recuento de referencias para activar un cierre implícito ().

Si realiza el manejo a nivel de archivos, detenga toda la actividad de la base de datos y luego cierre por solo en caso de que exista un subproceso fugitivo en el supuesto de que escriba las transacciones adecuadas para que el hilo fuera de control falle y la base de datos cerrada tenga al menos transacciones apropiadas que potencialmente una copia de nivel de archivo de una transacción parcial.

Puede intentar aplicar el nuevo enfoque de architecture anunciado en Google I / O 2017.

También incluye una nueva biblioteca ORM llamada Room

Contiene tres componentes principales: @Entity, @Dao y @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 getAll(); @Query("SELECT * FROM user WHERE uid IN (:userIds)") List 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(); } 

Sé que la respuesta es tardía, pero la mejor manera de ejecutar consultas sqlite en Android es a través de un proveedor de contenido personalizado. De esa forma, la IU está desacoplada con la clase de base de datos (la clase que amplía la clase SQLiteOpenHelper). También las consultas se ejecutan en una cadena de fondo (Cargador de Cursor).