使用Ktor与Exposed实现表关联CRUD时遇插入异常求助
UnsupportedNullValuesException及相关问题 首先,咱们先拆解你遇到的核心错误:io.ktor.gson.UnsupportedNullValuesException: Receiving null values is not supported,这个问题直接来自Ktor的Gson转换器——默认情况下它拒绝处理请求体中的null值,再结合你代码里的几个小问题,导致了插入失败。下面一步步帮你解决:
1. 先搞定Gson的null值处理问题
你有两个方向可以选:
方案A:让Gson允许接收null值
在Ktor的ContentNegotiation配置里,给Gson添加serializeNulls()配置:
install(ContentNegotiation) { gson { serializeNulls() // 允许序列化/反序列化null值 setLenient() // 可选,处理格式不严格的JSON } }
方案B:调整模型类字段为可空类型
如果你的业务场景中某些字段确实可能为空(比如邮箱),把模型类里的对应字段改成可空类型,同时更新Exposed表定义允许null:
// 模型类 data class LegalPerson( // ...其他字段 val email: String? // 改成可空 ) // Exposed表定义 object LegalPersons : Table("person.legal_person") { // ...其他字段 val email: Column<String?> = varchar("email", 100).nullable() // 允许null }
2. 修复代码里的明显笔误
在你的LegalPersonService.insert方法里,有个复制粘贴错误:
// 错误代码 it[email] = lp.fantasyName // 应该改成 it[email] = lp.email
这个错误不仅会导致邮箱字段被错误赋值,还可能因为lp.email如果是null(但模型类里是non-null)触发Gson的null异常。
3. 修正Phone模型的不合理字段类型
你的Phone数据类里phone字段是UUID类型,这完全不符合电话号码的存储逻辑,电话号码应该是字符串!赶紧修正:
// 模型类 data class Phone( val id: UUID, val internalId: Long, val phone: String // 改成String类型 ) // Exposed表定义 object Phones : Table("person.phone_legal_person") { val id: Column<UUID> = uuid("id").autoIncrement().primaryKey() // 添加外键关联LegalPerson,代替原来把id作为reference的不合理设计 val legalPersonId: Column<UUID> = reference("legal_person_id", LegalPersons.id) val internalId: Column<Long> = long("internal_id").autoIncrement() val phone: Column<String> = varchar("phone", 20) // 改成varchar类型 }
原来的Phones.id直接关联LegalPersons.id作为主键,意味着一个法人只能有一个手机号,如果你需要一对多的关联(一个法人多个手机号),上面的修改更合理;如果确实是一对一,也建议保留独立主键,用外键关联。
4. 修正Exposed的UUID自增配置
Exposed的uuid().autoIncrement()并不是所有数据库都支持(比如PostgreSQL需要额外配置),你可以改成手动生成UUID或者用数据库默认值:
// 手动生成UUID作为默认值 val id: Column<UUID> = uuid("id").default(UUID.randomUUID()).primaryKey() // 或者用PostgreSQL的uuid_generate_v4()函数(需要先安装uuid-ossp扩展) val id: Column<UUID> = uuid("id").default(Expression.build { "uuid_generate_v4()" }).primaryKey()
5. 完善关联查询和插入逻辑
关联查询
你的toLp方法里创建了Phone对象,但LegalPerson模型类里没有对应的字段,先给LegalPerson添加phone字段:
data class LegalPerson( // ...其他字段 val phone: Phone // 添加关联的Phone字段 )
然后修改查询逻辑,用join关联两张表:
suspend fun findAll(): List<LegalPerson> = dbQuery { LegalPersons.join(Phones, JoinType.INNER, LegalPersons.id, Phones.legalPersonId) .selectAll() .map { toLp(it) } }
关联插入
插入法人后,把生成的法人ID关联到手机号的外键字段:
suspend fun insert(lp: LegalPerson, ph: Phone) = dbQuery { // 获取插入后的法人ID val insertedLpId = LegalPersons.insert { it[id] = lp.id it[internalId] = lp.internalId it[companyId] = lp.companyId it[active] = lp.active it[tradeName] = lp.tradeName it[fantasyName] = lp.fantasyName it[email] = lp.email it[cnpj] = lp.cnpj it[stateRegistration] = lp.stateRegistration it[muninipalRegistration] = lp.muninipalRegistration it[address] = lp.address } get LegalPersons.id // 插入手机号时关联法人ID Phones.insert { it[id] = ph.id it[legalPersonId] = insertedLpId it[internalId] = ph.internalId it[phone] = ph.phone } }
按上面的步骤修改后,你的CRUD关联操作应该就能正常运行了,如果还有问题,可以检查请求体的JSON格式是否和模型类匹配,或者数据库的表结构是否和Exposed定义一致。
内容的提问来源于stack exchange,提问作者Rodrigo Batista

