Android Room 数据库 DAO 调试日志

Posted

技术标签:

【中文标题】Android Room 数据库 DAO 调试日志【英文标题】:Android Room Database DAO debug log 【发布时间】:2018-02-20 16:22:15 【问题描述】:

给定这样的 Room 数据库 DAO:

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



有没有办法将记录器或类似的东西添加到MyDao,以便我可以看到正在执行哪些语句。这在开发过程中非常有用,因为我可以立即检查函数是否正确转换为预期的 SQL 语句。

【问题讨论】:

你找到解决办法了吗? @MehulJoisar 我已经在下面发布了我的答案,它对我有用。可能会帮到你。 【参考方案1】:

假设 Room 使用框架的 Sqlite 作为底层数据库,可以很简单地记录语句。唯一的限制:这只能在 emulator 上完成。

来自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);  

默认情况下,log.tag.SQLiteStatements 的值未设置:

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

根据上面的文档,设置我们必须使用的属性:

alex@mbpro:~$ adb shell setprop log.tag.SQLiteStatements VERBOSE alex@mbpro:~$ adb shell getprop log.tag.SQLiteStatements 详细的

如我们所见,VERBOSE 值已成功设置。但是,如果我们重新运行我们的应用程序 - 我们将不会看到打印这些语句。为了使它工作,我们必须使用restart all the services adb shell stop 然后adb shell start。 如果您尝试使用普通设备执行此操作,您将收到以下错误(已尝试使用 Pixel XL/stock Android 9):

alex@mbpro:~$ adb shell 启动 开始:必须是根 alex@mbpro:~$ adb root adbd 无法在生产版本中以 root 身份运行

这就是我们必须使用模拟器的原因:

alex@mbpro:~$ adb root 以 root 身份重新启动 adbd alex@mbpro:~$ adb shell 停止 alex@mbpro:~$ adb shell 启动

模拟器将重新启动。 运行你的应用程序,你会在 logcat 中看到类似的 Sqlite 语句:

<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'"  

要撤消更改,请使用以下命令:

alex@mbpro:~$ adb shell setprop log.tag.SQLiteStatements "" alex@mbpro:~$ adb shell getprop log.tag.SQLiteStatements alex@mbpro:~$ adb shell 停止 alex@mbpro:~$ adb shell 启动 alex@mbpro:~$ adb unroot 以非 root 身份重新启动 adbd

【讨论】:

这对我非常有帮助。谢谢。 您在答案开头放置的布尔方法。这段代码去哪儿了? @AndroidDev123 我猜你指的是DEBUG_SQL_STATEMENTS。你不直接调用它(框架会),我添加它只是作为对上面写的评论的引用。 @AndroidDev123 根据this description,您应该使用允许提升权限的 AOSP 系统映像。 这很棒。正是我想要的。我现在可以精确地调试所有数据库操作!谢谢!!!!!!【参考方案2】:

在 DAO 级别似乎没有任何挂钩。有与数据库打开和升级相关的回调,但不是任意的。

不过,您可以file a feature request。我同意它可能有用。更好的是 OkHttp 风格的通用拦截器框架。

【讨论】:

由于似乎没有任何其他功能请求:issuetracker.google.com/issues/74877608 该功能似乎仍未实现。获取日志的任何其他解决方法? @MehulJoisar:Yiğit 在issuetracker.google.com/issues/74877608#comment4 中有一个有趣的角度,那就是编写一系列支持数据库类来处理日志记录。不过,我不知道有谁这样做过。 @CommonsWare 如何在我的应用程序中使用它?请分享一些想法。 @MehulJoisar:关于如何创建一组自定义支持数据库 API 类的说明远远超出了 Stack Overflow 答案的范围,更不用说评论了。简而言之,您需要创建多个接口的实现(例如,SupportSQLiteDatabase),将它们连接在一起,并拥有一个SupportSQLiteOpenHelper.Factory 以便能够将它们应用于RoomDatabase.Builder。我在CWAC-SafeRoom 中执行此操作,以支持 SQLCipher。【参考方案3】:

根据 Room 的document,它会执行编译时检查,因此如果您的 SQL 语句无效,则编译本身会失败并在日志中显示正确的错误消息。

默认情况下生成的代码也是可调试的,可以在下面提到的路径下找到。

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

此类包含 DAO 的实现,您可以在调试项目中的其他类时调试此类。 :-)

例子:

【讨论】:

感谢您的回复。我自己在调试时看到了_Impl,但我找不到一个完整的字符串,其中所有占位符都已经填充了参数。也许我错过了? @IHeartAndroid 有没有找到提供日志的解决方案?【参考方案4】:

截至Room 2.3.0-alpha04(2020 年 12 月 16 日发布,在您阅读本文时可能已经稳定),Room 直接支持使用新的RoomDatabase.QueryCallback 记录 SQL 查询

你在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()
    

请注意,这只是示例代码,您可能应该确保 MyDatabase 在您的应用程序中是一个单例。另一个提示是仅在应用程序处于调试状态时记录查询: if (BuildConfig.DEBUG) dbBuilder.setQueryCallback(... 以及上面的其余代码。

如果有人想要 Java 中的示例代码,请发表评论

【讨论】:

如果您决定使用 Log.xxx 方法,请记住,消息有最大字符长度限制。因此,如果您缺少 Logcat 中的某些查询,请尝试将 sqlQuery 拆分为多个日志条目。 (这里有一些例子 [***.com/questions/8888654/…) 现在是稳定版2.3.0 [developer.android.com/jetpack/androidx/releases/…. @georgiecasey,你能帮忙分享一下Java代码吗?提前致谢。 @georgiecasey,我已经能够使其与 Java 代码一起使用。但看起来都是占位符“?”在 SQL 中。反正有没有显示实际执行的SQL?这 ”?”必须是特定的值。【参考方案5】:

当我在房间数据库中插入或更新行时遇到一些未知错误时,Android 不会在调试控制台中显示任何错误。我发现如何检查调试时发生的一件事是:

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

输出是:

UNIQUE 约束失败:quiz.theme(代码 2067)

【讨论】:

在哪个地方? ViewModel / Repo 等!! @Abhiroop Nandi Ray,它只用于调试,在你想要的地方。如果您使用此代码:尝试 yourDao.getAllRowsFromDB() catch ... 并得到一些异常,它将在 catch 块中被捕获。 不错的方法,拯救了我的一天!【参考方案6】:

我已经能够通过对 Select 查询的 hack 来实现它。这不适用于插入/更新/删除操作:)

创建一个单独的类RoomLoggingHelper如下

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]

            when (bindingType) 
                NULL -> argList.add(NULL_QUERY)
                LONG -> argList.add(query.getLongBindings()[i].toString())
                DOUBLE -> argList.add(query.getDoubleBindings()[i].toString())
                STRING -> argList.add(query.getStringBindings()[i].toString())
            
            i++
        

        return String.format(query.sql.replace("?", "%s"), *argList.toArray())
    

    fun getStringSql(query: String?, args: Array<out Any>?): String? 
        return if (query != null && args != null) 
            String.format(query.replace("?", "%s"), *args)
         else
            ""
    


private fun RoomSQLiteQuery.getBindingTypes(): IntArray 

    return javaClass.getDeclaredField("mBindingTypes").let  field ->
        field.isAccessible = true
        return@let field.get(this) as IntArray
    


private fun RoomSQLiteQuery.getLongBindings(): LongArray 

    return javaClass.getDeclaredField("mLongBindings").let  field ->
        field.isAccessible = true
        return@let field.get(this) as LongArray
    


private fun RoomSQLiteQuery.getStringBindings(): Array<String> 

    return javaClass.getDeclaredField("mStringBindings").let  field ->
        field.isAccessible = true
        return@let field.get(this) as Array<String>
    


private fun RoomSQLiteQuery.getDoubleBindings(): DoubleArray 

    return javaClass.getDeclaredField("mDoubleBindings").let  field ->
        field.isAccessible = true
        return@let field.get(this) as DoubleArray
    


private fun RoomSQLiteQuery.getIntBindings(): IntArray 

    return javaClass.getDeclaredField("mBindingTypes").let  field ->
        field.isAccessible = true
        return@let field.get(this) as IntArray
    

或者,您可以从here下载此文件

将此文件添加到您的项目并从您的房间数据库类中调用它,如下所示: 像这样覆盖query 方法

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)
    

免责声明:

我正在使用反射来获取字符串 SQL,因此 仅在调试模式下使用它 这是仓促写的,可能包含错误,最好将其保留在try-catch 块中 另外,我已经针对字符串 args 对其进行了测试,应该也可以使用 long 和 double,但不适用于 Blobs

【讨论】:

我会检查一下,它是否也可以用于生产? @MehulJoisar 您应该只在调试版本中使用它,而不是在生产版本中使用它,因为我正在使用反射,它可能会影响性能。您可以点击此链接了解更多详情:***.com/questions/23844667/… 我正在使用@DineshSingh 代码示例,但是我的自定义查询有问题,所以我进行了以下更改:if (argList.size == 0) return; query.sql var sql = query.sql argList.forEach sql = sql.replaceFirst("?", it) return sql 而不是return String.format(query.sql.replace("?", "%s"), *argList.toArray())。要知道您是否在Debug Mode,可以使用以下语句:if (BuildConfig.DEBUG) val queryString = RoomLoggingHelper.getStringSql(query, args) Log.d("Room", queryString) return super.query(query)

以上是关于Android Room 数据库 DAO 调试日志的主要内容,如果未能解决你的问题,请参考以下文章

Android Room Dao:按 CASE 排序不起作用

Android Room 通用 DAO

Android Kotlin 中的 ROOM 数据库出错

android----Room数据库的简单操作

Android Jetpack ROOM 的Dao返回LiveData<Bean>封装及Bean普通的区别

Android jetpack room 记录数据库升级日志