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

¿Cuáles serí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, elimina y selecciona consultas desde doInBackground de AsyncTask? ¿O debo usar el hilo de la interfaz de usuario? Supongo que las consultas de base de datos pueden ser "pesadas" y no deben 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?

Las inserciones, las actualizaciones, las supresiones y las lecturas son generalmente aceptables de varios temas, pero la respuesta de Brad no es correcta. Tienes que tener cuidado con cómo crear sus conexiones y utilizarlas. 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 se mantiene en una conexión de base de datos. Parece ofrecerle una conexión de lectura y escritura, pero en realidad no. Llame a la de sólo lectura, y obtendrá la conexión de base de datos de escritura independientemente.

Por lo tanto, una instancia de ayuda, una conexión db. Incluso si lo usa desde múltiples subprocesos, una conexión a la vez. El objeto SqliteDatabase utiliza bloqueos java para mantener el acceso serializado. Por lo tanto, si 100 subprocesos tienen una instancia db, las llamadas a la base de datos real en el disco se serializan.

Por lo tanto, un ayudante, una conexión db, que se serializa en código java. Un hilo, 1000 subprocesos, si utiliza una instancia de ayuda 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 a partir de conexiones reales distintas al mismo tiempo, se producirá un error. No esperará hasta que se haga el primero y luego escriba. 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. Sólo obtendrá un mensaje en su LogCat, y eso será todo.

Entonces, ¿hilos múltiples? Utilice un ayudante. Período. Si usted SABE que sólo un hilo va a escribir, usted PUEDE ser capaz de usar múltiples conexiones, y sus lecturas serán más rápidas, pero el comprador tenga cuidado. No he probado tanto.

Esta es una entrada de blog con mucho más detalle y una aplicación de ejemplo.

  • Bloqueo de Android Sqlite (enlace actualizado 6/18/2012)
  • Android-Database-Locking-Collisions-Ejemplo de touchlab en GitHub

Gray y yo estamos terminando con una herramienta de ORM, basada en 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 entrada del blog. Eso debería salir muy pronto. Echar un vistazo.


Mientras tanto, hay una entrada de blog de seguimiento:

  • Conexión SQLite única

También verifique la horquilla por 2point0 del ejemplo de bloqueo mencionado anteriormente:

  • Android-Database-Locking-Collisions-Ejemplo de 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 base de datos androide sea seguro.


Asumiendo que usted tiene su propio SQLiteOpenHelper .

public class DatabaseHelper extends SQLiteOpenHelper { ... } 

Ahora desea escribir datos en bases 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(); 

Usted 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 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 a partir de conexiones reales distintas al mismo tiempo, se producirá un error. (De la respuesta anterior)

Para utilizar la base de datos con varios subprocesos, debemos asegurarnos de que estamos usando una conexión de base de datos.

Hagamos singleton class Database Manager que mantendrá y devolverá 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 los 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 le traerá otra caída.

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

Dado que estamos utilizando sólo 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á utilizando. Es por eso que tenemos un bloqueo de IllegalStateException .

Necesitamos asegurarnos de que nadie esté usando la base de datos y sólo entonces cerrarla. Algunas personas en stackoveflow recomienda nunca cerrar su base de datos SQLite . No sólo suena estúpido, sino también honrar con el siguiente mensaje 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(); } } } 

Utilícelo como sigue.

 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, 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 se reduce, siempre que va a cero, estamos cerrando la conexión de la base de datos.


Ahora debe ser capaz de utilizar su base de datos y asegúrese de que es hilo seguro.

  • Utilice un Thread o AsyncTask para operaciones largas (50ms +). Pruebe su aplicación para ver dónde está. La mayoría de las operaciones (probablemente) no requieren un hilo, ya que la mayoría de las operaciones (probablemente) sólo implican unas cuantas filas. Utilice un hilo para operaciones a granel.
  • Compartir una instancia de SQLiteDatabase para cada DB en disco entre hilos e implementar un sistema de conteo para realizar un seguimiento de las conexiones abiertas.

¿Existen mejores prácticas para estos escenarios?

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

Mi solución:

Para la versión más actual, vea https://github.com/JakarCo/databasemanager pero intentaré mantener el código actualizado aquí también. Si quieres entender mi solución, mira el código y lee mis notas. Mis notas suelen ser bastante útiles.

  1. Copie / pegue el código en un nuevo archivo denominado DatabaseManager . (O descargarlo de github)
  2. Extender DatabaseManager e implementar onCreate y onUpgrade como lo haría normalmente. Puede crear varias subclases de una clase DatabaseManager para tener diferentes bases de datos en disco.
  3. Instanciar su subclase y llamar a getDb() para utilizar la clase SQLiteDatabase .
  4. Call 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 [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(); } } } } 

La base de datos es muy flexible con multi-threading. Mis aplicaciones golpean sus DBs de muchos hilos diferentes simultáneamente y hace muy bien. En algunos casos, tengo varios procesos de golpear el DB simultáneamente y que funciona bien también.

Tus tareas asíncronas: usa la misma conexión cuando puedas, pero si tienes 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 para mi caso, invocaría una excepción de puntero nulo de otra manera, por ejemplo, getWritableDatabase aún no devuelto en un subproceso 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 multihebra, 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 subprocesos / procesos (también) comienzan a utilizar 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 un hilo, Android gestiona el bloqueo a través de diferentes subprocesos y permite sólo un hilo a la vez para tener acceso a la actualización exclusiva.

También puede hacer "Lee" de la base de datos y utilizar el mismo objeto SQLiteDatabase en un hilo diferente (mientras 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 " Escribir hilo "confirma los datos aunque ambos usan el mismo objeto SQLiteDatabase.

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

En mi aplicación empresarial, intento utilizar comprobaciones condicionales para que el subproceso de interfaz de usuario nunca tenga que esperar, mientras que el subproceso BG contiene el objeto SQLiteDatabase (exclusivamente). Trato de predecir las acciones de la interfaz de usuario y aplazar el hilo BG de ejecutar para 'x' segundos. También se puede mantener PriorityQueue para administrar la distribución de objetos de conexión de SQLiteDatabase para que el subproceso de interfaz de usuario lo obtenga primero.

Después de luchar con esto durante un par de horas, he encontrado que sólo se puede utilizar un objeto de ayuda db por ejecución 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 anexo 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 DBAdapter nuevo cada vez que el bucle itera era la única manera que podría conseguir mis secuencias en una base de datos con mi clase del ayudante.

Habiendo tenido algunos problemas, creo que he entendido por qué me he equivocado.

Había escrito una clase de contenedor de base de datos que incluía un close() que llamaba al ayudante cerrar como un espejo de open() que llamaba getWriteableDatabase y luego había migrado a un ContentProvider . El modelo para ContentProvider no utiliza SQLiteDatabase.close() que creo que es una pista grande como el código utiliza getWriteableDatabase En algunos casos seguía haciendo acceso directo (las consultas de validación de la pantalla en el principal por lo que migró a un modelo getWriteableDatabase / rawQuery .

Yo uso un singleton y hay el comentario ligeramente ominoso en la documentación de cierre

Cierre cualquier objeto de base de datos abierto

(Mi negrita).

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

Así que creo que close() obliga a cerrar la base de datos independientemente de cualquier otro hilo que contenga referencias – tan close() sí mismo no es simplemente deshacer la correspondencia getWriteableDatabase sino obligar a cerrar cualquier solicitud abierta. La mayoría de las veces esto no es un problema, ya que el código es un solo subproceso, pero en casos multi-threaded siempre existe la posibilidad de abrir y cerrar fuera de sincronización.

Después de leer los comentarios en otra parte que explica que la instancia de código SqLiteDatabaseHelper cuenta, entonces la única vez que desea un cierre es donde desea que la situación donde desea hacer una copia de seguridad, y desea forzar todas las conexiones a cerrarse y forzar SqLite a Escriba las cosas en caché que podrían estar merodeando, en otras palabras, detenga toda la actividad de la base de datos de la aplicación, cierre en caso de que el Ayudante haya perdido la pista, realice cualquier actividad de nivel de archivo (copia de seguridad / restauración) y comience de nuevo.

A pesar de que suena como una buena idea tratar de cerrar de manera controlada, la realidad es que Android se reserva el derecho a la basura de su máquina virtual por lo que cualquier cierre está reduciendo el riesgo de actualizaciones en caché no se está escribiendo, pero no se puede garantizar si el dispositivo Y si ha liberado correctamente sus cursores y referencias a las bases de datos (que no deberían ser miembros estáticos), entonces el ayudante habrá cerrado la base de datos de todos modos.

Así que mi opinión es que el enfoque es:

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

Nunca llame directamente a cerrar.

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

Si está haciendo el manejo de nivel de archivo, detener toda la actividad de la base de datos y luego llamar a cerrar en caso de que haya un hilo fuera de control en la suposición de que escribir transacciones adecuadas para que el hilo fugitivo fallará y la base de datos cerrada por lo menos Que potencialmente una copia de nivel de archivo de una transacción parcial.

Puede intentar aplicar un nuevo enfoque de arquitectura anunciado en Google I / O 2017.

También incluye la nueva biblioteca de 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<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(); } 
  • Convertir archivo de base de datos .db en .csv
  • Instalación y configuración de SQlite Browser en LINUX
  • Cómo borrar un Android ListView y llenarlo con nuevos datos?
  • Sqlite3_open_v2 ("/ data / data / com.android.packagename / databases / dump.sqlite", & handle, 1, NULL) falló
  • Android usando SQLCipher - ¿cómo se puede descifrar?
  • Cómo escribir una base de datos en un archivo de texto en android
  • Ejecución de varias sentencias con SQLiteDatabase.execSQL
  • Cómo escribir contiene consulta en SQLite fts3 fulltext search
  • Actualizar la base de datos Sqlite en android?
  • Recuperación del valor nulo de la base de datos Sqlite en android
  • Filtrar el parámetro ListView por 'selección' en CursorLoader
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.