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

JOOQ自定义RecordMapperProvider未生效,<EmptyString/>未替换为null

问题:Spring Boot+JOOQ+Kotlin自定义RecordMapperProvider未生效

我用Spring Boot+JOOQ+Kotlin开发Web应用,自定义了EmptyStringToNullRecordMapperProvider,意图将查询结果中的<EmptyString/>替换为null,但从CONFIG表加载数据时,大量结果未被处理,rawData中仍存在<EmptyString/>,调试未定位到原因,请求排查。

自定义EmptyStringToNullRecordMapperProvider代码

import org.jooq.*
import org.jooq.impl.DefaultConfiguration
import org.jooq.impl.SQLDataType

class EmptyStringToNullRecordMapperProvider : RecordMapperProvider {
    override fun <R : Record?, E : Any?> provide(recordType: RecordType<R>?, type: Class<out E>?): RecordMapper<R, E> {
        val originalMapper = DefaultConfiguration().recordMapperProvider().provide(recordType, type)

        // 自定义映射器,将"EMPTYSTRING"值替换为null
        return RecordMapper { record ->
            originalMapper.map(record).apply {
                recordType?.fields()?.forEach { field ->
                    if (field.dataType.sqlDataType == SQLDataType.VARCHAR) {
                        System.out.println(record);
                        val originalValue = record?.getValue(field) as String?
                        if ("<EmptyString/>" == originalValue) {
                            record?.setValue(field, null)
                        }
                    }
                }
            }
        }
    }
}

Repository代码

import fr.test.model.jooq.generated.tables.records.*
import fr.test.model.jooq.generated.tables.references.CONFIG
import org.jooq.DSLContext
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional
import java.util.stream.Stream

@Repository
@Transactional(readOnly = true)
class LiveConfigurationRepository(
        @Autowired
        private val jooq: DSLContext
) {
    fun getConfiguration(): Map<String, List<String>> {
        val rawData: MutableMap<String, List<String>> = mutableMapOf()

        jooq.dsl().select(CONFIG)
                .from(CONFIG)
                .fetch()
                .stream()
                .forEach { record ->
                    val mapData = record.intoMap();
                    val configRecord: ConfigRecord = mapData["config"] as ConfigRecord;

                    val key: String = configRecord.key!!
                    val value: String? = configRecord.value
                    if (!value.isNullOrBlank())
                        addValueToMap(rawData, key, value);
                }

        return rawData;
    }

    fun addValueToMap(rawData: MutableMap<String, List<String>>, key: String, value: String) {
        if (rawData.containsKey(key)) {
            val existingList = rawData[key]!!.toMutableList()
            existingList.add(value)
            rawData[key] = existingList
        } else {
            rawData[key] = listOf(value)
        }
    }
}

排查与解决方案

1. 自定义Provider未注册到JOOQ配置

你自定义的EmptyStringToNullRecordMapperProvider大概率未被注入到Spring管理的JOOQ Configuration中,导致JOOQ仍使用默认映射器。需通过Spring配置将其注册:

方式一:自定义JOOQ Configuration Bean

import org.jooq.Configuration
import org.jooq.impl.DefaultConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class JooqConfig {
    @Bean
    fun jooqConfiguration(): Configuration {
        return DefaultConfiguration()
                .set(EmptyStringToNullRecordMapperProvider())
                // 补充其他必要配置:SQLDialect、DataSource等
    }
}

方式二:用DefaultConfigurationCustomizer适配Spring Boot自动配置

import org.jooq.impl.DefaultConfigurationCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class JooqConfig {
    @Bean
    fun recordMapperProviderCustomizer(): DefaultConfigurationCustomizer {
        return DefaultConfigurationCustomizer { config ->
            config.set(EmptyStringToNullRecordMapperProvider())
        }
    }
}

2. 映射逻辑错误:修改了原始Record而非映射后的对象

当前代码中,originalMapper.map(record)已将Record转换为目标对象(如ConfigRecord),但后续修改的是原始Record,而非已经映射好的对象。而Repository中使用的record.intoMap()得到的ConfigRecord是独立的映射结果,修改原始Record不会影响它。

修正逻辑,直接修改映射后的对象:

override fun <R : Record?, E : Any?> provide(recordType: RecordType<R>?, type: Class<out E>?): RecordMapper<R, E> {
    val originalMapper = DefaultConfiguration().recordMapperProvider().provide(recordType, type)

    return RecordMapper { record ->
        val mappedObject = originalMapper.map(record)
        // 针对ConfigRecord做针对性处理
        if (mappedObject is ConfigRecord) {
            if ("<EmptyString/>" == mappedObject.value) {
                mappedObject.value = null
            }
        }
        // 通用处理方式:反射遍历所有字符串属性
        // mappedObject?.javaClass?.declaredFields?.forEach { field ->
        //     if (field.type == String::class.java) {
        //         field.isAccessible = true
        //         val value = field.get(mappedObject) as String?
        //         if ("<EmptyString/>" == value) {
        //             field.set(mappedObject, null)
        //         }
        //     }
        // }
        mappedObject
    }
}

3. Repository中intoMap()的用法问题

record.intoMap()返回的是Record的键值对映射,其中mapData["config"]对应的ConfigRecord可能未经过自定义Mapper处理。建议直接使用into(Class)方法,确保走全局配置的映射器:

// 替换原intoMap()逻辑
val configRecord = record.into(ConfigRecord::class.java)

4. 字段类型判断不全面

field.dataType.sqlDataType == SQLDataType.VARCHAR仅匹配VARCHAR类型,未覆盖CHAR、LONGVARCHAR等其他字符串类型,可改用更通用的判断:

if (field.dataType.isString) {
    // 处理逻辑
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 23:15:07