¿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 seleccionar consultas desde un doInBackground de AsyncTask? ¿O debería usar el hilo de interfaz de usuario? Supongo que las consultas de la base de datos pueden ser "pesadas" y no deberían usar el hilo de la interfaz de usuario, ya que puede bloquear la aplicación, lo que resulta en 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?
Respuestas:
Las inserciones, actualizaciones, eliminaciones y lecturas generalmente están bien en varios subprocesos, pero la respuesta de Brad no es correcta. Debes tener cuidado con cómo creas tus conexiones y las usas. Hay situaciones en las que sus llamadas de actualización fallarán, incluso si su base de datos no se corrompe.
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 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 bloqueos java para mantener el acceso serializado. Por lo tanto, si 100 subprocesos tienen una instancia de db, las llamadas a la base de datos en disco real se serializan.
Entonces, un ayudante, una conexión db, que se serializa en código java. Un subproceso, 1000 subprocesos, si usa una instancia auxiliar compartida entre ellos, todo su código de acceso a base de datos es serial. Y la vida es buena (ish).
Si intenta escribir en la base de datos desde conexiones distintas reales 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 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 SABES que solo se escribirá un hilo, PUEDES poder usar múltiples conexiones, y tus lecturas serán más rápidas, pero ten 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.
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 segura de creación / llamada 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:
También revise la horquilla por 2 puntos 0 del ejemplo de bloqueo mencionado anteriormente:
fuente
Acceso concurrente a la base de datos
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 base de datos de Android sea seguro.
Suponiendo que tiene su propio SQLiteOpenHelper .
Ahora desea escribir datos en la base de datos en hilos separados.
Recibirá el siguiente mensaje en su logcat y uno de sus cambios no se escribirá.
Esto sucede porque cada vez que crea un nuevo objeto SQLiteOpenHelper , en realidad está haciendo una nueva conexión de base de datos. Si intenta escribir en la base de datos desde conexiones distintas reales al mismo tiempo, una fallará. (de la respuesta anterior)
Para usar la base de datos con múltiples hilos necesitamos asegurarnos de que estamos usando una conexión de base de datos.
Hagamos el Administrador de base de datos de clase singleton que contendrá y devolverá un solo objeto SQLiteOpenHelper .
El código actualizado que escribe datos en la base de datos en hilos separados se verá así.
Esto te traerá otro choque.
Puesto que estamos utilizando sólo una conexión de base de datos, método getDatabase () devolver misma instancia de SQLiteDatabase objeto para Thread1 y Thread2 . Lo que está sucediendo, Thread1 puede cerrar la base de datos, mientras que Thread2 todavía la está usando. Es por eso que tenemos el bloqueo IllegalStateException .
Necesitamos asegurarnos de que nadie esté usando la base de datos y solo luego cerrarla. Algunas personas en stackoveflow recomendaron nunca cerrar su SQLiteDatabase . Esto dará como resultado el siguiente mensaje logcat.
Muestra de trabajo
Úselo de la siguiente manera.
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 de base de datos, de lo contrario, la conexión de 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, estamos cerrando la conexión de la base de datos.
Ahora debería poder usar su base de datos y asegurarse de que sea segura para subprocesos.
fuente
if(instance==null)
? No deja más remedio que llamar a initialize cada vez; ¿De qué otra manera sabría si se ha inicializado o no en otras aplicaciones, etc.?initializeInstance()
tiene un parámetro de tipoSQLiteOpenHelper
, pero en su comentario mencionó usarDatabaseManager.initializeInstance(getApplicationContext());
. Que esta pasando? ¿Cómo puede funcionar esto?Thread
oAsyncTask
para operaciones de larga duración (50 ms o más). Prueba tu aplicación para ver dónde está. La mayoría de las operaciones (probablemente) no requieren un subproceso, porque la mayoría de las operaciones (probablemente) solo involucran unas pocas filas. Use un hilo para operaciones masivas.SQLiteDatabase
instancia para cada base de datos en el disco entre subprocesos e implemente un sistema de conteo para realizar un seguimiento de las conexiones abiertas.Comparta un campo estático entre todas sus clases. Solía mantener un singleton para eso y otras cosas que necesitan ser compartidas. También se debe usar un esquema de conteo (generalmente usando AtomicInteger) para asegurarse de que nunca cierre la base de datos antes de tiempo o la deje abierta.
Para obtener la versión más reciente, consulte https://github.com/JakarCo/databasemanager, pero también intentaré mantener el código actualizado aquí. Si quieres entender mi solución, mira el código y lee mis notas. Mis notas suelen ser bastante útiles.
DatabaseManager
. (o descargarlo de github)DatabaseManager
e implementaronCreate
yonUpgrade
como lo haría normalmente. Puede crear múltiples subclases de unaDatabaseManager
clase para tener diferentes bases de datos en el disco.getDb()
para usar laSQLiteDatabase
clase.close()
por cada subclase que haya instanciadoEl código para copiar / pegar :
fuente
close
, debeopen
volver a llamar antes de usar la misma instancia de la clase O puede crear una nueva instancia. Como establecí en elclose
código,db=null
no podría usar el valor de retorno degetDb
(ya que sería nulo), por lo que obtendría unNullPointerException
si hiciera algo comomyInstance.close(); myInstance.getDb().query(...);
getDb()
yopen()
en un solo método?La base de datos es muy flexible con subprocesos múltiples. Mis aplicaciones llegan a sus bases de datos desde muchos subprocesos diferentes simultáneamente y funciona bien. En algunos casos, tengo múltiples procesos golpeando la base de datos simultáneamente y eso también funciona bien.
Sus tareas asíncronas: use la misma conexión cuando pueda, pero si es necesario, está bien acceder a la base de datos desde diferentes tareas.
fuente
SQLiteDatabase
objetos en diferentesAsyncTask
s /Thread
s, pero a veces conducir a errores y por eso SQLiteDatabase (línea 1297) usosLock
sLa 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 una excepción de puntero nulo, de lo contrario, getWritableDatabase aún no devuelto en un subproceso y openDatabse llamado en otro subproceso mientras tanto.
fuente
Después de luchar con esto durante un par de horas, descubrí que solo puede usar un objeto de ayuda de db por ejecución de db. Por ejemplo,
según lo dispuesto para:
crear un nuevo DBAdapter cada vez que el ciclo itera era la única forma en que podía ingresar mis cadenas en una base de datos a través de mi clase auxiliar.
fuente
Comprendo las API de SQLiteDatabase es que, en caso de que tenga una aplicación multiproceso, no puede permitirse tener más de un objeto SQLiteDatabase que apunte a una sola base de datos.
El objeto definitivamente se puede crear, pero las inserciones / actualizaciones fallan si diferentes subprocesos / procesos (también) comienzan a usar diferentes objetos SQLiteDatabase (como la forma en que usamos en JDBC Connection).
La única solución aquí es quedarse con 1 objetos SQLiteDatabase y cada vez que se usa startTransaction () en más de 1 subproceso, Android administra el bloqueo en diferentes subprocesos y permite que solo 1 subproceso a la vez tenga acceso exclusivo a la actualización.
También puede hacer "Lecturas" de la base de datos y usar el mismo objeto SQLiteDatabase en un hilo diferente (mientras otro hilo escribe) y nunca habría corrupción de la base de datos, es decir, "leer hilo" no leería los datos de la base de datos hasta que " hilo de escritura "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 (use el mismo) el objeto de conexión entre los hilos de lectura y escritura, probablemente también estaríamos imprimiendo datos no confirmados.
En mi aplicación empresarial, trato de usar verificaciones condicionales para que el subproceso de la interfaz de usuario nunca tenga que esperar, mientras que el subproceso BG contiene el objeto SQLiteDatabase (exclusivamente). Intento predecir las acciones de la interfaz de usuario y diferir la ejecución del hilo BG durante 'x' segundos. También se puede mantener PriorityQueue para administrar la distribución de objetos de conexión SQLiteDatabase para que el subproceso de interfaz de usuario lo obtenga primero.
fuente
"read thread" wouldn't read the data from the database till the "write thread" commits the data although both use the same SQLiteDatabase object
. Esto no siempre es cierto, si comienza "leer hilo" justo después de "escribir hilo", es posible que no obtenga los datos recién actualizados (insertados o actualizados en el hilo de escritura). El hilo de lectura puede leer los datos antes de comenzar el hilo de escritura. Esto sucede porque la operación de escritura inicialmente habilita el bloqueo reservado en lugar del bloqueo exclusivo.Puede intentar aplicar un nuevo enfoque de arquitectura 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
UsuarioDao.java
AppDatabase.java
fuente
Habiendo tenido algunos problemas, creo que he entendido por qué me he estado equivocando.
Había escrito una clase de contenedor de base de datos que incluía una
close()
que llamaba al asistente de cierre como un espejo de laopen()
que llamaba getWriteableDatabase y luego había migrado a unaContentProvider
. El modelo paraContentProvider
no utiliza,SQLiteDatabase.close()
lo que creo que es una gran pista, ya que el código sí lo hace.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 hay un comentario ligeramente ominoso en la documentación cercana
(Mi negrita).
Así que he tenido bloqueos intermitentes donde uso hilos de fondo para acceder a la base de datos y se ejecutan al mismo tiempo que en primer plano.
Por lo tanto, creo que
close()
obliga a la base de datos a cerrarse independientemente de cualquier otro subproceso que contenga referencias, porclose()
lo que no solo deshace la coincidencia,getWriteableDatabase
sino que fuerza el cierre de cualquier solicitud abierta. La mayoría de las veces esto no es un problema, ya que el código es de un solo subproceso, pero en casos de subprocesos múltiples siempre existe la posibilidad de abrir y cerrar la sincronización.Después de leer los 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 obligar a SqLite a escriba cualquier material almacenado en caché que pueda estar merodeando; en otras palabras, detenga toda la actividad de la base de datos de la aplicación, cierre en caso de que el Asistente haya perdido la pista, realice cualquier actividad a nivel de archivo (copia de seguridad / restauración) y luego comience de nuevo.
Aunque parece una buena idea intentar cerrar de forma controlada, la realidad es que Android se reserva el derecho de tirar a la basura su VM, por lo que cualquier cierre está reduciendo el riesgo de que no se escriban actualizaciones en caché, pero no se puede garantizar si el dispositivo está estresado, y si ha liberado correctamente sus cursores y referencias a 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. (Usé 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 al cierre.
Nunca almacene la base de datos resultante en ningún objeto que no tenga un alcance obvio y confíe en el recuento de referencias para desencadenar un cierre implícito ().
Si realiza el manejo a nivel de archivo, detenga toda la actividad de la base de datos y luego cierre la llamada solo en caso de que haya un subproceso fuera de control en el supuesto de que escriba transacciones adecuadas para que el subproceso fallido y la base de datos cerrada al menos tenga las transacciones adecuadas en lugar que potencialmente una copia a nivel de archivo de una transacción parcial.
fuente
Sé que la respuesta llega tarde, pero la mejor manera de ejecutar consultas sqlite en Android es a través de un proveedor de contenido personalizado. De esa manera, la interfaz de usuario se desacopla con la clase de base de datos (la clase que extiende la clase SQLiteOpenHelper). También las consultas se ejecutan en un hilo de fondo (Cursor Loader).
fuente