Army:让 SQL 回归本真的类型安全 DSL 框架
在 Java 生态中,数据库访问框架的选择似乎已成定局——Hibernate 做 ORM,MyBatis 写 XML,jOOQ 做类型安全 SQL。但 Army 提出了另一个选项:一个以 SQL 为核心抽象、以编译时类型安全为基石、以纯 POJO 为结果的 SQL DSL 框架。本文将基于 Army 的源码和文档,深入剖析其设计理念和技术实现。
一、Army 是什么
Army 是一个类型安全的 SQL DSL(Domain-Specific Language)框架。它不是 ORM——不管理实体生命周期、不做级联删除、不生成 Schema;它不是代码生成器——不依赖数据库连接生成代码;它不是模板引擎——不用 XML 或字符串拼接 SQL。
Army 的核心定位是:用 Java 代码写 SQL,编译时保证类型安全,运行时返回纯 POJO。
它的设计哲学只有两句话:
- 不要创造新世界,只映射真实世界。
- 我们需要标准,也需要方言,这就是真实世界。
二、编译时元模型:类型安全的基石
2.1 问题:字符串与类型安全
在传统 SQL 框架中,字段引用通常是字符串:
// MyBatis 风格
SELECT id, name FROM stock WHERE status = #{status}
// JDBC 风格
String sql = "SELECT id, name FROM stock WHERE status = ?";
preparedStatement.setString(1, status);
字符串没有编译时检查,字段名拼错、类型不匹配等问题只能到运行时才能发现。
2.2 Army 的方案:注解处理器生成元模型
Army 通过编译时注解处理器 ArmyMetaModelDomainProcessor,在编译阶段扫描 @Table 注解的域类,自动生成对应的静态元模型类。
以下是一个域类定义,就是一个纯 POJO,不需要继承任何基类:
@Table(name = "stock",
indexes = @Index(name = "uni_stock_exchange_code", fieldList = {"exchange", "code"}, unique = true),
comment = "stock")
public class Stock {
@Generator(value = "io.army.generator.snowflake.Snowflake8Generator",
params = @Param(name = "startTime", value = "1779012232202"))
@Column
private long id;
@Column
private LocalDateTime createTime;
@Column
private LocalDateTime updateTime;
@Column(comment = "version")
private int version;
@Column(notNull = true, updatable = false, precision = 5, comment = "exchange code")
private String exchange;
@Column(notNull = true, updatable = false, precision = 15, comment = "stock code")
private String code;
@Column(notNull = true, precision = 130, comment = "company name")
private String name;
@Column(notNull = true, precision = 10, defaultValue = "'NORMAL'", comment = "listing status")
private StockStatus status;
@Column(precision = 10, scale = 2, defaultValue = "0.00", comment = "offer price")
private BigDecimal offerPrice;
@Column(defaultValue = "DATE '1970-01-01'", comment = "listing date")
private LocalDate listingDate;
}
编译后,注解处理器自动生成 Stock_ 类:
@Generated(value = "io.army.modelgen.ArmyMetaModelDomainProcessor", date = "...")
public final class Stock_ {
public static final SimpleTableMeta<Stock> T;
static {
T = _TableMetaFactory.getSimpleTableMeta(Stock.class);
final int fieldSize = T.fieldList().size();
if (fieldSize != 11) {
throw _TableMetaFactory.tableFiledSizeError(Stock.class, fieldSize);
}
}
// 字段名常量
public static final String ID = "id";
public static final String NAME = "name";
public static final String STATUS = "status";
// ... 其他字段
// 类型安全的字段元数据引用
public static final PrimaryFieldMeta<Stock> id = T.id();
public static final FieldMeta<Stock> name = T.field(NAME);
public static final FieldMeta<Stock> status = T.field(STATUS);
// ... 其他字段
}
关键设计决策:
- 不需要数据库连接:与 jOOQ 的代码生成不同,Army 的元模型完全从 Java 注解生成,编译时不需要连接数据库。
- 字段数量校验:
static块中检查字段数量,如果域类修改后忘记重新编译,会在运行时立即报错。 @Generator支持:主键生成策略(如 Snowflake8)通过注解配置,无需数据库自增。
2.3 枚举的自动映射
Army 支持三种枚举映射策略,通过接口自动识别:
| 接口 | 映射方式 | 适用场景 |
|---|---|---|
CodeEnum | 枚举序号 → 数据库整数 | 状态码(如 0=正常, 1=停盘) |
LabelEnum | 枚举标签 → 数据库字符串 | 有语义的字符串编码 |
| 默认(无接口) | 枚举名称 → 数据库字符串 | 简单枚举 |
在 Stock 示例中,StockStatus 是一个普通枚举,Army 自动使用 NameEnumType 按名称映射:
public enum StockStatus {
NORMAL, // → "NORMAL"
DELISTED, // → "DELISTED"
STOPT, // → "STOPT"
UNKNOWN // → "UNKNOWN"
}
如果需要按整数编码存储,只需让枚举实现 CodeEnum 接口:
public enum StockStatus implements CodeEnum<StockStatus> {
NORMAL(0), SUSPENDED(1), DELISTED(2);
private final int code;
StockStatus(int code) { this.code = code; }
@Override
public int code() { return this.code; }
}
不需要 @Enumerated 注解,不需要 TypeHandler,不需要 Converter——接口即配置。
三、Criteria API:像写 SQL 一样写 Java
3.1 方法链与 SQL 语法顺序一致
Army 的 Criteria API 采用静态方法入口模式,方法链的调用顺序与 SQL 语法顺序完全一致:
Select stmt = SQLs.query()
.select(Stock_.id, Stock_.name)
.from(Stock_.T, AS, "s")
.join(Exchange_.T, AS, "e")
.on(Stock_.exchange.equal(Exchange_.code))
.where(Stock_.status.equal(StockStatus.NORMAL))
.and(Stock_.listingDate.greaterEqual(LocalDate.of(2020, 1, 1)))
.orderBy(Stock_.offerPrice.desc())
.limit(20)
.asQuery();
对比 SQL:
SELECT s.id, s.name
FROM stock AS s
JOIN exchange AS e ON s.exchange = e.code
WHERE s.status = 'NORMAL'
AND s.listing_date >= DATE '2020-01-01'
ORDER BY s.offer_price DESC
LIMIT 20
方法链的每一步都返回类型安全的接口,编译器会检查参数类型。例如,Stock_.status 的类型是 FieldMeta<Stock>,其 equal() 方法接受 StockStatus 类型参数——传错类型会在编译时报错。
3.2 语句构建与执行分离
Army 的一个重要设计决策是:SQL 语句构建不需要 Session 或 Connection。SQLs.query() 返回的 Select 对象是一个纯数据结构,可以在任何地方构建,然后交给 Session 执行:
// 在服务层构建语句(不需要 Session)
Select stmt = SQLs.query()
.select(Stock_.T)
.from(Stock_.T)
.where(Stock_.status.equal(StockStatus.NORMAL))
.asQuery();
// 在 DAO 层执行
List<Stock> stocks = session.queryObjectList(stmt, Stock::new);
这个设计的好处是:语句构建逻辑可以复用、可以测试、可以组合。
3.3 可组合的条件
Army 的 where 和 and 方法接受 Expression 对象,这意味着每个条件都是一等公民,可以传递和组合:
// 构建可复用的条件
Expression activeCondition = Stock_.status.equal(StockStatus.NORMAL)
.and(Stock_.listingDate.greaterEqual(LocalDate.of(2020, 1, 1)));
// 在多个查询中复用
Select stmt1 = SQLs.query()
.select(Stock_.id)
.from(Stock_.T)
.where(activeCondition)
.asQuery();
Select stmt2 = SQLs.query()
.select(Stock_.name)
.from(Stock_.T)
.where(activeCondition)
.and(Stock_.offerPrice.greaterThan(BigDecimal.valueOf(100)))
.asQuery();
3.4 方言特定的语法
对于 PostgreSQL 特有的语法,Army 提供了 Postgres 入口类:
// RETURNING 子句(PostgreSQL 特有)
Delete stmt = Postgres.singleDelete()
.with("w1").as(ws -> ws
.deleteFrom(StockChatConversation_.T, AS, "t")
.where(StockChatConversation_.userId.equal(userId))
.and(StockChatConversation_.id.equal(conversationId))
.returning(StockChatConversation_.id)
.asReturningDelete()
).space()
.deleteFrom(StockChatMemory_.T, AS, "t")
.using("w1")
.where(StockChatMemory_.conversationId.equal(refField("w1", StockChatConversation_.ID)))
.asDelete();
对于 MySQL 特有的语法,Army 提供了 MySQLs 入口类:
// INSERT ... ON DUPLICATE KEY UPDATE(MySQL 特有)
Insert insert = MySQLs.singleInsert()
.insert(Stock_.T)
.values(...)
.onDuplicateKeyUpdate()
.set(Stock_.name, "Updated Name")
.asInsert();
四、三层类型系统:类型映射的新思路
4.1 问题:一层映射不够
大多数 SQL 框架只有一层类型映射:数据库类型名 → Java 类。这在实际使用中会遇到问题。
以 MySQL 的 TINYINT UNSIGNED 为例:它的取值范围是 0-255,但 Java 的 Byte 取值范围是 -128 到 127。一层映射会导致数据溢出——255 存入 byte 后变成 -1。
4.2 Army 的三层架构
Army 将类型映射分为三个正交的层:
| 层 | 回答的问题 | 示例 |
|---|---|---|
| SQLType | 这个数据库叫它什么? | MySQLType.TINYINT_UNSIGNED → ("TINYINT UNSIGNED", Short.class) |
| ArmyType | 这个类型的语义是什么? | INTEGER_UNSIGNED — 32位无符号整数 |
| MappingType | Java 如何与 JDBC 交互? | beforeBind(Java) → JDBC 值;afterGet(JDBC) → Java 值 |
类型协作流程:
Java 类型
↓ MappingFactory.getDefault()
MappingType
↓ MappingType.map(ServerMeta)
DataType (SQLType)
↓ MappingType.beforeBind()
数据库值
↓ JDBC
数据库列
4.3 MySQL 无符号整数的正确映射
Army 对 MySQL 的五种无符号整数类型都做了自动正确映射,选择能够容纳该范围的最小 Java 类型:
| MySQL 类型 | Java 类型 | 取值范围 | 选择原因 |
|---|---|---|---|
TINYINT UNSIGNED | Short | 0 ~ 255 | 255 超出 byte 范围(-128~127) |
SMALLINT UNSIGNED | Integer | 0 ~ 65,535 | 65535 超出 short 范围(±32767) |
MEDIUMINT UNSIGNED | Integer | 0 ~ 16,777,215 | 在 int 范围内 |
INT UNSIGNED | Long | 0 ~ 4,294,967,295 | 42亿超出 int 范围(±21亿) |
BIGINT UNSIGNED | BigInteger | 0 ~ 2^64 − 1 | 超出 long 范围 |
这些映射不需要配置,不需要 TypeHandler,不需要 Converter——类型系统已经知道每种类型"意味着什么"。
4.4 PostgreSQL 类型支持
Army 对 PostgreSQL 的类型支持是内置的,不需要插件或代码生成配置:
Range 类型(12种):INT4RANGE、INT8RANGE、NUMRANGE、TSRANGE、TSTZRANGE、DATERANGE 及 6 种 multirange 变体。
Array 类型(54种):BOOLEAN_ARRAY、INTEGER_ARRAY、BIGINT_ARRAY、TEXT_ARRAY、UUID_ARRAY、JSONB_ARRAY 等,通过 army-array 模块提供。
pgvector 操作符:l2Distance、cosineDistance、hammingDistance、jaccardDistance、innerProduct、l1Distance 六个操作符作为一等表达式。
HSTORE 类型:支持三种映射模式——Map<K,V>、EnumMap<K extends Enum<K>, V> 和 POJO。
复合类型:通过 @DefinedType 注解将 Java POJO 映射到 PostgreSQL 复合类型,支持嵌套。
五、Session 与事务管理
5.1 Session 架构
Army 的 Session 分为两种:
SyncLocalSession:本地事务会话SyncRmSession:XA 分布式事务会话(两阶段提交)
这两种在编译时通过接口区分,保证了类型安全。
5.2 TransactionTemplate
Army 提供了 TransactionTemplate 用于编程式事务管理,相比 Spring 的 TransactionTemplate,它使用了参数化方法设计:
// 读操作(只读)
return this.transactionTemplate.execute(true, _ -> dao.queryUserConversation(userId));
// 写操作(指定隔离级别)
return this.transactionTemplate.executeNoNull(Isolation.READ_COMMITTED, false,
_ -> dao.deleteConversation(userId, conversationId));
// 无返回值操作
this.transactionTemplate.executeWithoutResult(Isolation.READ_COMMITTED, false,
_ -> dao.save(entity));
execute、executeNoNull、executeWithoutResult 三个方法分别对应"可能返回 null"、"不应返回 null"和"无返回值"三种场景。隔离级别和只读标志作为方法参数传入,而非通过 setter 设置,避免了状态遗留问题。
5.3 Spring 集成
Army 通过 army-spring 模块提供 Spring 框架集成:
@Configuration
public class ArmyConfig {
@Bean
public SyncSessionFactory sessionFactory(DataSource dataSource) {
return new ArmySyncSessionFactoryBean()
.setDataSource(dataSource)
.setDatabase(Database.POSTGRESQL)
.afterPropertiesSet();
}
@Bean
public PlatformTransactionManager transactionManager(SyncSessionFactory factory) {
return new ArmySyncLocalTransactionManager(factory);
}
}
配置完成后,可以按照 Domain-Specific DAO + Service 的模式组织业务代码。
5.4 Domain-Specific DAO
继承基础 DAO,使用 Army 的 Criteria API 实现领域特定查询:
@Repository("stockChatConversationDao")
public class StockChatConversationDaoImpl extends ArmyStockBaseDao
implements StockChatConversationDao {
public StockChatConversationDaoImpl(SyncSessionContext sessionContext) {
super(sessionContext);
}
@Override
public List<StockChatConversation> queryUserConversation(long userId) {
final Select stmt = SQLs.query()
.select("t", PERIOD, StockChatConversation_.T)
.from(StockChatConversation_.T, AS, "t")
.where(StockChatConversation_.userId.equal(userId))
.orderBy(StockChatConversation_.id.desc())
.asQuery();
return this.sessionContext.currentSession()
.queryObjectList(stmt, StockChatConversation::new);
}
@Override
public long deleteConversation(long userId, long conversationId) {
final String w1 = "w1";
final Delete stmt = Postgres.singleDelete()
.with(w1).as(ws -> ws.deleteFrom(StockChatConversation_.T, AS, "t")
.where(StockChatConversation_.userId.equal(userId))
.and(StockChatConversation_.id.equal(conversationId))
.returning(StockChatConversation_.id)
.asReturningDelete()
).space()
.deleteFrom(StockChatMemory_.T, AS, "t")
.using(w1)
.where(StockChatMemory_.conversationId.equal(refField(w1, StockChatConversation_.ID)))
.asDelete();
return this.sessionContext.currentSession().update(stmt);
}
}
这段代码展示了两个关键能力:
StockChatConversation::new是一个方法引用,编译器会检查构造函数签名。查询结果直接映射到 POJO,没有代理对象、没有状态机、没有attached/detached生命周期。- CTE + USING 级联删除:
deleteConversation方法通过 PostgreSQL 的WITH ... RETURNING+USING语法,在一条 SQL 中完成"删除会话 + 删除关联消息"的级联操作,不需要多次数据库往返。
5.5 Domain-Specific Service
继承基础 Service,使用 TransactionTemplate 管理事务:
@Service("stockChatConversationService")
public class StockChatConversationServiceImpl extends AbstractStockBaseService
implements StockChatConversationService {
private final StockChatConversationDao stockChatConversationDao;
public StockChatConversationServiceImpl(TransactionTemplate transactionTemplate,
StockChatConversationDao stockChatConversationDao) {
super(transactionTemplate);
this.stockChatConversationDao = stockChatConversationDao;
}
@Override
public List<StockChatConversation> queryUserConversation(long userId) {
return this.transactionTemplate.executeNoNull(true,
_ -> this.stockChatConversationDao.queryUserConversation(userId)
);
}
@Override
public long deleteConversation(long userId, long conversationId) {
return this.transactionTemplate.executeNoNull(Isolation.READ_COMMITTED, false,
_ -> this.stockChatConversationDao.deleteConversation(userId, conversationId)
);
}
@Override
protected StockBaseDao getDao() {
return this.stockChatConversationDao;
}
}
注意 Service 层的事务管理方式:
- 读操作:
executeNoNull(true, ...)— 第一个参数true表示只读事务,框架可以优化(如路由到读库)。 - 写操作:
executeNoNull(Isolation.READ_COMMITTED, false, ...)— 显式指定隔离级别和读写标志。 - 隔离级别作为参数传入:不会像 setter 模式那样出现"上一次调用设置了 READ_COMMITTED,下一次调用忘记重置"的状态遗留问题。
DAO 层只负责 SQL 构建和执行,Service 层只负责事务边界和业务编排,职责分离清晰。
六、保留字段
Army 定义了 5 个保留字段名,当域类中声明了这些字段时,框架会自动赋予特殊行为。保留字段的定义在 _MetaBridge.java 中:
public static final String ID = "id";
public static final String CREATE_TIME = "createTime";
public static final String UPDATE_TIME = "updateTime";
public static final String VERSION = "version";
public static final String VISIBLE = "visible";
public static final List<String> RESERVED_FIELDS = List.of(
ID, CREATE_TIME, UPDATE_TIME, VERSION, VISIBLE
);
回顾前面 Stock 域类的定义,其中已经包含了 4 个保留字段:
public class Stock {
@Generator(value = "io.army.generator.snowflake.Snowflake8Generator", ...)
@Column
private long id; // 保留字段:主键,自动生成
@Column
private LocalDateTime createTime; // 保留字段:创建时间,插入时自动填充
@Column
private LocalDateTime updateTime; // 保留字段:更新时间,插入和更新时自动填充
@Column(comment = "version")
private int version; // 保留字段:乐观锁版本号,更新时自动递增
// ... 业务字段
}
6.1 五个保留字段的职责
| 保留字段 | Java 类型 | 框架行为 | 触发时机 |
|---|---|---|---|
id | long / Long | 通过 @Generator 自动生成主键值 | INSERT |
createTime | LocalDateTime | 自动填充当前时间 | INSERT |
updateTime | LocalDateTime | 自动填充当前时间 | INSERT + UPDATE |
version | int / long | 乐观锁,UPDATE 时自动 SET version = version + 1,并附加 WHERE version = ? 条件 | UPDATE |
visible | boolean | 软删除,会话级自动注入 WHERE visible = ? 条件 | SELECT / UPDATE / DELETE |
关键设计:这些字段都是可选的。域类中声明了哪个保留字段,框架就启用对应的功能;不声明则不启用。例如,如果 Stock 不需要乐观锁,去掉 version 字段即可,框架不会报错。
6.2 id:主键自动生成
通过 @Generator 注解指定主键生成策略:
@Generator(value = "io.army.generator.snowflake.Snowflake8Generator",
params = @Param(name = "startTime", value = "1779012232202"))
@Column
private long id;
Army 内置 Snowflake8Generator(雪花算法),也支持数据库自增(@Generator(type = GeneratorType.POST))。插入时框架自动填充 id 字段,不需要手动设置。
6.3 createTime / updateTime:时间戳自动填充
当域类声明了 createTime 或 updateTime 字段时:
- INSERT:框架自动将
createTime和updateTime设置为当前时间(如果字段值为 null)。 - UPDATE:框架自动将
updateTime更新为当前时间。
不需要拦截器、不需要 AOP、不需要 @PrePersist / @PreUpdate——字段名即约定。
6.4 version:乐观锁
当域类声明了 version 字段时,UPDATE 语句会自动变为:
-- 框架自动生成
UPDATE stock
SET name = ?, version = version + 1
WHERE id = ?
AND version = ? -- 乐观锁条件
如果并发更新导致版本号不匹配,受影响行数为 0,框架会抛出 OptimisticLockException。
6.5 visible:软删除
当域类声明了 visible 字段时:
@Column(notNull = true, defaultValue = "true")
private boolean visible;
会话配置可见性模式:
SyncSession session = sessionFactory.syncSessionBuilder()
.visible(Visible.ONLY_VISIBLE) // 只查询可见记录
.build();
Visible 枚举提供三种模式:
| 模式 | 行为 | 适用场景 |
|---|---|---|
ONLY_VISIBLE | 自动注入 WHERE visible = true | 默认业务查询 |
ONLY_NON_VISIBLE | 自动注入 WHERE visible = false | 回收站 / 已删除数据查询 |
BOTH | 不注入条件,查询所有记录 | 管理后台 / 数据导出 |
visible 条件注入发生在 SQL 解析阶段,对 MySQL、PostgreSQL、SQLite 三种方言均生效。开发者不需要在每个查询中手动添加 WHERE visible = true——会话级别配置一次,所有查询自动生效。
与 version 乐观锁类似,visible 也是可选的。如果域类没有声明 visible 字段,会话的 Visible 配置不会产生任何效果。
七、Spring AI 集成
Army 已经扩展到 Spring AI 生态,提供了 army-spring-ai-model-chat-memory 和 army-spring-ai-vector-store 两个模块。
7.1 聊天记忆存储
ArmyMessageChatMemory 实现了 Spring AI 的 ChatMemory 接口,使用 Army 的 Criteria API 进行消息持久化:
@Bean
public ArmyMessageChatMemory<CoderChatMemory> coderChatMemory(SyncSessionContext context) {
return ArmyMessageChatMemory.builder(context, CoderChatMemory_.T)
.maxMessages(30)
.build();
}
7.2 记忆工具化设计
一个值得关注的设计决策是:ArmyMessageChatMemoryAdvisor 只做保存操作,不自动将记忆注入到每次请求中。记忆通过 memoryTool() 方法以工具形式提供给 AI 模型:
@Bean
public ChatClient coderChatClient(ChatClient.Builder builder,
ArmyMessageChatMemory<CoderChatMemory> chatMemory) {
List<Advisor> advisorList = List.of(
SystemMessageAdvisor.of(Ordered.HIGHEST_PRECEDENCE + 10),
ArmyMessageChatMemoryAdvisor.builder(chatMemory).build(),
ToolCallingAdvisor.builder().build()
);
builder.defaultAdvisors(advisorList);
builder.defaultTools(chatMemory.memoryTool(null)); // 记忆作为工具
return builder.build();
}
这种设计的好处是:AI 模型可以按需查询记忆,而不是每次请求都携带全部历史记录,有效降低了 token 消耗。
八、框架对比
以下对比基于各框架的公开文档和设计理念,旨在帮助开发者根据自身需求选择合适的工具。
8.1 核心设计对比
| 维度 | Hibernate | jOOQ | MyBatis | Army |
|---|---|---|---|---|
| 方法论 | 对象关系映射 | 类型安全 SQL DSL | SQL 模板 | 类型安全 SQL DSL |
| 数据源 | DDL + 实体类 | 数据库 DDL | XML / 注解 | 域 POJO + 注解 |
| 构建时需要数据库 | 否 | 是 | 否 | 否 |
| 结果类型 | 代理对象(有状态) | Record(有状态) | 任意(手动映射) | 纯 POJO(无状态) |
| 类型映射层数 | 1 层(JDBC) | 1 层(JDBC) | 0 层(TypeHandler) | 3 层(方言/语义/Java) |
| 枚举策略 | ORDINAL / STRING | Converter | TypeHandler | Code / Label / Name + SET |
| PG Range/Array | 插件 | 代码生成 | TypeHandler | 内置 |
| pgvector | 不支持 | 不支持 | 不支持 | 内置(6 个操作符) |
| 许可证 | LGPL | Apache 2.0(免费版)/ 商业版 | Apache 2.0 | Apache 2.0 |
8.2 各框架的适用场景
Hibernate 适合:需要对象关系映射、级联操作、懒加载的场景。它的强项是让开发者以面向对象的方式操作数据库,适合 CRUD 密集的应用。
jOOQ 适合:需要类型安全 SQL 且能接受数据库依赖的场景。它的代码生成基于数据库 schema,保证了 SQL 与数据库结构的一致性。
MyBatis 适合:需要完全控制 SQL 的场景。它的灵活性最高,但类型安全需要开发者自行保证。
Army 适合:需要类型安全 SQL、跨方言支持、纯 POJO 结果的场景。它的优势在于不依赖数据库连接的编译时元模型生成、三层类型系统、以及对 PostgreSQL 特有类型的内置支持。
8.3 类型映射对比示例
以 MySQL INT UNSIGNED 为例:
| 框架 | 默认映射 | 问题 |
|---|---|---|
| Hibernate | Integer | 42亿超出 int 范围(±21亿),静默溢出 |
| jOOQ | UInteger(自定义类型) | 需要 jOOQ 特定类型,与普通 Java 代码不兼容 |
| MyBatis | 需手写 TypeHandler | 开发者负担 |
| Army | Long(自动) | 无需配置,自动选择正确类型 |
以 PostgreSQL 数组为例:
| 框架 | 支持方式 | 配置量 |
|---|---|---|
| Hibernate | 需要插件 | 中等 |
| jOOQ | 代码生成 | 需要数据库连接 |
| MyBatis | 需手写 TypeHandler | 高 |
| Army | 内置 | 零配置 |
九、模块架构
Army 采用多模块设计,按需引入:
army (parent pom)
├── army-struct # 核心类型定义和工具
├── army-core # 核心模块:Criteria API、Session、类型映射、方言
├── army-annotation # 注解和编译时处理器
├── army-array # 数组类型映射(可选)
├── army-sync # 同步会话 API
├── army-jdbc # JDBC 执行器实现
├── army-mysql # MySQL 方言
├── army-postgre # PostgreSQL 方言
├── army-sqlite # SQLite 方言
├── army-oracle # Oracle 方言
├── army-spring # Spring 集成
├── army-guava # Guava 集成
├── army-spring-ai-model-chat-memory # Spring AI 聊天记忆
├── army-spring-ai-vector-store # Spring AI 向量存储
└── army-example # 示例和测试
一个值得注意的设计是 army-core 与 army-array 的解耦。army-core 在编译时不依赖 army-array,而是通过反射在运行时检测 army-array 是否存在。如果存在,则自动启用数组类型支持;如果不存在,框架仍然可以正常工作,只是不支持数组类型。这让 army-array 成为真正的可选模块。
十、总结
Army 的设计理念可以概括为:SQL 是正确的抽象层,不要隐藏它,而是给它一个类型安全的 API。
它通过三个核心设计实现了这个理念:
- 编译时元模型:从 Java 注解生成类型安全的字段引用,不需要数据库连接。
- 三层类型系统:将数据库类型、类型语义和 Java 类型分离,让方言成为一等公民。
- 纯 POJO 结果:查询结果通过构造方法引用映射到普通 Java 对象,没有代理、没有状态机。
Army 不试图替代 Hibernate 或 MyBatis——它们各有擅长的领域。Army 提供的是另一种选择:给那些"已经懂 SQL、想要类型安全、不想被框架绑架"的开发者一个工具。
如果你正在寻找一个类型安全的 SQL DSL 框架,或者对 PostgreSQL 的特有类型(Range、Array、HSTORE、pgvector)有需求,Army 值得一试。
许可证:Apache 2.0
Maven 坐标:
<dependency>
<groupId>io.qinarmy</groupId>
<artifactId>army-jdbc</artifactId>
<version>0.6.7</version>
</dependency>