Army on GitHub: https://github.com/PillArmy/army
Every claim about Army below comes from its source (
0.6.8-SNAPSHOT, Java 25) and the army-example test suite. Hibernate claims are benchmarked against the current stable release as of September 2026 — 7.4.8.Final (2026-09-13, Jakarta Persistence 3.2) — plus official documentation (hibernate.org, docs.hibernate.org). Line counts were measured locally withwc -lon 2026-09-17.Source layering: every Army code block is copied from project source/tests; the Hibernate/JPA block in §2.1 is my own hand-written sketch against the standard JPA API (not taken from any project), included only to contrast the programming models.
Bias disclaimer: no winner is declared. The main difference is not "who is stronger" — these two were never trying to do the same job.
0. Meet the Contestants
| Army | Hibernate ORM | |
|---|---|---|
| What it is | A type-safe SQL DSL ("not an ORM", its words) | Java's oldest, fullest ORM / JPA implementation |
| Current version |
0.6.8-SNAPSHOT (0.6.7 latest on Maven Central) |
7.4.8.Final stable; 8.0 in Beta |
| Java baseline |
Java 25 (maven.compiler.source=25, non-negotiable) |
Java 17 / 21 / 25 / 26 |
| License | Apache 2.0 | Apache 2.0 since 7.0 (6.x was LGPL 2.1) |
| Standard alignment | None — proprietary API | Most mainstream Jakarta Persistence 3.2 (JPA) implementation — JPA itself was heavily influenced by Hibernate, but the official RI is EclipseLink |
| Age | A young 0.x project | 1.0 series in 2002 — a twenty-something "living fossil" |
| Ecosystem | army-spring integration + two Spring AI modules (Chat Memory / Vector Store); bring your own pool | Spring Data JPA, Quarkus, WildFly, Envers, Search, Reactive… |
Army's README opens with a deliberately stubborn self-definition:
Army is a type-safe, composable SQL DSL for Java — not an ORM, not a code generator, not a template engine. It treats SQL as the right abstraction and gives you an API that matches its power without hiding it.
Hibernate's own "A Short Guide to Hibernate 7" describes its central job differently: reading normalized relational data, then renormalizing it into an object graph, and synchronizing in-memory dirty state back to the database.
The worldview gap in one sentence:
- Hibernate wants you to write less SQL — manipulate an object graph, and the framework generates SQL and manages object state for you.
- Army wants what you write to be SQL — expressed through a compile-time type-safe Java chain, with every resulting byte of SQL under your control.
1. Size: Hold the "Lightweight" Thought
"SQL DSL = small and cute" is a natural assumption. Measured main-source line counts:
| Module | Java files | Main LOC |
|---|---|---|
| army-core (DSL contracts + rendering skeleton + metamodel + mapping) | 805 | 120,470 |
| army-postgre (PostgreSQL dialect) | 78 | 39,742 |
| army-mysql (MySQL dialect) | 79 | 30,479 |
| army-jdbc (JDBC executor) | 9 | 8,437 |
| army-sync (synchronous Session API) | 35 | 5,193 |
| army-annotation (annotations + annotation processor) | 46 | 5,099 |
| army-spring (Spring integration) | 16 | 1,903 |
| army-sqlite | 13 | 1,472 |
| army-oracle | 5 | 315 (essentially a placeholder) |
So Army is not light in lines — the core alone is 120k. Its "lightness" is a simple runtime model: no persistence context, no proxies, no cache, no dirty checking. The complexity went into a typed transcription of SQL grammar — 70k lines for just the MySQL and PostgreSQL dialects.
hibernate-core is a several-hundred-thousand-line engineering product built over two decades, with complexity spent in entirely different places: object-graph management, caching, fetch strategies, the HQL translator, bytecode enhancement.
Complexity is conserved — the only question is whether it lives on the SQL side or the object side.
2. One Requirement, Two Worldviews
A boring requirement: insert one row, then fetch 20 rows by condition.
2.1 Hibernate / JPA (standard API, implementation-agnostic)
@Entity
class Stock {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
@Enumerated(EnumType.STRING)
private StockStatus status;
// getters/setters...
}
em.getTransaction().begin();
Stock stock = new Stock(); // plain new, set fields
stock.setCode("600519");
stock.setStatus(StockStatus.ACTIVE);
em.persist(stock); // now a managed entity in the persistence context
Stock again = em.find(Stock.class, 1L); // by PK (L1 cache first)
List<Stock> list = em.createQuery(
"select s from Stock s where s.status = :st order by s.offerPrice desc", Stock.class)
.setParameter("st", StockStatus.ACTIVE)
.setMaxResults(20)
.getResultList();
em.getTransaction().commit(); // dirty checking flush
You face objects: after persist, the object joins the persistence context; mutate its fields freely, and at commit/flush Hibernate computes the UPDATEs via dirty checking. You don't even need to know how many statements fire — which, of course, is also the birthplace of every N+1 horror story.
2.2 Army (README + real army-example code)
The entity is a plain POJO with just @Table/@Column — no @Entity:
@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(notNull = true, precision = 10, defaultValue = "'NORMAL'")
private StockStatus status;
// ...
}
The ArmyMetaModelDomainProcessor annotation processor generates a static metamodel Stock_ at compile time (visually reminiscent of the JPA static metamodel, but carrying something completely different):
public abstract class Stock_ {
public static final SimpleTableMeta<Stock> T;
public static final PrimaryFieldMeta<Stock> id = T.id();
public static final FieldMeta<Stock> name = T.field(NAME);
// ...
}
For queries you face the structured shape of a SQL statement (from the README):
Select stmt = SQLs.query()
.select(s -> s.space(Stock_.id, Stock_.code, Stock_.name, Stock_.offerPrice)
.comma(Exchange_.name, Exchange_.country))
.from(Stock_.T, AS, "s")
.join(Exchange_.T, AS, "e")
.on(Stock_.exchange.equal(Exchange_.code))
.where(Stock_.status.equal(StockStatus.ACTIVE))
.and(Stock_.listingDate.greaterEqual(LocalDate.of(2020, 1, 1)))
.orderBy(Stock_.offerPrice.desc())
.limit(20)
.asQuery();
List<StockSummary> results = session.queryObjectList(stmt, StockSummary::new);
Insertion goes through save (see ArmySyncSession.save — it immediately builds and executes an INSERT):
session.save(stock);
save returns the affected-row count; its body hands the domain to SQLStmts.insertStmt(...), executes it, and verifies the count. No managed state, no snapshot, no flush-timing puzzles — you can see exactly when SQL goes out.
3. Session Layer: A Stateful Object World vs a Stateless SQL Channel
This is the most fundamental difference, and the source evidence is hard.
Hibernate's Session/EntityManager is a Unit of Work plus a first-level cache: find/persist/merge/refresh/remove/flush/evict/clear form a full object state machine, on top of an optional second-level cache (JCache), query cache, and EntityGraph fetch control.
I grepped the entire io.army.session / io.army.dao source of both army-core and army-sync for state-machine methods — persist(, merge(, refresh(, remove(, flush(, evict(, detach( — zero hits (the only merge matches are PostgreSQL MERGE statements and an internal optionMap utility). What SyncSession offers:
- Queries:
queryOne / queryList / queryObjectList(stmt, ctor ref) / queryRecordList / query(Stream) / paging - Writes:
update(stmt)/save(domain)/batchSave(domainList)/batchUpdate(stmt) - Transactions:
startTransaction / commit / rollback / setSavePoint(onSyncLocalSession)
A fairness note, to avoid overstating it: Army is not unable to fetch by primary key. SyncBaseDao in army-sync (and SyncBaseService in army-spring) offer get(class, id), findById, getByUnique, existsById, rowCount. But look at the implementation — ArmySyncBaseDao.get simply delegates to findByUnique, which runs an immediate SELECT. They return plain POJOs that never enter a persistence context; that is a different thing from Hibernate's managed entities.
Result mapping is equally magic-free: session.queryObjectList(stmt, StockSummary::new) instantiates a plain object with your constructor/Supplier and fills fields by name. The README is visibly proud of this:
No
RecordN<T1,T2,...,TN>withvalue1()/value2(). Noattached/detachedlifecycle. No lazy-loading proxy that explodes when the session closes. You can serialize it, cache it, pass it across threads — it's just data.
In fairness, Hibernate also knows a heavy state model isn't for everything — it ships StatelessSession: no L1 cache, no dirty checking, no cascades, reserved for batch jobs. The mental model of Army's main session is roughly "what if Hibernate's StatelessSession sideline had been promoted to the main event" — except Army equipped that path with a complete compile-time SQL DSL.
What happens to caching?
Army's README answers "not my problem":
Army deliberately does NOT manage: Database connections → use your preferred connection pool; Transactions → use Spring
@Transactionalor JTA; Caches → use Redis, Caffeine, or your own; Schema migrations → use Flyway or Liquibase.
You supply the pool (SyncFactoryBuilder.builder().datasource(hikariDataSource)); you bring Redis/Caffeine. Hibernate ships a mandatory L1 cache, optional JCache L2, and a query cache — out of the box, at the price of learning CacheMode, invalidation strategies, and (new in 7.4) even CacheMode.REFRESH_SESSION.
4. The Mapping Model: Seventeen Annotations and Zero Relationships
Army's annotation package contains exactly 17 files:
Codec, Column, DdlMode, DiscriminatorValue, FieldParam, Generator, GeneratorType,
Index, IndexField, Inheritance, MappedSuperclass, Mapping, NullsOrder,
OverrideParams, Param, SortOrder, Table
Look for ManyToOne, OneToMany, ManyToMany, OneToOne, JoinColumn, ElementCollection, Embedded — not there. Army does not map object associations. In the example project, ChinaProvince referencing its parent region is just an unglamorous FK column:
@Table(name = "china_province", ...)
@DiscriminatorValue("PROVINCE")
public class ChinaProvince extends ChinaRegion<ChinaProvince> {
@Column(defaultValue = "0", notNull = true, updatable = false, comment = "relation Id")
private Long relationId;
// ...
}
Want related data? Write a JOIN. Hibernate's entire apparatus — @ManyToOne(fetch=LAZY), join fetch, @EntityGraph, @BatchSize, FetchMode.SUBSELECT for "renormalizing the object graph" — physically does not exist in Army. The N+1 problem is gone with it (but so is lazy-loading convenience; every JOIN is now yours to write).
The ORM-flavored capabilities Army keeps are all within the "single table / table family" scope:
-
Inheritance + discriminator:
@Inheritance("regionType")+@DiscriminatorValue("PROVINCE"); parentChinaRegionmaps tochina_region, childChinaProvincetochina_province, managed by theComplexTableMetafamily. -
@MappedSuperclass:AbstractChinaRegionpulls up shared columns (id, createTime, version…). -
ID generation:
@Generator(type = POST)identity, or a configured Snowflake (Snowflake8Generator). -
Built-in logical delete: the
Visibleenum (ONLY_VISIBLE / ONLY_NON_VISIBLE / BOTH) plusArmyKey.VISIBLE_MODE; the parser appends visibility predicates automatically to domain queries/updates/deletes. This parallels Hibernate's@SoftDelete, only added in 6.4 — Army put it in the SQL rendering layer. -
Built-in optimistic locking: domain DML handles version automatically. This real test from army-example's
DomainUpdateTestspasses a deliberately wrong version and assertsOptimisticLockException:
stmt = SQLs.domainUpdate()
.update(ChinaRegion_.T, AS, "c")
.set(ChinaRegion_.regionGdp, SQLs::plusEqual, SQLs::param, gdpAmount)
.where(ChinaRegion_.id.in(SQLs::rowParam, extractRegionIdList(regionList)))
.and(ChinaRegion_.createTime.between(SQLs::param, now.minusMinutes(10), AND, now))
.and(ChinaRegion_.regionGdp.plus(SQLs::param, gdpAmount).greaterEqual(0))
.and(ChinaRegion_.version.equal(SQLs::param, 20)) // error version
.asUpdate();
Hibernate's counterpart is @Version with conditions auto-added during dirty-checking flush; Army injects them while rendering an explicit domain DML — same optimistic lock, one via state machine, one via SQL generation.
On type mapping, Army splits DataType (what the database type is) from MappingType (how Java talks to JDBC). The showcase: unsigned integers auto-resolve to "the smallest Java type that can't overflow" (INT UNSIGNED→Long, BIGINT UNSIGNED→BigInteger), and the three enum strategies CodeEnum/LabelEnum/NameEnum work with zero annotations. Hibernate offers the JPA-standard AttributeConverter, @JdbcTypeCode, UserType — a bigger, more configurable system.
5. Querying: An HQL Translator vs Clause-by-Clause SQL Modeling
On Hibernate's side
Four cards to play:
- HQL/JPQL: entity-oriented string query language; the 6.0 translator rewrite (SQM) is very capable (fetch joins, subqueries, CTEs, window functions following along). In fairness, HQL is not an unsupervised raw string: the Hibernate Processor can validate HQL syntax and typing at compile time, and IntelliJ IDEA provides live completion/validation — but the language itself remains string-based rather than a Java-typed chain like Army's DSL;
- JPA Criteria API: type-safe but famously verbose;
-
QuerySpecification(7.0) and Jakarta Data repositories: Data Repositories shipped as a tech preview in 6.5 and went GA in 6.6, with an annotation processor generating repositories at compile time to close the type-safety gap; -
Native SQL:
createNativeQuery, the escape hatch any time.
The abstraction sits higher: from Stock s join s.exchange e derives the join condition from mappings. Code barely moves when dialects or tables change — at the price of understanding HQL-to-SQL translation, fetch timing, and Cartesian-product warnings.
On Army's side
There is no query language string. The DSL is everything, and rendering is direct: a DSL statement is just a data holder; ArmyParser walks it in SQL grammar order (WITH→SELECT→FROM→WHERE→GROUP BY→…→LIMIT→locks), each expression node writes itself into one shared StringBuilder, and parameters land in one ordered list (army-core README documents the whole pipeline).
Dialect differences land as template methods on concrete parsers — backticks and MySQL's LIMIT offset,count, for example:
@Override protected final char identifierDelimitedQuote() { return BACKTICK; }
// MySQL native: LIMIT offset, rowCount (not standard LIMIT n OFFSET m)
Two direct consequences:
-
Database-specific power is a first-class citizen. MySQL's
STRAIGHT_JOIN,USE/FORCE INDEX, the 36HintTypeenum constants of/*+ */optimizer hints,LOAD DATA,ON DUPLICATE KEY UPDATE(including the 8.0.19 row alias),SET @row:=0user variables; PostgreSQL's 12 range/multirange types, 54 array types, six pgvector distance operators,RETURNING,DECLARE CURSOR— each has a typed place on the chain, and version gating happens at render time (a CTE on 5.7 throws immediately instead of failing at the server). Hibernate is catching these too: JDBC Array support arrived in 6.1, array functions in 6.4, Extended Array Support in 6.6; thehibernate-vectormodule already existed in 6.4 (SQL Server vectors only landed in 7.2); soft-delete likewise dates to 6.4 — but they surface as HQL functions, extra module dependencies, or native SQL: seams under a unified abstraction. -
Portability is one-directional. Build with
MySQLs.query()and you are bound to MySQL; portability requires staying on theSQLs.query()common subset — which deliberately removes about half of the goodies above. Hibernate is "portable by default, extra work for specifics." The arrows point opposite ways.
A real query (army-example QueryTests): CTE + JOIN + window function + subquery + bind params in one statement:
stmt = SQLs.query()
.with("cte").as(s -> s.select(ChinaRegion_.id)
.from(ChinaRegion_.T, AS, "c")
.where(ChinaRegion_.id.in(SQLs::rowParam, regionIdList))
.asQuery())
.space()
.select(ChinaProvince_.id, Windows.sum(ChinaRegion_.regionGdp).over("w").as("gdpSum"))
.from(ChinaProvince_.T, AS, "p")
.join(ChinaRegion_.T, AS, "c").on(ChinaProvince_.id::equal, ChinaRegion_.id)
.where(ChinaProvince_.id::in, SQLs.subQuery()
.select(s -> s.space(SQLs.refField("cte", ChinaRegion_.ID)))
.from("cte")
.asQuery())
.window("w").as(s -> s.partitionBy(ChinaRegion_.name)
.orderBy(ChinaRegion_.regionGdp::desc).rows(UNBOUNDED_PRECEDING))
.orderBy(ChinaProvince_.id)
.limit(SQLs::param, regionSize)
.asQuery();
List<Map<String, Object>> rowList = session.queryObjectList(stmt, RowMaps::hashMap);
The price is a huge interface surface: the mysql grammar-contract package alone (io.army.criteria.mysql, 23 files) contains 3,519 lines of nested interfaces, and staged chains like _PrimaryOptionSpec → _PartitionSpec → … are a notorious learning cliff. It buys "illegal SQL does not compile" with sheer interface count.
6. Dialect Coverage: Wide Net vs Deep Wells
In 7.4, Hibernate ships 16 officially tested dialects inside hibernate-core (MySQL, PostgreSQL, Oracle, SQL Server, the three DB2 flavors, Sybase, H2, HSQL, MariaDB, HANA, Spanner, etc. — per the official Dialect report), while a separate hibernate-community-dialects module maintains ~30 community/legacy dialects on a best-effort basis (Informix, Ingres, Teradata, TiDB, SQLite…). The mechanism is a unified HQL translator plus dialect classes registering function differences.
Army's Database enum lists only five — MySQL, PostgreSQL, H2, SQLite, Oracle — with very different levels of honesty:
| Dialect | Module LOC | Real state |
|---|---|---|
| MySQL | 30,479 | Deep: 5.5–8.0 version gating, LOAD DATA, multi-table DML, full optimizer hints |
| PostgreSQL | 39,742 | Deeper: ranges/arrays/composites/pgvector/RETURNING/DECLARE CURSOR |
| SQLite | 1,472 | Basically usable (with its own SchemaComparer) |
| H2 | inside core | Mostly serves tests |
| Oracle | 315 | Just the Oracles entry and a few grammar interfaces — a placeholder |
| SQL Server | none | Research notes still lying around in local-doc/sql/SqlServer.sql
|
At the executor level, the army-jdbc README itself states support for only MySQL / PostgreSQL / SQLite.
So, objectively: Army is "two ultra-deep dialects + one lightweight + two uncashed checks"; Hibernate is "16 officially backed dialects + ~30 community ones, whose depth bows to a unified abstraction." This table matters more than any marketing line when choosing.
7. Transactions, DDL, Engineering: Where Marketing Can Mislead
Transactions. Army doesn't take over declarative transactions, but its capabilities are solid: SyncLocalSession offers programmatic startTransaction(option, mode) (rendering dialect-specific SET TRANSACTION ISOLATION LEVEL … ; START TRANSACTION …, even multi-statement on MySQL/PG), savepoints, rollback-only marking, plus XA via RmSession. The recommended daily driver is army-spring's ArmySyncLocalTransactionManager (a standard PlatformTransactionManager); tests simply use @Transactional. Hibernate is a long-standing JTA/Jakarta Transactions citizen with near-default Spring declarative transactions. Bottom line: in Spring both end up as @Transactional; you just configure one extra TransactionManager with Army.
DDL. Time to gently contradict Army's own README — it claims "doesn't … generate schemas," yet DdlMode offers five levels: NONE / VALIDATE_UNIQUE / VALIDATE / DROP_CREATE / UPDATE. ArmySyncFactoryBuilder.initializingSchema extracts metadata at factory bootstrap → compares via SchemaComparer → executes DDL automatically; the global ddl.mode key even defaults to UPDATE, and the example's FactoryUtils explicitly sets DdlMode.UPDATE. That maps almost one-to-one to Hibernate's hbm2ddl.auto=validate/update/create/create-drop. The difference: Army's docs recommend Flyway/Liquibase, and Hibernate's docs likewise discourage update in production — both projects actually agree that automated DDL is a dev-time convenience; Army's README is just unusually humble about what the code already does.
Batch writing. Both support JDBC batching. In Army batching is an explicit first-class citizen: separate batchQuery/batchSingleUpdate/batchMultiUpdate/batchSingleDelete/batchMultiDelete factories with namedParamList — SQL rendered once, parameters bound in batches. In Hibernate you tune hibernate.jdbc.batch_size, flush order, and order_inserts; powerful, but you must learn its rhythm.
Reactive. Hibernate has a separate Hibernate Reactive product line, independent of hibernate-core (Vert.x/Netty drivers; one of the default options in Quarkus's reactive data stack). Army's core Javadoc references io.army.reactive.ReactiveSession in several places, and commented-out reactive bootstrap code survives in the example factory — but no reactive module ships in this repository; today the only executable path is synchronous + JDBC.
Enterprise features. Hibernate has the long-standing Envers auditing module; 7.4 additionally introduced @Audited/@Temporal temporal/audited entities in core as an incubating feature (officially a re-implementation of Envers' ideas, with Envers-compatible audit logs), plus Search (full-text), L2 cache, multi-tenancy, and GraalVM. Army's closest neighbors are the two army-spring-ai-* modules (Chat Memory, Vector Store). No auditing, multi-tenancy, or caching.
8. The Big Table
| Dimension | Army | Hibernate ORM 7.4 |
|---|---|---|
| Essence | Type-safe SQL DSL (direct dialect SQL) | Full ORM / JPA 3.2 implementation |
| Abstraction center | SQL statements | Entity object graph + persistence context |
| Query language | Staged Java DSL (SQLs/MySQLs/Postgres) |
HQL/JPQL, JPA Criteria, Jakarta Data repository methods, native SQL |
| Compile-time safety | Headline feature: illegal syntax doesn't compile | Criteria/static metamodel/Data Repositories fully compile-time; HQL validated at compile time by Hibernate Processor and IDEs, but remains string-based |
| Object state machine | None (no persist/merge/flush/evict) | Yes, and central (plus the StatelessSession sideline) |
| L1/L2 cache | None; wire your own | Mandatory L1, JCache L2, query cache |
| Lazy loading / associations / cascade | No association mappings; relations are FK columns + hand-written JOINs |
@ManyToOne/@OneToMany/…, fetch joins, EntityGraph, cascade |
| N+1 exposure | Structurally impossible (but every JOIN is manual) | Continuous governance (EntityGraph/FetchProfile/batch size) |
| Soft delete / optimistic lock |
Visible built into renderer; version auto-added to domain DML |
@SoftDelete (6.4+), @Version auto-injection |
| Inheritance | Discriminator table families (@Inheritance/@DiscriminatorValue) |
Full SINGLE_TABLE/JOINED/TABLE_PER_CLASS |
| Dialect depth | MySQL/PG extremely deep (70k LOC), SQLite basic, Oracle placeholder | 16 core-supported dialects + ~30 community dialects; depth subordinate to unified HQL abstraction |
| Proprietary SQL | First-class (LOAD DATA, hints, ranges, vectors…) | Mostly supported, often via HQL extensions/annotations/native SQL |
| Auto DDL | Yes (VALIDATE/UPDATE/DROP_CREATE), default UPDATE | Yes (five hbm2ddl modes), plus hibernate-tools ecosystem |
| Pool / transactions | Bring a DataSource; Spring/JTA transactions | Embedded or external pools; deep Spring/JTA integration |
| Reactive | Interface traces in core, no shipping module | Separate Hibernate Reactive product line |
| Java baseline | 25 required | 17+ |
| Standardization | None, single vendor | Jakarta Persistence; massive talent/docs pool |
| Maturity | 0.6.x, API may still move | 24 years of evolution; 7.4 stable, 8.0 underway |
9. So, Which Do You Pick?
The case for Hibernate is straightforward:
- Your team wants standard skills, Stack Overflow coverage for everything, and plug-and-play hiring;
- The business is entity-CRUD, form-driven admin/back-office work where object-graph navigation and cascades genuinely remove code;
- You need L2 caching, auditing (Envers), multi-tenancy, many database vendors, or a complete reactive stack;
- You are willing (and able) to govern its classic traps: N+1, flush timing, equals/hashCode, lazy-loading session boundaries, cache invalidation.
The case for Army is pickier:
- Your team knows SQL cold and insists "every SQL statement hitting production must be readable and reviewable," yet is tired of string concatenation and MyBatis XML;
- The system leans hard on MySQL/PostgreSQL-specific power: multi-table DML, UPSERT, LOAD DATA, index/optimizer hints, ranges/arrays/pgvector;
- You want proxy-free, state-machine-free, freely serializable POJO results, and distinct types for single/batch/multi-table INSERT/UPDATE/DELETE;
- You can accept a Java 25 baseline, 0.x API churn, missing Oracle/SQL Server, and a support model that is basically "read the source" (fortunately the READMEs are unusually good and the 120k-line core is densely commented).
Hibernate's own guide offers the fair verdict: "should I use ORM, or plain SQL? The answer is usually: use both." Army happens to fill the long-neglected half of "both" — the half previously improvised with JdbcTemplate, MyBatis, or hand-written SQL — with a compile-time type-safe option that genuinely believes in dialects.
Of course, faith has a price: Army hands you the database's full complexity back, dressed in type-safe clothing. Every Hibernate pitfall you escape gets repaid in the form of a JOIN you write yourself.
Army: https://github.com/PillArmy/army (runnable cases in army-example/src/test/java/io/army/session/sync/)
Hibernate: https://hibernate.org/orm/ (7.4 docs: https://docs.hibernate.org/orm/7.4/)
Top comments (0)