无CDI请求上下文时检索实体问题(OptaPlanner、Hibernate Panache、Kotlin)
解决OptaPlanner ConstraintProvider中调用Hibernate Panache查询的上下文异常
问题背景
你尝试通过数据库配置动态控制OptaPlanner的约束开关,在ConstraintProvider的defineConstraints方法中直接调用Hibernate Panache的ConfigEntity.findByKey查询配置,但应用启动时触发以下异常:
(Quarkus Main Thread) Failed to start application (with profile dev): javax.enterprise.context.ContextNotActiveException: Cannot use the EntityManager/Session because neither a transaction nor a CDI request context is active. Consider adding @Transactional to your method to automatically activate a transaction, or @ActivateRequestContext if you have valid reasons not to use transactions.
给defineConstraints方法添加@Transactional注解后问题依然存在。
问题原因
OptaPlanner的ConstraintProvider在应用启动阶段初始化,此时CDI请求上下文和事务上下文都未激活。@Transactional注解仅在CDI容器管理的事务上下文触发方法调用时生效,而defineConstraints是由OptaPlanner初始化逻辑直接调用的,不受CDI事务上下文管理,所以添加注解无效。
解决方案
将配置加载逻辑移到CDI bean中,利用Quarkus启动事件提前把配置加载到内存,再在ConstraintProvider中注入该bean获取配置值,避免直接在defineConstraints中访问数据库。
步骤1:创建配置加载CDI Bean
import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.event.Observes import jakarta.transaction.Transactional import io.quarkus.runtime.StartupEvent @ApplicationScoped class ConfigLoader { private val configCache = mutableMapOf<String, String>() // 应用启动时加载配置到内存缓存 @Transactional fun loadConfigOnStartup(@Observes event: StartupEvent) { // 按需加载需要的配置键,也可调用ConfigEntity.listAll()加载全部配置 listOf("room", "teacher").forEach { key -> ConfigEntity.findByKey(key)?.let { config -> configCache[key] = config.stringValue } } } // 从缓存获取配置值 fun getConfig(key: String): String? { return configCache[key] } }
步骤2:修改ConstraintProvider注入配置Bean
import jakarta.inject.Inject import org.optaplanner.core.api.score.stream.Constraint import org.optaplanner.core.api.score.stream.ConstraintFactory import org.optaplanner.core.api.score.stream.ConstraintProvider class TimeTableConstraintProvider : ConstraintProvider { @Inject lateinit var configLoader: ConfigLoader override fun defineConstraints(constraintFactory: ConstraintFactory): Array<Constraint> { val constraints = mutableListOf<Constraint>() if (configLoader.getConfig("room") == "ENABLED") { constraints.add(roomConflict(constraintFactory)) } if (configLoader.getConfig("teacher") == "ENABLED") { constraints.add(teacherConflict(constraintFactory)) } return constraints.toTypedArray() } // 约束逻辑示例(根据实际业务调整) private fun roomConflict(constraintFactory: ConstraintFactory): Constraint { return constraintFactory.from(Lesson::class.java) .join(Lesson::class.java, equal(Lesson::room), overlapping(Lesson::startTime, Lesson::duration) ) .penalize("Room conflict", HardSoftScore.ONE_HARD) } private fun teacherConflict(constraintFactory: ConstraintFactory): Constraint { return constraintFactory.from(Lesson::class.java) .join(Lesson::class.java, equal(Lesson::teacher), overlapping(Lesson::startTime, Lesson::duration) ) .penalize("Teacher conflict", HardSoftScore.ONE_HARD) } }
原理说明
ConfigLoader作为应用级CDI bean,通过@Observes StartupEvent监听启动事件,触发带有@Transactional注解的加载方法,此时事务上下文已激活,可正常调用Panache查询并将配置存入内存缓存。TimeTableConstraintProvider初始化时通过CDI注入ConfigLoader,直接从内存缓存读取配置值,无需再访问数据库,彻底规避了上下文缺失的问题。
内容的提问来源于stack exchange,提问作者twango
相关产品推荐
相关产品推荐

