You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

添加Stat子实体时触发java.lang.IllegalStateException空值异常求助

问题排查与解决方案

问题描述

Android应用中,用户拥有多个Stat实体,添加新Stat时偶尔随机抛出以下异常:

java.lang.IllegalStateException: Required value was null.

完整错误栈:

FATAL EXCEPTION: DefaultDispatcher-worker-3
Process: com.apps.irlleveler, PID: 15274                    
java.lang.IllegalStateException: Required value was null.
at com.apps.irlleveler.data.db.UserDao_Impl.__fetchRelationshipStatAscomAppsIrllevelerDataStatWithTasks(UserDao_Impl.kt:273)
at com.apps.irlleveler.data.db.UserDao_Impl.getUserWithStatsWithTasks$lambda$1(UserDao_Impl.kt:83)
at com.apps.irlleveler.data.db.UserDao_Impl.$r8$lambda$f2zhaSZJKsl9DSGFGRABt37Yfa0(Unknown Source:0)
at com.apps.irlleveler.data.db.UserDao_Impl$$ExternalSyntheticLambda2.invoke(D8$$SyntheticClass:0)
at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invokeSuspend(DBUtil.kt:68)
at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:8)
at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$lambda$1$$inlined$internalPerform$1.invoke(Unknown Source:4)
at androidx.room.driver.SupportSQLiteConnectionPool.useConnection(SupportSQLiteConnectionPool.android.kt:42)
at androidx.room.RoomConnectionManager.useConnection(RoomConnectionManager.android.kt:126)
at androidx.room.RoomDatabase.useConnection$room_runtime_release(RoomDatabase.android.kt:593)
at androidx.room.util.DBUtil__DBUtil_androidKt$performSuspending$$inlined$compatCoroutineExecute$DBUtil__DBUtil_androidKt$1.invokeSuspend(DBUtil.android.kt:113)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@79f54d6, Dispatchers.IO]

推测是插入Stat时无法找到关联User实体导致,但实际根源不止于此。

核心问题分析

  • Stat实体设计错误
    • Stat类中把User对象作为@ColumnInfo字段,Room无法直接持久化复杂对象,会导致序列化/反序列化异常,最终关联查询时出现null
    • owningUserId定义为Long,但User的id是Int,类型不匹配,Room关联查询时无法正确匹配
    • owningUserId的默认值依赖owner.id,但插入时如果owner未正确初始化,会导致该值为null或无效
  • Flow使用不当导致竞态条件
    • loadUser方法中,先获取Flow再调用first(),然后插入新用户后重新获取Flow,这种方式可能导致并发场景下数据不一致,出现User还未插入完成就执行关联查询的情况
  • 关联查询的非空约束
    • UserWithStatsWithTheStatsTasks中的user字段是非空的,但getUserWithStatsWithTasks()返回的是Flow<UserWithStatsWithTheStatsTasks?>,当查询不到User时会返回null,但后续转换UI模型时未做充分判空

解决方案

1. 修复Stat实体结构

移除owner字段,改用owningUserId作为外键关联User,确保类型一致:

@Entity(
    foreignKeys = [ForeignKey(
        entity = User::class,
        parentColumns = ["id"],
        childColumns = ["owningUserId"],
        onDelete = ForeignKey.CASCADE,
        onUpdate = ForeignKey.CASCADE
    )]
)
data class Stat (
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo val name: String,
    @ColumnInfo val description: String = "",
    @ColumnInfo val level: Int = 1,
    @ColumnInfo val totalXP: Int = 0,
    @ColumnInfo val currentXP: Int = 0,
    @ColumnInfo val neededXP: Int = 1,
    @ColumnInfo val owningUserId: Int = 0 // 和User的id类型保持一致
)

2. 修正UserDao的查询逻辑

用@Transaction确保关联查询的原子性:

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User): Long

    @Transaction
    @Query("SELECT * FROM user WHERE id = 0")
    fun getUserWithStatsWithTasks(): Flow<UserWithStatsWithTheStatsTasks?>
}

3. 修复ViewModel中的竞态问题

优化loadUser方法,避免重复获取Flow,确保User插入完成后再更新UI:

private fun loadUser() {
    viewModelScope.launch(Dispatchers.IO) {
        val userFlow = userDao.getUserWithStatsWithTasks()
        val initialUser = userFlow.firstOrNull()
        
        if (initialUser?.user == null) {
            val newUser = User(username = "Player")
            userDao.insertUser(newUser)
            // 插入后直接收集最新数据
            userFlow.firstOrNull()?.let {
                _uiUser.value = it.toUIModel()
            }
        } else {
            _uiUser.value = initialUser.toUIModel()
        }
    }
}

4. 确保外键约束生效

在Room数据库构建时直接启用外键约束:

instance = Room.databaseBuilder(
    context, AppDatabase::class.java, NAME
)
    .allowMainThreadQueries() // 建议仅调试用,正式版移除
    .setForeignKeysEnforced(true) // 直接启用外键约束
    .build()

5. 修正StatViewModel的插入逻辑

插入Stat时确保传入正确的owningUserId(从当前用户获取):

// 在StatViewModel中依赖UserViewModel获取当前用户ID
class StatViewModel(private val userViewModel: UserViewModel) : ViewModel() {
    val statDao = MainApplication.appDatabase.getStatDao()

    fun insertStat(statName: String, description: String = ""){
        viewModelScope.launch(Dispatchers.IO) {
            val currentUserId = userViewModel.uiUser.value?.id ?: 0
            val stat = Stat(
                name = statName,
                description = description,
                owningUserId = currentUserId
            )
            statDao.insertStat(stat)
            // 插入后通知UserViewModel刷新数据
            userViewModel.reloadUser()
        }
    }
}

内容的提问来源于stack exchange,提问作者Nickname

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 17:37:31