Army:让 SQL 回归本真的类型安全 DSL 框架

1 阅读16分钟

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

它的设计哲学只有两句话:

  1. 不要创造新世界,只映射真实世界。
  2. 我们需要标准,也需要方言,这就是真实世界。

二、编译时元模型:类型安全的基石

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 或 ConnectionSQLs.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位无符号整数
MappingTypeJava 如何与 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 UNSIGNEDShort0 ~ 255255 超出 byte 范围(-128~127)
SMALLINT UNSIGNEDInteger0 ~ 65,53565535 超出 short 范围(±32767)
MEDIUMINT UNSIGNEDInteger0 ~ 16,777,215在 int 范围内
INT UNSIGNEDLong0 ~ 4,294,967,29542亿超出 int 范围(±21亿)
BIGINT UNSIGNEDBigInteger0 ~ 2^64 − 1超出 long 范围

这些映射不需要配置,不需要 TypeHandler,不需要 Converter——类型系统已经知道每种类型"意味着什么"。

4.4 PostgreSQL 类型支持

Army 对 PostgreSQL 的类型支持是内置的,不需要插件或代码生成配置:

Range 类型(12种):INT4RANGEINT8RANGENUMRANGETSRANGETSTZRANGEDATERANGE 及 6 种 multirange 变体。

Array 类型(54种):BOOLEAN_ARRAYINTEGER_ARRAYBIGINT_ARRAYTEXT_ARRAYUUID_ARRAYJSONB_ARRAY 等,通过 army-array 模块提供。

pgvector 操作符l2DistancecosineDistancehammingDistancejaccardDistanceinnerProductl1Distance 六个操作符作为一等表达式。

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));

executeexecuteNoNullexecuteWithoutResult 三个方法分别对应"可能返回 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);
    }
}

这段代码展示了两个关键能力:

  1. StockChatConversation::new 是一个方法引用,编译器会检查构造函数签名。查询结果直接映射到 POJO,没有代理对象、没有状态机、没有 attached/detached 生命周期。
  2. 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 类型框架行为触发时机
idlong / Long通过 @Generator 自动生成主键值INSERT
createTimeLocalDateTime自动填充当前时间INSERT
updateTimeLocalDateTime自动填充当前时间INSERT + UPDATE
versionint / long乐观锁,UPDATE 时自动 SET version = version + 1,并附加 WHERE version = ? 条件UPDATE
visibleboolean软删除,会话级自动注入 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 核心设计对比

维度HibernatejOOQMyBatisArmy
方法论对象关系映射类型安全 SQL DSLSQL 模板类型安全 SQL DSL
数据源DDL + 实体类数据库 DDLXML / 注解域 POJO + 注解
构建时需要数据库
结果类型代理对象(有状态)Record(有状态)任意(手动映射)纯 POJO(无状态)
类型映射层数1 层(JDBC)1 层(JDBC)0 层(TypeHandler)3 层(方言/语义/Java)
枚举策略ORDINAL / STRINGConverterTypeHandlerCode / Label / Name + SET
PG Range/Array插件代码生成TypeHandler内置
pgvector不支持不支持不支持内置(6 个操作符)
许可证LGPLApache 2.0(免费版)/ 商业版Apache 2.0Apache 2.0

8.2 各框架的适用场景

Hibernate 适合:需要对象关系映射、级联操作、懒加载的场景。它的强项是让开发者以面向对象的方式操作数据库,适合 CRUD 密集的应用。

jOOQ 适合:需要类型安全 SQL 且能接受数据库依赖的场景。它的代码生成基于数据库 schema,保证了 SQL 与数据库结构的一致性。

MyBatis 适合:需要完全控制 SQL 的场景。它的灵活性最高,但类型安全需要开发者自行保证。

Army 适合:需要类型安全 SQL、跨方言支持、纯 POJO 结果的场景。它的优势在于不依赖数据库连接的编译时元模型生成、三层类型系统、以及对 PostgreSQL 特有类型的内置支持。

8.3 类型映射对比示例

以 MySQL INT UNSIGNED 为例:

框架默认映射问题
HibernateInteger42亿超出 int 范围(±21亿),静默溢出
jOOQUInteger(自定义类型)需要 jOOQ 特定类型,与普通 Java 代码不兼容
MyBatis需手写 TypeHandler开发者负担
ArmyLong(自动)无需配置,自动选择正确类型

以 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

它通过三个核心设计实现了这个理念:

  1. 编译时元模型:从 Java 注解生成类型安全的字段引用,不需要数据库连接。
  2. 三层类型系统:将数据库类型、类型语义和 Java 类型分离,让方言成为一等公民。
  3. 纯 POJO 结果:查询结果通过构造方法引用映射到普通 Java 对象,没有代理、没有状态机。

Army 不试图替代 Hibernate 或 MyBatis——它们各有擅长的领域。Army 提供的是另一种选择:给那些"已经懂 SQL、想要类型安全、不想被框架绑架"的开发者一个工具。

如果你正在寻找一个类型安全的 SQL DSL 框架,或者对 PostgreSQL 的特有类型(Range、Array、HSTORE、pgvector)有需求,Army 值得一试。


项目地址github.com/PillArmy/ar…

文档pillarmy.github.io/army/

许可证:Apache 2.0

Maven 坐标

<dependency>
    <groupId>io.qinarmy</groupId>
    <artifactId>army-jdbc</artifactId>
    <version>0.6.7</version>
</dependency>