Room数据库同表多对多关联嵌套POJO引发StackOverflowError求助
解决Room递归嵌套实体导致的StackOverflowError
问题场景
我维护一套产品数据库结构:产品表可关联备件列表,每个产品的用户手册存储在附件表中。使用Room定义实体和关联POJO时,在PopulatedProduct类中通过@Relation将备件声明为List<PopulatedProduct>,编译时触发java.lang.StackOverflowError。若将备件类型替换为ProductEntity可正常编译,但无法获取备件关联的用户手册,需要保留嵌套的完整信息。
相关实体及POJO代码如下:
产品实体类
@Entity( tableName = "products", foreignKeys = [ ForeignKey( entity = AttachmentEntity::class, parentColumns = ["id"], childColumns = ["attachment_id"], onDelete = ForeignKey.CASCADE ), ] ) data class ProductEntity( @PrimaryKey val id: Int, @ColumnInfo(name = "attachment_id") val attachmentId: Int, )
附件实体类
@Entity( tableName = "attachment" ) data class AttachmentEntity( @PrimaryKey val id: Int )
产品关联表
@Entity( tableName = "products_products", primaryKeys = ["product_id1", "product_id2"], foreignKeys = [ ForeignKey( entity = ProductEntity::class, parentColumns = ["id"], childColumns = ["product_id1"], onDelete = ForeignKey.CASCADE ), ForeignKey( entity = ProductEntity::class, parentColumns = ["id"], childColumns = ["product_id2"], onDelete = ForeignKey.CASCADE ), ] ) data class ProductProductCrossRef( @ColumnInfo(name = "product_id1") val productId1: Int, @ColumnInfo(name = "product_id2") val productId2: Int, )
报错的POJO类
data class PopulatedProduct( @Embedded val entity: ProductEntity, @Relation( parentColumn = "attachment_id", entityColumn = "id" ) val userManual: AttachmentEntity?, @Relation( entity = ProductEntity::class, parentColumn = "id", entityColumn = "id", associateBy = Junction( value = ProductProductCrossRef::class, parentColumn = "product_id1", entityColumn = "product_id2", ) ) val spareparts: List<PopulatedProduct> // 此处触发StackOverflowError )
问题原因
Room的@Relation注解不支持递归嵌套实体(即实体/POJO中包含自身类型的关联字段)。编译阶段Room会尝试解析嵌套结构,无限递归生成查询逻辑,最终超出栈内存限制,抛出StackOverflowError。
解决方案
方案1:拆分POJO类,避免递归引用
创建两个层级的POJO,将产品+手册的基础信息与备件关联拆分,从根源避免递归:
// 基础POJO:封装产品实体+用户手册 data class ProductWithManual( @Embedded val entity: ProductEntity, @Relation( parentColumn = "attachment_id", entityColumn = "id" ) val userManual: AttachmentEntity? ) // 扩展POJO:基础信息+备件列表(备件使用基础POJO) data class PopulatedProduct( @Embedded val productWithManual: ProductWithManual, @Relation( entity = ProductEntity::class, parentColumn = "id", entityColumn = "id", associateBy = Junction( value = ProductProductCrossRef::class, parentColumn = "product_id1", entityColumn = "product_id2" ) ) val spareparts: List<ProductWithManual> )
这种方案既保留了备件的用户手册信息,又避免了递归引用,编译和运行都能正常工作。
方案2:@Transaction手动实现递归查询(支持深层嵌套)
如果需要支持多层级备件嵌套(比如备件自身也包含备件),可以在Dao中通过@Transaction手动编写递归查询逻辑,完全控制查询流程:
@Dao interface ProductDao { @Transaction suspend fun getPopulatedProduct(productId: Int, maxDepth: Int = 3): PopulatedProduct? { // 限制递归深度,避免运行时栈溢出 if (maxDepth <= 0) return null // 查询当前产品及手册 val product = getProductWithManual(productId) ?: return null // 查询当前产品的备件ID列表 val sparePartIds = getSparePartIds(productId) // 递归查询每个备件的完整信息,深度减1 val spareParts = sparePartIds.mapNotNull { getPopulatedProduct(it, maxDepth - 1) } return PopulatedProduct(product.entity, product.userManual, spareParts) } // 基础查询:获取产品+用户手册 @Query("SELECT * FROM products WHERE id = :productId") suspend fun getProductWithManual(productId: Int): ProductWithManual? // 查询指定产品的备件ID列表 @Query("SELECT product_id2 FROM products_products WHERE product_id1 = :productId") suspend fun getSparePartIds(productId: Int): List<Int> }
注意:必须添加递归深度限制,防止运行时因无限递归触发栈溢出。
内容的提问来源于stack exchange,提问作者pfontain
相关产品推荐
相关产品推荐

