Registro de depuración DAO de la base de datos de habitaciones de Android

10 minutos de lectura

Dada una base de datos de habitaciones DAO como esta:

import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;

import java.util.Date;
import java.util.List;

@Dao
public interface MyDao {

    @Query("SELECT * FROM MyTable")
    List<MyItem> all();

    @Query("SELECT * FROM MyTable WHERE date = :date AND language = :language")
    MyItem byDate(Date date, String language);


}

¿Hay alguna manera de tener un registrador o algo así agregado a MyDao para poder ver qué declaraciones se están realizando. Esto sería realmente útil durante el desarrollo, porque podría verificar de inmediato si las funciones se transforman correctamente a la declaración SQL esperada o no.

  • ¿Has encontrado alguna solución?

    – Mehul Joisar

    20 de octubre de 2018 a las 8:37

  • @MehulJoisar He publicado mi respuesta a continuación, funcionó para mí. Podría ayudarte.

    – Dinesh Singh

    9 dic 2018 a las 15:33

avatar de usuario
alex lipov

Suponiendo que Room utilice Sqlite del marco como base de datos subyacente, las declaraciones se pueden registrar de forma muy sencilla. La única limitación: esto se puede hacer sólo en emulador.

De SQLiteDebug.java:

/**
 * Controls the printing of SQL statements as they are executed.
 *
 * Enable using "adb shell setprop log.tag.SQLiteStatements VERBOSE".
 */
public static final boolean DEBUG_SQL_STATEMENTS =
        Log.isLoggable("SQLiteStatements", Log.VERBOSE);  

Por defecto, el log.tag.SQLiteStatementsEl valor de no está establecido:

alex@mbpro:~$ adb shell getprop log.tag.SQLiteStatements

De acuerdo con la documentación anterior, para establecer la propiedad tenemos que usar:

alex@mbpro:~$ adb shell setprop log.tag.SQLiteStatements VERBOSE

alex@mbpro:~$ adb shell getprop log.tag.SQLiteStatements

VERBOSO

Como podemos ver, el VERBOSE el valor se estableció con éxito. Sin embargo, si volvemos a ejecutar nuestra aplicación, no veremos esas declaraciones impresas. Para que funcione, tendremos que reiniciar todos los servicios usando adb shell stop y entonces adb shell start.
Si intenta hacer eso con un dispositivo normal, recibirá el siguiente error (probado con Pixel XL / stock Android 9):

alex@mbpro:~$ inicio de shell adb

inicio: debe ser root
alex@mbpro:~$ adb raíz

adbd no puede ejecutarse como root en compilaciones de producción

Es por esto que tenemos que usar el emulador:

alex@mbpro:~$ adb raíz

reiniciar adbd como root
alex@mbpro:~$ parada de shell adb

alex@mbpro:~$ inicio de shell adb

El emulador se reiniciará.
Ejecute su aplicación y verá declaraciones Sqlite similares en logcat:

<redacted..>
V/SQLiteStatements: <redacted>/my_db: "BEGIN EXCLUSIVE;"
V/SQLiteStatements: <redacted>/my_db: "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)"
V/SQLiteStatements: <redacted>/my_db: "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, "3cb5664b6da264c13388292d98141843")"
V/SQLiteStatements: <redacted>/my_db: "CREATE TABLE IF NOT EXISTS `MyTable` (`id` TEXT NOT NULL, `date` INTEGER, `language` TEXT, PRIMARY KEY(`id`))"
<redacted..>
V/SQLiteStatements: <redacted>/my_db: "BEGIN EXCLUSIVE;"
V/SQLiteStatements: <redacted>/my_db: "PRAGMA temp_store = MEMORY;"
V/SQLiteStatements: <redacted>/my_db: "PRAGMA recursive_triggers="ON";"
V/SQLiteStatements: <redacted>/my_db: "CREATE TEMP TABLE room_table_modification_log(version INTEGER PRIMARY KEY AUTOINCREMENT, table_id INTEGER)"
V/SQLiteStatements: <redacted>/my_db: "COMMIT;"
<redacted..>
V/SQLiteStatements: <redacted>/my_db: "SELECT * FROM MyTable"
V/SQLiteStatements: <redacted>/my_db: "SELECT * FROM MyTable WHERE date = 1551562171387 AND language="en""  

Para deshacer los cambios, use estos comandos:

alex@mbpro:~$ adb shell setprop registro.etiqueta.SQLiteStatements “”

alex@mbpro:~$ adb shell getprop log.tag.SQLiteStatements


alex@mbpro:~$ parada de shell adb

alex@mbpro:~$ inicio de shell adb

alex@mbpro:~$ adb desrootear

reiniciar adbd como no root

  • Esto fue extremadamente útil para mí. Gracias.

    – Josué W.

    8 de agosto de 2019 a las 13:54

  • El método booleano que pones al comienzo de tu respuesta. ¿Adónde va este código?

    – AndroidDev123

    23 de julio de 2020 a las 10:15

  • @ AndroidDev123 Supongo que te refieres a DEBUG_SQL_STATEMENTS. No lo llama directamente (el marco lo hace), lo agregué solo como una referencia al comentario escrito arriba.

    – Alex Lipov

    23 de julio de 2020 a las 10:19

  • @ AndroidDev123 Debe usar una imagen del sistema AOSP que permita privilegios elevados, según esta descripción.

    – Alex Lipov

    23 de julio de 2020 a las 11:37


  • Esto es genial. Exactamente lo que estaba buscando. ¡Ahora puedo depurar todas las operaciones de la base de datos con precisión! ¡¡¡¡¡Gracias!!!!!

    – gHombre

    31 de julio de 2020 a las 5:01


A partir de Habitación 2.3.0-alpha04 (lanzado el 16 de diciembre de 2020, podría ser estable para cuando esté leyendo esto), hay soporte directo en Room para registrar consultas SQL con el nuevo RoomDatabase.QueryCallback

Establece esta devolución de llamada en el RoomDatabase.Builder

    fun getDatabase(context: Context): MyDatabase {
        val dbBuilder = Room.databaseBuilder(
            context.applicationContext,
            MyDatabase::class.java, "mydatabase.db"
        )
        dbBuilder.setQueryCallback(RoomDatabase.QueryCallback { sqlQuery, bindArgs ->
            println("SQL Query: $sqlQuery SQL Args: $bindArgs")
        }, Executors.newSingleThreadExecutor())
        return dbBuilder.build()
    }

Tenga en cuenta que este es solo un código de ejemplo y probablemente debería asegurarse MyDatabase es un singleton en su aplicación. Otro consejo es solo registrar consultas cuando la aplicación está DEBUG:
if (BuildConfig.DEBUG) dbBuilder.setQueryCallback(… y el resto del código de arriba.

Comenta si alguien quiere el código de ejemplo en Java

  • En caso de que decida utilizar el método Log.xxx, tenga en cuenta que existe un límite máximo de longitud de caracteres para el mensaje. Entonces, si le faltan algunas de las consultas en su Logcat, intente dividir el sqlQuery en varias entradas de registro. (Algunos ejemplos están aquí [stackoverflow.com/questions/8888654/…)

    – Almighty

    May 22, 2021 at 6:47

  • It’s in stable release 2.3.0 now [developer.android.com/jetpack/androidx/releases/….

    – Myroslav

    Aug 5, 2021 at 6:36


  • @georgiecasey, could you please help to share the Java code? Thanks in advance.

    – g g

    Sep 18, 2021 at 6:42

  • @georgiecasey, I’ve been able to make it work with Java code. But looks it’s all placeholders “?” in the SQL. Is there anyway to display the actually executed SQL? The “?” has to be specific value.

    – g g

    Sep 18, 2021 at 12:12

There does not appear to be any hooks for that at the DAO level. There are callbacks related to database opens and upgrades, but not arbitrary stuff.

You could file a feature request, though. I agree that it could be useful. Even better would be an OkHttp-style generic interceptor framework.

  • Since there didn’t seem to be any other feature request: issuetracker.google.com/issues/74877608

    – fast3r

    Mar 15, 2018 at 11:36

  • it seems the feature is still not implemented. Any other workaround to get logs?

    – Mehul Joisar

    Oct 20, 2018 at 8:38

  • @MehulJoisar: Yiğit had an interesting angle in issuetracker.google.com/issues/74877608#comment4, which is to write a series of support database classes that would handle the logging. I do not know of anyone who has done that for logging, though.

    – CommonsWare

    Oct 20, 2018 at 11:49

  • @CommonsWare how can I use that in my application? please share some thoughts.

    – Mehul Joisar

    Oct 27, 2018 at 15:55

  • @MehulJoisar: Instructions for how to create a custom set of support database API classes is well beyond the scope of a Stack Overflow answer, let alone a comment. In a nutshell, you need to create implementations of several interfaces (e.g., SupportSQLiteDatabase), wire them together, and have a SupportSQLiteOpenHelper.Factory to be able to apply them to a RoomDatabase.Builder. I do this in CWAC-SafeRoom, in my case to support SQLCipher.

    – CommonsWare

    Oct 27, 2018 at 16:06

As per document of Room it performs compile time check so if your SQL statement is not valid compilation itself failed and proper error message is displayed in the log.

Also generated code is debuggable by default and can be found under below mentioned path.

build > generated > source > apt > your Package > yourDao_Impl.java

This class contains implementation of your DAO you can debug this class as you debug other classes in your project. 🙂

Example :

enter image description here

When I have got some unknown error while inserting or updating row in room db Android does not show any error in debug console. One thing I found how to check whats happen while debug is:

try { someSource.update(someRow) } catch (e: Throwable) { println(e.message) }

Output is:

UNIQUE constraint failed: quiz.theme (code 2067)

  • In which place? ViewModel / Repo etc !!

    – Abhiroop Nandi Ray

    Aug 6, 2018 at 12:04

  • @Abhiroop Nandi Ray, It’s only for debug, in place where you want. If you use this code: try { yourDao.getAllRowsFromDB() } catch … and get some exception it’ll be caught in catch block.

    – bitvale

    Aug 6, 2018 at 12:33


  • Nice approach, saved my day!

    – moallemi

    May 15, 2019 at 6:09

I have been able to achieve it via a hack for Select queries. This wont work for insert/update/delete operations 🙂

Create a separate class RoomLoggingHelper as follows

import android.annotation.SuppressLint
import androidx.room.RoomSQLiteQuery

private const val NULL = 1
private const val LONG = 2
private const val DOUBLE = 3
private const val STRING = 4
private const val BLOB = 5
private const val NULL_QUERY = "NULL"

const val ROOM_LOGGING_TAG = "roomQueryLog"

object RoomLoggingHelper {

    @SuppressLint("RestrictedApi")
    fun getStringSql(query: RoomSQLiteQuery): String {
        val argList = arrayListOf<String>()
        val bindingTypes = query.getBindingTypes()
        var i = 0

        while (i < bindingTypes.size) {
            val bindingType = bindingTypes[i]

            cuando (bindingType) { NULL -> argList.add(NULL_QUERY) LONG -> argList.add(query.getLongBindings()[i].toString()) DOBLE -> argList.add(query.getDoubleBindings()[i].toString()) CADENA -> argList.add(consulta.getStringBindings()[i].toString()) } i++ } return String.format(query.sql.replace("?", "%s"), *argList.toArray()) } fun getStringSql(query: String?, args: Array?): Cadena?  { return if (consulta != nulo && argumentos != nulo) { String.format(consulta.reemplazar("?", "%s"), *argumentos) } else "" } } diversión privada RoomSQLiteQuery.getBindingTypes(): IntArray { return javaClass.getDeclaredField("mBindingTypes").let { field -> field.isAccessible = true return@let field.get(this) as IntArray } } diversión privada RoomSQLiteQuery.getLongBindings(): LongArray { return javaClass.getDeclaredField( "mLongBindings").let { field -> field.isAccessible = true return@let field.get(this) as LongArray } } diversión privada RoomSQLiteQuery.getStringBindings(): Array { return javaClass.getDeclaredField("mStringBindings") .let { field -> field.isAccessible = true return@let field.get(this) as Array } } diversión privada RoomSQLiteQuery.getDoubleBindings(): DoubleArray { return javaClass.getDeclaredField("mDoubleBindings").let { campo -> field.isAccessible = true return@let field.get(this) as DoubleArray } } diversión privada RoomSQLiteQuery.getIntBindings(): IntArray { return javaClass.getDeclaredField("mBi  ndingTypes").let { field -> field.isAccessible = true return@let field.get(this) as IntArray } }

O bien, puede descargar este archivo desde aquí

Agregue este archivo a su Proyecto y llámelo desde su clase Room Database de la siguiente manera: Anule ambos query métodos como este

override fun query(query: SupportSQLiteQuery?): Cursor {
        //This will give you the SQL String
        val queryString = RoomLoggingHelper.getStringSql(query as RoomSQLiteQuery)
        //You can log it in a way you like, I am using Timber
        Timber.d("$ROOM_LOGGING_TAG $queryString")
        return super.query(query)
    }

    override fun query(query: String?, args: Array<out Any>?): Cursor {
        //This will give you the SQL String
        val queryString = RoomLoggingHelper.getStringSql(query, args)
        //You can log it in a way you like, I am using Timber
        Timber.d("$ROOM_LOGGING_TAG $queryString")
        return super.query(query, args)
    }

Descargos de responsabilidad:

  • Estoy usando Reflection para obtener una cadena SQL, por lo tanto use esto SOLO en modo DEBUG
  • Esto está escrito con prisa y puede contener errores, sería prudente mantenerlo en try-catch bloquear
  • Además, lo he probado para argumentos de cadena, debería funcionar para long y double también, no funcionará para Blobs

  • ¿En qué lugar? ViewModel / Repo, etc !!

    – Abhiroop Nandi Ray

    6 de agosto de 2018 a las 12:04

  • @Abhiroop Nandi Ray, es solo para depurar, en el lugar que desee. Si usa este código: intente { yourDao.getAllRowsFromDB() } catch … y obtenga alguna excepción, quedará atrapado en el bloque catch.

    – bitvale

    6 de agosto de 2018 a las 12:33


  • ¡Buen enfoque, me salvó el día!

    – moallemi

    15 de mayo de 2019 a las 6:09

¿Ha sido útil esta solución?