Merge branch 'feature/10-jimmer-support-jimmer-specification' into 'main'

feat(webmvc): 添加 Jimmer规范实现

Closes #10

See merge request gewuyou/forgeboot!15
This commit is contained in:
gewuyou 2025-08-02 04:06:54 +00:00
commit 401b9cf065
17 changed files with 473 additions and 32 deletions

View File

@ -1,4 +1,6 @@
dependencies {
val libs = rootProject.libs
compileOnly(libs.slf4j.api)
implementation(libs.kotlinReflect)
implementation(libs.kotlinxCoroutines.reactor)
}

View File

@ -4,19 +4,100 @@ package com.gewuyou.forgeboot.core.extension
* 尝试执行一段代码块如果执行过程中抛出异常则返回null
* 使用inline避免额外的函数调用开销对于性能敏感的场景特别有用
*
* @param block 一个lambda表达式代表尝试执行的代码块
* @return 成功执行的结果或者如果执行过程中抛出异常则返回null
* @param T 泛型参数表示代码块执行的返回类型
* @param block 一个lambda表达式代表尝试执行的代码块
* @return T? 成功执行的结果或者如果执行过程中抛出异常则返回null
*/
inline fun <T> tryOrNull(block: () -> T): T? =
try { block() } catch (_: Exception) { null }
try {
block()
} catch (_: Exception) {
null
}
/**
* 尝试执行一段代码块如果执行过程中抛出异常则返回一个预设的备选值
* 同样使用inline关键字以减少性能开销
*
* @param fallBack 备选值如果代码块执行过程中抛出异常则返回该值
* @param block 一个lambda表达式代表尝试执行的代码块
* @return 成功执行的结果或者如果执行过程中抛出异常则返回备选值
* @param T 泛型参数表示代码块执行的返回类型和备选值类型
* @param defaultValue 备选值如果代码块执行过程中抛出异常则返回该值
* @param block 一个lambda表达式代表尝试执行的代码块
* @return T 成功执行的结果或者如果执行过程中抛出异常则返回备选值
*/
inline fun <T> tryOrFallBack(fallBack: T, block: () -> T): T =
try { block() } catch (_: Exception) { fallBack }
inline fun <T> tryOrFallBack(defaultValue: T, block: () -> T): T =
try {
block()
} catch (_: Exception) {
defaultValue
}
/**
* 检查给定的条件是否为真若为假则使用指定的异常工厂函数创建并抛出异常
*
* @param T 异常类型必须继承自 Throwable
* @param value 要检查的布尔值如果为 false 则会使用exceptionFactory创建并抛出异常
* @param exceptionFactory 用于创建异常实例的工厂函数接收一个字符串消息参数
* @param lazyMessage 延迟计算的消息提供函数当需要抛出异常时调用并转换为字符串作为异常消息
* @throws T 当value为false时通过exceptionFactory创建的异常类型
*/
inline fun <T : Throwable> requireX(
value: Boolean,
exceptionFactory: (String) -> T,
lazyMessage: () -> Any,
) {
if (!value) throw exceptionFactory(lazyMessage().toString())
}
/**
* 检查给定的条件是否为真若为假则使用指定的异常类型抛出带有延迟生成消息的异常
*
* @param T 异常类型必须继承自 Throwable并且需要有仅接受单个 String 参数的构造函数
* @param condition 要检查的布尔值如果为 false 则会抛出异常
* @param lazyMessage 延迟计算的消息提供函数当需要抛出异常时调用
* @throws IllegalArgumentException 如果指定的异常类型没有合适的单参数字符串构造函数
*/
inline fun <reified T : Throwable> checkOrThrow(
condition: Boolean,
lazyMessage: () -> Any,
) {
if (!condition) {
val constructor = T::class.constructors.firstOrNull {
it.parameters.size == 1 && it.parameters[0].type.classifier == String::class
} ?: throw IllegalArgumentException("Exception type must have a single String parameter constructor")
throw constructor.call(lazyMessage().toString())
}
}
/**
* 条件检查失败时通过异常工厂函数创建异常并抛出
*
* @param T 异常类型必须继承自 Throwable
* @param condition 要检查的布尔值如果为 false 则会使用exceptionFactory创建并抛出异常
* @param exceptionFactory 用于创建异常实例的工厂函数接收一个字符串消息参数
* @param lazyMessage 延迟计算的消息提供函数当需要抛出异常时调用并转换为字符串作为异常消息
*/
inline fun <T : Throwable> checkOrThrowWithFactory(
condition: Boolean,
exceptionFactory: (String) -> T,
lazyMessage: () -> Any,
) {
if (!condition) {
throw exceptionFactory(lazyMessage().toString())
}
}
/**
* 条件检查失败时直接抛出指定的异常
*
* @param condition 要检查的布尔值如果为 false 则会调用exception函数并抛出其返回的异常
* @param exception 提供要抛出的 Throwable 实例的函数
*/
inline fun checkOrThrowDirect(
condition: Boolean,
exception: () -> Throwable,
) {
if (!condition) {
throw exception()
}
}

View File

@ -1,5 +1,8 @@
package com.gewuyou.webmvc.spec.core.service
import com.gewuyou.forgeboot.webmvc.dto.PageResult
import com.gewuyou.webmvc.spec.core.page.QueryComponent
/**
*CRUD服务规范
*
@ -59,13 +62,6 @@ interface CrudServiceSpec <Entity: Any, Id: Any> {
*/
fun deleteByOne(entity: Entity)
/**
* 批量删除实体
*
* @param entities 要删除的实体列表
*/
fun deleteByAll(entities: List<Entity>)
/**
* 软删除
*
@ -130,4 +126,14 @@ interface CrudServiceSpec <Entity: Any, Id: Any> {
* @return 返回记录总数
*/
fun count(): Long
/**
* 分页查询实体列表
*
* 通过提供的查询组件进行分页数据检索返回包含分页信息的结果对象
*
* @param query 查询组件包含分页和过滤条件等信息
* @return 返回分页结果对象包含当前页的数据列表总记录数等信息
*/
fun page(query: QueryComponent): PageResult<Entity>
}

View File

@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

40
forgeboot-webmvc/spec-jimmer/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Kotlin ###
.kotlin

View File

@ -0,0 +1,6 @@
dependencies {
api(project(Modules.Webmvc.DTO))
api(project(Modules.Webmvc.Spec.CORE))
compileOnly(libs.org.babyfish.jimmer.springBootStarter)
}

View File

@ -0,0 +1,2 @@
package com.gewuyou.webmvc.spec.jimmer.extension

View File

@ -0,0 +1,27 @@
package com.gewuyou.webmvc.spec.jimmer.page
import com.gewuyou.webmvc.spec.core.page.Filterable
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecification
import kotlin.reflect.KClass
/**
*Jimmer可过滤接口
*
* @since 2025-07-30 21:35:21
* @author gewuyou
*/
interface JimmerKFilterable<EntityType : Any> : Filterable<KSpecification<EntityType>> {
/**
* 获取当前的查询规范
*
* @return KSpecification<EntityType>? 返回当前的查询规范可能为null
*/
fun getSpecification(): KSpecification<EntityType>? = filter
/**
* 获取实体类型对应的KClass对象
*
* @return KClass<EntityType> 返回实体类型的KClass对象
*/
fun entityClass(): KClass<EntityType>
}

View File

@ -0,0 +1,24 @@
package com.gewuyou.webmvc.spec.jimmer.request
import com.gewuyou.webmvc.spec.core.enums.SortDirection
import com.gewuyou.webmvc.spec.core.page.Pageable
import com.gewuyou.webmvc.spec.jimmer.page.JimmerKFilterable
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecification
/**
*抽象过滤分页请求
*
* @param T 实体类型参数
* @param currentPage 当前页码默认值为1
* @param pageSize 每页大小默认值为10
* @param filter 查询过滤条件可为空默认值为null
* @param sortDirection 排序方向
* @since 2025-07-30 21:53:03
* @author gewuyou
*/
abstract class AbstractFilteredPageRequest<T : Any>(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
override val filter: KSpecification<T>? = null,
sortDirection: SortDirection,
) : Pageable, JimmerKFilterable<T>

View File

@ -0,0 +1,23 @@
package com.gewuyou.webmvc.spec.jimmer.request
import com.gewuyou.webmvc.spec.core.entities.SortCondition
import com.gewuyou.webmvc.spec.core.enums.SortDirection
import com.gewuyou.webmvc.spec.core.page.Pageable
import com.gewuyou.webmvc.spec.core.page.Sortable
import com.gewuyou.webmvc.spec.jimmer.page.JimmerKFilterable
import org.babyfish.jimmer.sql.kt.ast.query.specification.KSpecification
/**
*抽象排序日期过滤分页请求
*
* @since 2025-07-30 21:59:34
* @author gewuyou
*/
abstract class AbstractSortedDateFilterPageRequest<T : Any>(
override val currentPage: Int = 1,
override val pageSize: Int = 10,
override val sortBy: String = "createdAt",
override val sortDirection: SortDirection = SortDirection.DESC,
override val sortConditions: List<SortCondition> = emptyList(),
override val filter: KSpecification<T>? = null,
) : Pageable, Sortable, JimmerKFilterable<T>

View File

@ -0,0 +1,12 @@
package com.gewuyou.webmvc.spec.jimmer.service
import com.gewuyou.webmvc.spec.core.service.CrudServiceSpec
/**
*Jimmer Crud服务规范
*
* @since 2025-07-30 21:16:56
* @author gewuyou
*/
interface JimmerCrudServiceSpec<Entity : Any, Id : Any> : CrudServiceSpec<Entity, Id> {
}

View File

@ -0,0 +1,211 @@
package com.gewuyou.webmvc.spec.jimmer.service.impl
import com.gewuyou.forgeboot.webmvc.dto.PageResult
import com.gewuyou.webmvc.spec.core.extension.toPageRequest
import com.gewuyou.webmvc.spec.core.extension.toPageResult
import com.gewuyou.webmvc.spec.core.page.Pageable
import com.gewuyou.webmvc.spec.core.page.QueryComponent
import com.gewuyou.webmvc.spec.jimmer.page.JimmerKFilterable
import com.gewuyou.webmvc.spec.jimmer.service.JimmerCrudServiceSpec
import org.babyfish.jimmer.View
import org.babyfish.jimmer.spring.repository.KRepository
import org.babyfish.jimmer.spring.repository.fetchSpringPage
import org.babyfish.jimmer.sql.ast.mutation.SaveMode
/**
*JimmerCrud服务实现规范
*
* @since 2025-07-30 21:30:05
* @author gewuyou
*/
open class JimmerCrudServiceImplSpec<Entity : Any, Id : Any>(
open val repository: KRepository<Entity, Id>,
) : JimmerCrudServiceSpec<Entity, Id> {
/**
* 根据查询条件分页查询并返回指定视图类型的分页结果
*
* 该函数通过泛型参数指定实体类型和视图类型将查询结果转换为对应的视图对象返回
*
* @param E 实体类型参数必须是Any的子类型
* @param V 视图类型参数必须是View<E>的子类型
* @param query 查询组件包含分页和过滤条件
* @return PageResult<V> 返回指定视图类型的分页结果
*/
inline fun <reified E : Any, reified V : View<E>> pageWithView(
query: QueryComponent,
): PageResult<V> {
query as Pageable
val pageable = query.toPageRequest()
@Suppress("UNCHECKED_CAST")
query as JimmerKFilterable<E>
return repository.sql.createQuery(E::class) {
where(query.getSpecification())
select(table.fetch(V::class))
}.fetchSpringPage(pageable).toPageResult()
}
/**
* 根据ID获取实体
*
* @param id 实体的唯一标识符
* @return 返回实体如果不存在则返回null
*/
override fun findById(id: Id): Entity? {
return repository.findById(id).orElse(null)
}
/**
* 获取所有实体列表
*
* @return 返回实体列表
*/
override fun list(): List<Entity> {
return repository.findAll()
}
/**
* 保存一个实体
*
* @param entity 要保存的实体
* @return 返回保存后的实体
*/
override fun save(entity: Entity): Entity {
return repository.save(entity)
}
/**
* 更新一个实体
*
* @param entity 要更新的实体
* @return 返回更新后的实体
*/
override fun update(entity: Entity): Entity {
return repository.save(entity, SaveMode.UPDATE_ONLY)
}
/**
* 删除一个实体
*
* @param id 要删除的实体的ID
*/
override fun deleteById(id: Id) {
repository.deleteById(id)
}
/**
* 批量删除实体
*
* @param ids 要删除的实体的ID列表
*/
override fun deleteByIds(ids: List<Id>) {
repository.deleteByIds(ids)
}
/**
* 删除一个实体
*
* @param entity 要删除的实体
*/
override fun deleteByOne(entity: Entity) {
repository.delete(entity)
}
/**
* 软删除
*
* 本函数用于标记实体为删除状态而不是真正从数据库中移除
* 这种方法可以保留历史数据同时避免数据泄露
*
* @param id 实体的唯一标识符
*/
override fun softDelete(id: Id) {
throw UnsupportedOperationException("softDelete Not supported yet.")
}
/**
* 批量软删除
*
* @param ids 要软删除的实体ID列表
*/
override fun softDeleteByIds(ids: List<Id>) {
throw UnsupportedOperationException("softDeleteByIds Not supported yet.")
}
/**
* 取消软删除恢复已删除实体
*
* @param id 要恢复的实体ID
*/
override fun restore(id: Id) {
throw UnsupportedOperationException("restore Not supported yet.")
}
/**
* 批量取消软删除
*
* @param ids 要恢复的实体ID列表
*/
override fun restoreByIds(ids: List<Id>) {
throw UnsupportedOperationException("restoreByIds Not supported yet.")
}
/**
* 判断实体是否已被软删除
*
* @param id 实体ID
* @return 如果是软删除状态返回 true否则返回 false
*/
override fun isSoftDeleted(id: Id): Boolean {
throw UnsupportedOperationException("isSoftDeleted Not supported yet.")
}
/**
* 根据ID检查实体是否存在
*
* @param id 实体的ID
* @return 如果实体存在返回true否则返回false
*/
override fun existsById(id: Id): Boolean {
return repository.existsById(id)
}
/**
* 批量保存实体
*
* @param entities 要保存的实体列表
* @return 返回保存后的实体列表
*/
override fun saveAll(entities: List<Entity>): List<Entity> {
return repository.saveAll(entities)
}
/**
* 查询记录总数
*
*
* @return 返回记录总数
*/
override fun count(): Long {
return count()
}
/**
* 分页查询实体列表
*
* 通过提供的查询组件进行分页数据检索返回包含分页信息的结果对象
*
* @param query 查询组件包含分页和过滤条件等信息
* @return 返回分页结果对象包含当前页的数据列表总记录数等信息
*/
override fun page(query: QueryComponent): PageResult<Entity> {
query as Pageable
val pageable = query.toPageRequest()
@Suppress("UNCHECKED_CAST")
query as JimmerKFilterable<Entity>
return repository.sql.createQuery(query.entityClass()) {
where(query.getSpecification())
select(table)
}.fetchSpringPage(pageable).toPageResult()
}
}

View File

@ -2,7 +2,9 @@ package com.gewuyou.webmvc.spec.jpa.extension
import com.gewuyou.webmvc.spec.core.extension.toPageRequest
import com.gewuyou.webmvc.spec.core.page.*
import com.gewuyou.webmvc.spec.core.page.DateRangeFilterable
import com.gewuyou.webmvc.spec.core.page.KeywordSearchable
import com.gewuyou.webmvc.spec.core.page.QueryComponent
import com.gewuyou.webmvc.spec.jpa.page.JpaFilterable
import com.gewuyou.webmvc.spec.jpa.page.JpaStatusFilterable
import jakarta.persistence.criteria.CriteriaBuilder

View File

@ -1,9 +1,9 @@
package com.gewuyou.webmvc.spec.jpa.request
import com.gewuyou.webmvc.spec.core.enums.SortDirection
import com.gewuyou.webmvc.spec.core.page.Pageable
import com.gewuyou.webmvc.spec.jpa.page.JpaFilterable
import org.hibernate.query.SortDirection
/**

View File

@ -11,17 +11,6 @@ import com.gewuyou.webmvc.spec.core.service.CrudServiceSpec
* @author gewuyou
*/
interface JpaCrudServiceSpec<Entity: Any, Id: Any>: CrudServiceSpec<Entity, Id> {
/**
* 分页查询实体列表
*
* 通过提供的查询组件进行分页数据检索返回包含分页信息的结果对象
*
* @param query 查询组件包含分页和过滤条件等信息
* @return 返回分页结果对象包含当前页的数据列表总记录数等信息
*/
fun page(query: QueryComponent): PageResult<Entity>
/**
* 分页查询并映射结果
*
@ -45,4 +34,12 @@ interface JpaCrudServiceSpec<Entity: Any, Id: Any>: CrudServiceSpec<Entity, Id>
* @return 返回符合条件的记录总数
*/
fun count(query: QueryComponent): Long
/**
* 批量删除实体
*
* @param entities 要删除的实体列表
*/
fun deleteByAll(entities: List<Entity>)
}

View File

@ -5,20 +5,22 @@
[versions]
jjwt-version = "0.12.6"
gradleMavenPublishPlugin-version = "0.32.0"
kotlin-version = "2.0.0"
kotlin-version = "2.1.20"
kotlinxDatetime-version = "0.6.1"
kotlinxSerializationJSON-version = "1.7.3"
#kotlinxCoroutines-version = "1.9.0"
axion-release-version = "1.18.7"
spring-cloud-version = "2024.0.1"
spring-boot-version = "3.4.4"
spring-boot-version = "3.5.3"
slf4j-version = "2.0.17"
caffeine-version = "3.2.1"
redisson-version = "3.50.0"
org-pf4j-version = "3.13.0"
org-pf4j-spring-version = "0.10.0"
org-yaml-snakeyaml-version = "2.4"
jimmer-version = "0.9.101"
[libraries]
org-babyfish-jimmer-springBootStarter = { group = "org.babyfish.jimmer", name = "jimmer-spring-boot-starter", version.ref = "jimmer-version" }
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-version" }
kotlinxDatetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime-version" }
kotlinxSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJSON-version" }
@ -38,7 +40,6 @@ springBootStarter-jpa = { group = "org.springframework.boot", name = "spring-boo
springBootStarter-validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation" }
springBootStarter-redis = { group = "org.springframework.boot", name = "spring-boot-starter-data-redis" }
redisson-springBootStarter = { group = "org.redisson", name = "redisson-spring-boot-starter", version.ref = "redisson-version" }
springBoot-configuration-processor = { group = "org.springframework.boot", name = "spring-boot-configuration-processor", version.ref = "spring-boot-version" }
springBoot-autoconfigure = { group = "org.springframework.boot", name = "spring-boot-autoconfigure" }

View File

@ -77,6 +77,7 @@ include(
":forgeboot-webmvc:validation",
":forgeboot-webmvc:spec-core",
":forgeboot-webmvc:spec-jpa",
":forgeboot-webmvc:spec-jimmer",
)
project(":forgeboot-webmvc").name = "forgeboot-webmvc-spring-boot-starter"
project(":forgeboot-webmvc:version").name = "forgeboot-webmvc-version-spring-boot-starter"
@ -87,6 +88,7 @@ project(":forgeboot-webmvc:dto").name = "forgeboot-webmvc-dto"
project(":forgeboot-webmvc:validation").name = "forgeboot-webmvc-validation"
project(":forgeboot-webmvc:spec-core").name = "forgeboot-webmvc-spec-core"
project(":forgeboot-webmvc:spec-jpa").name = "forgeboot-webmvc-spec-jpa"
project(":forgeboot-webmvc:spec-jimmer").name = "forgeboot-webmvc-spec-jimmer"
//endregion
//region module core
@ -152,6 +154,7 @@ include(
":forgeboot-demo:forgeboot-plugin-demo:forgeboot-plugin-demo-server",
)
//endregion
//region module plugin
include(
"forgeboot-plugin",
":forgeboot-plugin:forgeboot-plugin-core",
@ -160,13 +163,14 @@ include(
project(":forgeboot-plugin").name = "forgeboot-plugin-spring-boot-starter"
project(":forgeboot-plugin:forgeboot-plugin-core").name = "forgeboot-plugin-core"
project(":forgeboot-plugin:forgeboot-plugin-spring").name = "forgeboot-plugin-spring"
//endregion
//region module cache
include(
"forgeboot-cache",
":forgeboot-cache:forgeboot-cache-api",
":forgeboot-cache:forgeboot-cache-impl",
":forgeboot-cache:forgeboot-cache-autoconfigure"
)
)
project(":forgeboot-cache").name = "forgeboot-cache-spring-boot-starter"
project(":forgeboot-cache:forgeboot-cache-api").name = "forgeboot-cache-api"
project(":forgeboot-cache:forgeboot-cache-impl").name = "forgeboot-cache-impl"