Skip to content

Access 接口与派生查询

SmartAccess 的核心思路与 Spring Data Repository 相似:业务只声明接口,启动时根据实体泛型和方法签名生成实现。但 SmartAccess 没有代理一层层转发,而是使用 ASM 生成一个实现类,再交给 Rain DI 注册。

从 Access 到实例

最底层接口只有模型元数据:

kotlin
interface Access<T, PK> {
    val modelType: Class<T>
    val primaryKeyType: Class<PK>
}

CRUD 并不属于基础 Access,而由具体技术接口提供:

  • JDBCAccess<T, PK>:同步 JDBC 能力。
  • JpaAccess<T, PK>:同步 JPA/Hibernate 能力。
  • JpaAsyncAccess<T, PK>:挂起函数形式的异步能力。

应用通常直接继承 JpaAccess

kotlin
interface StudentAccess : JpaAccess<Student, Int>

启动过程:

text
AppLoader 扫描 Access 子接口

AccessLoader 读取 @Database 与泛型元数据

选择对应 DBService

DBService.createAccess(...)

AccessMaker 使用 ASM 生成 StudentAccess$Impl

实例注册进 Rain DiContext

因此注入的是生成类实例,不是 JDK 动态代理。方法名解析和字节码生成发生在启动期,请求/业务调用时直接执行生成的方法。

JpaAccess 内置方法

实体操作

kotlin
studentAccess.get(id)
studentAccess.save(student)
studentAccess.update(student)
studentAccess.saveOrUpdate(student)
studentAccess.delete(id)
studentAccess.delete(student)

saveOrUpdate 通过实体主键读取器判断:主键为 nullpersist,否则 merge。这只是主键存在性判断,不等价于检查数据库中记录是否存在。

全量与分页

kotlin
val all = studentAccess.findAll()
val first20 = studentAccess.findAll(Page(start = 0, num = 20))
val result = studentAccess.findPage(Page.page(pIndex = 2, pSize = 20))

Page 的两个字段是:

  • start:从 0 开始的结果偏移量。
  • num:最大结果数量。

Page.page(2, 20) 会转换为 Page(start = 20, num = 20)Page.single 等于 Page(0, 1)

PageResult<T> 包含 total: Longdata: List<T>

手写 JPQL

kotlin
val active = studentAccess.list(
    "from Student where active = ?1 order by name",
    true,
)

val page = studentAccess.page(
    "from Student where age >= ?1",
    Page.page(1, 50),
    18,
)

val count = studentAccess.count(
    "select count(id) from Student where active = ?1",
    true,
)

参数按 1 开始的位置参数绑定。single 在 JPA 抛出 NoResultException 时返回 null,但多结果异常仍会向外传播。

锁模式

内置 singlelistpage 有接受 LockModeType 的重载。派生查询方法也可标记 @Lock

kotlin
@Lock(LockModeType.PESSIMISTIC_WRITE)
fun findById(id: Int): Student?

锁必须在有效事务与数据库支持范围内使用。

底层逃生口

kotlin
val entityManager = studentAccess.getEntityManager()
val connection = studentAccess.getConnection()
val query = studentAccess.jpaQuery("select s from Student s")
val typed = studentAccess.typedQuery("select s from Student s", Student::class.java)

当派生方法不能表达复杂查询时,可以回到底层 JPA API。不要把 EntityManager 长期保存到单例字段;其生命周期由当前 DBContext 决定。

未实现入口

当前 JpaAccessBase.where(Map)where(Map, Page) 仍是 TODO,不要在生产代码中使用。

派生查询语法

MethodInterpreter 读取方法名,支持四种前缀:

前缀生成操作
findselect
countcount
updateupdate
deletedelete

基础结构:

text
<操作><选择/更新字段>By<条件>OrderBy<排序>

查询

kotlin
fun findByName(name: String): List<Student>
fun findByNameAndAge(name: String, age: Int): Student?
fun findByNameOrEmail(name: String, email: String): List<Student>

条件属性首字母会转为小写。方法参数依次绑定生成查询中的占位符。

条件操作符

源码支持以下后缀:

后缀运算符是否需要参数示例
=findByName
IsnIS NULLfindByDeletedAtIsn
IsrIS NOT NULLfindByDeletedAtIsr
LikeLIKEfindByNameLike
InINfindByIdIn
Ne!=findByStatusNe
NlNOT LIKEfindByNameNl
NinNOT INfindByIdNin
Gt>findByAgeGt
Lt<findByAgeLt
Gte>=findByAgeGte
Lte<=findByAgeLte

连接词支持 AndOr

kotlin
fun findByStatusAndCreatedAtGte(status: String, time: Instant): List<Order>
fun findByOwnerIdOrAssigneeId(ownerId: Long, assigneeId: Long): List<Task>

这些缩写来自当前解析器实现,与 Spring Data 的 IsNullGreaterThanEqual 等完整关键字不同。不要按 Spring Data 习惯猜测;以本表为准。

排序

kotlin
fun findByAgeGteOrderByNameAsc(age: Int): List<Student>
fun findByStatusOrderByCreatedAtDescAndIdAsc(status: String): List<Order>

OrderBy 后支持 AscDescAnd 连接多个字段;未显式指定方向时默认 ASC。

count

kotlin
fun countByStatus(status: String): Long
fun countIdByStatus(status: String): Long

count 前缀到 By 之间最多只能指定一个字段。无字段时生成 count(*)

delete

kotlin
fun deleteByName(name: String): Int
fun deleteByExpiredAtLt(time: Instant): Int

deleteBy 之间不能声明选择字段。返回值通常使用受影响行数 Int

update

解析器要求 updateBy 之间至少有一个待更新字段:

kotlin
fun updateStatusById(status: String, id: Long): Int
fun updateNameAndAgeById(name: String, age: Int, id: Int): Int

参数顺序先对应更新字段,再对应 where 条件。此能力由生成器构造更新语句,使用前应为目标数据库编写集成测试。

返回类型决定执行方式

生成器根据方法返回类型选择查询路径:

  • List<T>:调用 TypedQuery.getResultList()
  • PageResult<T>:构建分页查询和 count 查询。
  • 实体或可空实体:调用 singleOrNull()
  • 更新/删除:调用 executeUpdate() 并转换为声明返回类型。

Page 作为参数时,生成器调用 setFirstResult(page.start)setMaxResults(page.num)

参数命名

JPA 生成器默认按方法参数顺序绑定。@Named 可以固定命名参数信息:

kotlin
fun findByName(@Named("name") value: String): List<Student>

编译器可能不保留 Java 参数名。复杂方法建议使用 @Named,并在构建配置中保留参数元数据。

显式查询

JDBC 注解可绕过方法名解释器:

kotlin
@Select("from Student where age >= ?1")
fun adults(age: Int): List<Student>

@Execute("delete from Student where disabled = ?1")
fun deleteDisabled(disabled: Boolean): Int

在 JPA Access 中,这些字符串仍交给 JPA Query API;除非生成器明确处理 @NativeQuery,不要仅因注解名称就假设它是原生 SQL。

QueryRewriter

实体可通过 @SearchRewriter@ExecuteRewriter 指定 QueryRewriter,在创建 Query 前改写查询字符串:

kotlin
@SearchRewriter(TenantQueryRewriter::class)
@Entity
class Order

这适合统一添加表名/实体名约定,但字符串改写很容易破坏复杂 JPQL。租户隔离等安全边界不应只依赖脆弱的文本替换。

多数据库

在 Access 接口上使用 @Database

kotlin
@Database("analytics")
interface EventAccess : JpaAccess<Event, Long>

AccessLoader 从 SmartAccess.dbServiceMap 选择同名 DBService。未标记时使用 default

自定义模型元数据

默认 AccessMetadataProvider 从第一个泛型接口读取 TPK。如果接口层级复杂,泛型不直接落在当前接口,可指定:

kotlin
@MetadataProvider(CustomMetadataProvider::class)
interface StudentAccess : BaseAccess<Student, Int>

Provider 负责返回实体类型和主键类型。

与普通 DAO 相比

普通 DAO 把查询写在实现类中,编译器能直接检查方法体,但重复代码多。SmartAccess 把规则集中到 AccessMaker:

text
接口签名 → 方法名解释 → AbstractQuery → JPQL/SQL → ASM 方法体

实现很短、调用直接,但错误更多在应用启动或数据库执行时暴露。核心业务查询应有真实数据库集成测试,不能只依赖接口能成功注入。

基于 Apache License 2.0 发布