tutorial - ¿Cuáles son las mejores prácticas para SQLite en Android?
sqlite android tutorial español pdf (10)
¿Cuáles serían las mejores prácticas consideradas al ejecutar consultas en una base de datos SQLite dentro de una aplicación de Android?
¿Es seguro ejecutar inserciones, eliminaciones y selección de 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 subproceso 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 buenas prácticas para estos escenarios?
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 base de datos de Android sea seguro.
Suponiendo que tienes tu 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 se escribirá.
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
Esto está sucediendo 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, se producirá un error. (de la respuesta anterior)
Para usar la base de datos con varios subprocesos, necesitamos asegurarnos de que estamos usando una conexión de base de datos.
Hagamos el Administrador de base de datos de clase singleton que retendrá 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 solo estamos utilizando 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 la está utilizando. Es por eso que tenemos accidente IllegalStateException .
Necesitamos asegurarnos de que nadie esté usando la base de datos y luego cerrarla. Algunas personas en stackoveflow recomiendan no cerrar nunca su base de datos SQLite . No solo suena estúpido sino que también lo 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();
}
}
}
Úsalo 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 de base de datos, de lo contrario, la conexión de 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, siempre que va 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 seguro para subprocesos.
Después de haber tenido algunos problemas, creo que he entendido por qué me he equivocado.
Había escrito una clase de envoltura de base de datos que incluía un close()
que llamaba al ayudante close como un espejo de open()
que llamaba getWriteableDatabase y luego había migrado a un 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 aún estaba haciendo acceso directo (las consultas de validación de la pantalla en general, así que migré a un modelo getWriteableDatabase / rawQuery .
Utilizo un singleton y hay un comentario ligeramente ominoso en la documentación de cierre
Cerrar cualquier objeto de base de datos abierta
(mi negrita).
Así que he tenido bloqueos intermitentes en los que utilizo 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 subproceso que contenga referencias, por lo que close()
sí mismo no es simplemente deshacer la base de datos getWriteableDatabase
sino que obliga a cerrar las solicitudes abiertas. La mayoría de las veces, esto no es un problema, ya que el código es de un solo subproceso, pero en los casos con múltiples subprocesos siempre existe la posibilidad de abrir y cerrar la sincronización.
Después de haber leído comentarios en otros lugares que explican que la instancia del código SqLiteDatabaseHelper cuenta, la única vez que desea cerrar es cuando desea que 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 escriba cualquier cosa almacenada en la memoria caché que pueda estar merodeando; en otras palabras, detenga toda la actividad de la base de datos de la aplicación, ciérrela en caso de que el Ayudante haya perdido la pista, realice cualquier actividad a nivel de archivo (copia de seguridad / restauración) y luego comience de nuevo.
Aunque suena como una buena idea tratar de cerrar de forma controlada, la realidad es que Android se reserva el derecho de desechar la máquina virtual, por lo que cualquier cierre reduce 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.
Así que mi opinión es que el enfoque es:
Utilice getWriteableDatabase para abrir desde un contenedor de 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 al cierre.
Nunca almacene la base de datos resultante en ningún objeto que no tenga un alcance obvio y confíe en el conteo 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 en caso de que haya un hilo fuera de control en el supuesto de que escribe las transacciones correctas para que el hilo fuera de control falle y la base de datos cerrada tenga al menos las transacciones correctas. que potencialmente una copia a nivel de archivo de una transacción parcial.
La base de datos es muy flexible con múltiples subprocesos. Mis aplicaciones llegan a sus bases de datos desde muchos subprocesos diferentes simultáneamente y funciona bien. En algunos casos tengo varios procesos que golpean 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 tiene que hacerlo, está bien para acceder a la base de datos desde diferentes tareas.
La respuesta de Dmytro funciona bien para mi caso. Creo que es mejor declarar la función como sincronizada. al menos en mi caso, invocaría la excepción de puntero nulo de lo contrario, por ejemplo, getWritableDatabase aún no se devolvió en un subproceso y openDatabse se llamó en otro subproceso mientras tanto.
public synchronized SQLiteDatabase openDatabase() {
if(mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
Las inserciones, actualizaciones, eliminaciones y lecturas generalmente están bien en varios subprocesos, pero la answer de Brad no es correcta. Tienes que tener cuidado con la forma en que creas tus conexiones y las usas. Existen situaciones en las que las llamadas de actualización fallará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 ofrecerte una conexión de lectura y escritura, pero realmente no lo hace. Llame a solo lectura y obtendrá la conexión de la base de datos de escritura independientemente.
Por lo tanto, una instancia de ayuda, una conexión de base de datos. Incluso si lo usas 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 hilos tienen una instancia de db, las llamadas a la base de datos en disco real se serializan.
Entonces, un ayudante, una conexión de base de datos, 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 a la 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, se producirá un error. 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 la base de datos SQLite, no obtendrá una excepción. Solo recibirás un mensaje en tu LogCat, y eso será todo.
Entonces, ¿múltiples hilos? Utilice un ayudante. Período. Si SABES que solo se escribirá un hilo, PUEDE ser capaz de usar múltiples conexiones, y sus lecturas serán más rápidas, pero tenga cuidado con el comprador. No he probado mucho.
Aquí hay una publicación de blog con mucho más detalle 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 en realidad estamos envolviendo una herramienta ORM, basada en su Ormlite, que funciona de forma nativa con implementaciones de base 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:
También revisa la horquilla por 2point0 del ejemplo de bloqueo mencionado anteriormente:
Mi comprensión de las API de base de datos SQLite es que, en caso de que tenga una aplicación de múltiples subprocesos, no puede permitirse tener más de 1 objeto SQLiteDatabase apuntando a una única base de datos.
El objeto definitivamente se puede crear, pero las inserciones / actualizaciones fallan si diferentes procesos / subprocesos (también) comienzan a usar diferentes objetos de base de datos SQLite (como la forma en que usamos en la conexión JDBC).
La única solución aquí es seguir con 1 objetos de base de datos SQLite y siempre que se use un startTransaction () en más de 1 subproceso, Android administra el bloqueo a través de diferentes subprocesos y permite que solo 1 subproceso a la vez tenga acceso exclusivo a la actualización.
También puede hacer "Lecturas" desde la base de datos y usar el mismo objeto SQLiteDatabase en un hilo diferente (mientras se escribe otro hilo) y nunca habría corrupción en la base de datos, es decir, "leer hilo" no leería los datos de la base de datos hasta el " el 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 (usa el mismo) el objeto de conexión entre subprocesos de lectura y escritura, es probable que también imprimamos 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 de BG contiene el objeto SQLiteDatabase (exclusivamente). Intento predecir las acciones de la interfaz de usuario y diferir que el subproceso de BG se ejecute durante ''x'' segundos. También se puede mantener PriorityQueue para administrar la entrega de objetos de Conexión de Base de Datos de SQLite para que el subproceso de la interfaz de usuario lo obtenga primero.
Puede intentar aplicar un nuevo enfoque de arquitectura anounced en Google I / O 2017.
También incluye una nueva biblioteca de ORM llamada Room
Contiene tres componentes principales: @Entity, @Dao y @Database
Usuario.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();
}
Sé que la respuesta es tardía, pero la mejor manera de ejecutar consultas de sqlite en Android es a través de un proveedor de contenido personalizado. De esa manera, la IU se desacopla con la clase de base de datos (la clase que extiende la clase SQLiteOpenHelper). Además, las consultas se ejecutan en un subproceso en segundo plano (Cursor Loader).
después de luchar con esto durante un par de horas, descubrí que solo se puede usar un objeto de ayuda db 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();
}
según se adjunta 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();
la creación de un nuevo DBAdapter cada vez que se repite la repetición del bucle fue la única forma en que podía obtener mis cadenas en una base de datos a través de mi clase auxiliar.
- Use un
Thread
oAsyncTask
paraAsyncTask
de larga duración (50ms +). 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 involucran unas pocas filas. Utilice un hilo para operaciones a granel. - Comparta una instancia de
SQLiteDatabase
para cada base de datos en el disco entre subprocesos e implemente un sistema de conteo para realizar un seguimiento de las conexiones abiertas.
¿Existen buenas prácticas para estos escenarios?
Comparte un campo estático entre todas tus 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.
Mi solución:
Para obtener la versión más actualizada, 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.
- copie / pegue el código en un nuevo archivo llamado
DatabaseManager
. (o descargarlo desde github) - extienda
DatabaseManager
e implementeonCreate
yonUpgrade
como lo haría normalmente. Puede crear varias subclases de la claseDatabaseManager
para tener diferentes bases de datos en el disco. - Cree una instancia de su subclase y llame a
getDb()
para usar la claseSQLiteDatabase
. - Llame a
close()
para cada subclase que haya instanciado
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 [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();
}
}
}
}