Access 接口与派生查询
SmartAccess 的核心思路与 Spring Data Repository 相似:业务只声明接口,启动时根据实体泛型和方法签名生成实现。但 SmartAccess 没有代理一层层转发,而是使用 ASM 生成一个实现类,再交给 Rain DI 注册。
从 Access 到实例
最底层接口只有模型元数据:
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:
interface StudentAccess : JpaAccess<Student, Int>启动过程:
AppLoader 扫描 Access 子接口
↓
AccessLoader 读取 @Database 与泛型元数据
↓
选择对应 DBService
↓
DBService.createAccess(...)
↓
AccessMaker 使用 ASM 生成 StudentAccess$Impl
↓
实例注册进 Rain DiContext因此注入的是生成类实例,不是 JDK 动态代理。方法名解析和字节码生成发生在启动期,请求/业务调用时直接执行生成的方法。
JpaAccess 内置方法
实体操作
studentAccess.get(id)
studentAccess.save(student)
studentAccess.update(student)
studentAccess.saveOrUpdate(student)
studentAccess.delete(id)
studentAccess.delete(student)saveOrUpdate 通过实体主键读取器判断:主键为 null 时 persist,否则 merge。这只是主键存在性判断,不等价于检查数据库中记录是否存在。
全量与分页
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: Long 与 data: List<T>。
手写 JPQL
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,但多结果异常仍会向外传播。
锁模式
内置 single、list、page 有接受 LockModeType 的重载。派生查询方法也可标记 @Lock:
@Lock(LockModeType.PESSIMISTIC_WRITE)
fun findById(id: Int): Student?锁必须在有效事务与数据库支持范围内使用。
底层逃生口
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 读取方法名,支持四种前缀:
| 前缀 | 生成操作 |
|---|---|
find | select |
count | count |
update | update |
delete | delete |
基础结构:
<操作><选择/更新字段>By<条件>OrderBy<排序>查询
fun findByName(name: String): List<Student>
fun findByNameAndAge(name: String, age: Int): Student?
fun findByNameOrEmail(name: String, email: String): List<Student>条件属性首字母会转为小写。方法参数依次绑定生成查询中的占位符。
条件操作符
源码支持以下后缀:
| 后缀 | 运算符 | 是否需要参数 | 示例 |
|---|---|---|---|
| 无 | = | 是 | findByName |
Isn | IS NULL | 否 | findByDeletedAtIsn |
Isr | IS NOT NULL | 否 | findByDeletedAtIsr |
Like | LIKE | 是 | findByNameLike |
In | IN | 是 | findByIdIn |
Ne | != | 是 | findByStatusNe |
Nl | NOT LIKE | 是 | findByNameNl |
Nin | NOT IN | 是 | findByIdNin |
Gt | > | 是 | findByAgeGt |
Lt | < | 是 | findByAgeLt |
Gte | >= | 是 | findByAgeGte |
Lte | <= | 是 | findByAgeLte |
连接词支持 And 和 Or:
fun findByStatusAndCreatedAtGte(status: String, time: Instant): List<Order>
fun findByOwnerIdOrAssigneeId(ownerId: Long, assigneeId: Long): List<Task>这些缩写来自当前解析器实现,与 Spring Data 的 IsNull、GreaterThanEqual 等完整关键字不同。不要按 Spring Data 习惯猜测;以本表为准。
排序
fun findByAgeGteOrderByNameAsc(age: Int): List<Student>
fun findByStatusOrderByCreatedAtDescAndIdAsc(status: String): List<Order>OrderBy 后支持 Asc、Desc 和 And 连接多个字段;未显式指定方向时默认 ASC。
count
fun countByStatus(status: String): Long
fun countIdByStatus(status: String): Longcount 前缀到 By 之间最多只能指定一个字段。无字段时生成 count(*)。
delete
fun deleteByName(name: String): Int
fun deleteByExpiredAtLt(time: Instant): Intdelete 与 By 之间不能声明选择字段。返回值通常使用受影响行数 Int。
update
解析器要求 update 与 By 之间至少有一个待更新字段:
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 可以固定命名参数信息:
fun findByName(@Named("name") value: String): List<Student>编译器可能不保留 Java 参数名。复杂方法建议使用 @Named,并在构建配置中保留参数元数据。
显式查询
JDBC 注解可绕过方法名解释器:
@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 前改写查询字符串:
@SearchRewriter(TenantQueryRewriter::class)
@Entity
class Order这适合统一添加表名/实体名约定,但字符串改写很容易破坏复杂 JPQL。租户隔离等安全边界不应只依赖脆弱的文本替换。
多数据库
在 Access 接口上使用 @Database:
@Database("analytics")
interface EventAccess : JpaAccess<Event, Long>AccessLoader 从 SmartAccess.dbServiceMap 选择同名 DBService。未标记时使用 default。
自定义模型元数据
默认 AccessMetadataProvider 从第一个泛型接口读取 T 和 PK。如果接口层级复杂,泛型不直接落在当前接口,可指定:
@MetadataProvider(CustomMetadataProvider::class)
interface StudentAccess : BaseAccess<Student, Int>Provider 负责返回实体类型和主键类型。
与普通 DAO 相比
普通 DAO 把查询写在实现类中,编译器能直接检查方法体,但重复代码多。SmartAccess 把规则集中到 AccessMaker:
接口签名 → 方法名解释 → AbstractQuery → JPQL/SQL → ASM 方法体实现很短、调用直接,但错误更多在应用启动或数据库执行时暴露。核心业务查询应有真实数据库集成测试,不能只依赖接口能成功注入。