The original Ujorm2 homepage has moved here.
"Do the simplest thing that could possibly work." β Kent Beck, creator of Extreme Programming and pioneer of Test-Driven Development.
Ujorm3 is a lightweight object-relational mapping (ORM) library designed for efficient relational database development with minimalist code and a straightforward API. The library maps database rows to standard Java objects using clean SQL without unnecessary abstraction. It supports mapping to both mutable JavaBeans and immutable Records, including M:1 relations.
To achieve data manipulation speeds comparable to hand-written JDBC code, Ujorm3 compiles its own bytecode at runtime. At its core, the library is built around the Typed Key Pattern. These keys act as typed descriptors, providing compile-time safety without casting and enabling fast bulk operations. Consequently, the overhead of Java reflection is strictly limited to the initial loading of object metadata.
To maintain a high utility-to-code ratio and minimize bugs, Ujorm3 intentionally limits its scope:
- No Lazy Loading: Relationships are not lazily fetched to avoid hidden database round-trips typical of transparent lazy loading. You still need explicit fetches or batch queries where you would otherwise load related data in a loop (an N+1 pattern).
- M:1 Relations Only: Collection attributes (1:M) are not supported. Query from the "many" side or use a secondary SQL query instead.
- No Magic / No Stateful Lifecycle: The library does not manage database transactions or entity caching. Entities are treated as stateless data carriers.
- No SQL Dialects: Advanced queries are written in native SQL. This unlocks the full performance and feature set of your specific database engine.
Open, reproducible benchmarks against seven popular Java ORM and database-mapping frameworks (PostgreSQL and H2) show Ujorm3 consistently placing among the top performers β frequently leading in execution speed and memory efficiency. It also carries the smallest compiled footprint of all tested libraries. Full metrics and methodology are available at π GitHub: orm-benchmarks.
- Quick Start (TL;DR)
- Detailed Operations & Relations
- Class Diagram
- Generated Meta Models
- Configuration
- Maven Dependencies & Setup
- FAQ
- Feedback & Contributions
- Related Links
Ujorm3 provides a specialized toolset for different levels of complexity. Choose the one that best fits your current task:
The fastest way to handle single-table operations by primary key. It requires no SQL writing and supports standard Jakarta annotations.
/** Simple CRUD operations for Entities */
void simpleCrud(java.sql.Connection connection) {
var crud = CITY_EM.crud(connection);
var saved = crud.insert(new City(null, "Barcelona", "ES"));
var barcelona = crud.findById(saved.id()).orElseThrow();
}The primary tool for searching and fetching relations. It uses Criterion objects for type-safe filtering (verifying both structure and parameter types) and handles JOIN clauses automatically.
/** Type-safe selection */
List<Employee> findEmployees(Connection connection) {
return SelectQuery.run(connection, EMPLOYEE_EM, query -> query
.columns(true) // Select all domain columns including foreign keys
.column(MetaEmployee.city, MetaCity.name)
.where(MetaEmployee.id.whereGe(1L))
.toList());
}A universal approach for complex requirements. It allows writing raw SQL while maintaining safety via bind() and label(). Using the generic mapper, Ujorm3 can automatically populate even nested relations by matching SQL aliases to Key paths.
/** Universal native SQL with relations β label() approach */
List<Employee> findEmployees(Connection connection) {
return SqlQuery.run(connection, query -> query
.sql("""
SELECT e.id AS ${e.id}
, e.name AS ${e.name}
, c.name AS ${c.name}
FROM employee e
JOIN city c ON c.id = e.city_id
WHERE e.id >= :id
""")
.label("e.id" , MetaEmployee.id)
.label("e.name", MetaEmployee.name)
.label("c.name", MetaEmployee.city, MetaCity.name) // Key Path mapping
.bind("id", 1L)
.toStream(EMPLOYEE_MAPPER.mapper()) // Generic mapping including relations
.toList());
}As an alternative, the .column("table.col", Key...) mode uses the ${COLUMNS} placeholder and lets Ujorm generate the column list β the two modes cannot be mixed within a single query. See QuickStartTutorialTest.select_by_column for a full example, and select_by_clean_native_query in the same file for the plain dot-notation shortcut (c.name AS "city.name") that the mapper resolves automatically without any registration.
While the Quick Start covers the basics, real-world development involves complex relationships and batch processing.
Building on the SelectQuery builder, Ujorm3 excels at querying hierarchical data by automatically generating JOIN clauses. The join type depends on the @JoinColumn annotation:
- INNER JOIN: Used for mandatory attributes (
nullable = false). - LEFT JOIN: Used for nullable attributes (default).
For complex logic, use the binary-tree based Criterion for type-safe filtering. If you need to append specific SQL fragments (like ORDER BY or GROUP BY), use the tail() method.
The .columns(boolean) parameter controls which entity fields are included in SELECT:
trueβ selects all fields, including FK reference IDs (required when you need to traverse or identify relations).falseβ selects only non-FK primitive fields; combine with explicit.column(Key, Key)calls to load just the relation attributes you need.
For non-entity results such as aggregations or custom projections, use .toStream(rs -> ...) with a lambda instead of .toList().
final EntityContext CTX = EntityContext.ofDefault();
final EntityManager<Employee, Long> EMPLOYEE_EM = CTX.entityManager(Employee.class);
/** Fetching an entity with its relations */
List<Employee> select(Connection connection) {
return SelectQuery.run(connection, EMPLOYEE_EM, query -> query
.columns(true)
.column(MetaEmployee.city, MetaCity.name) // INNER JOIN
.column(MetaEmployee.boss, MetaEmployee.name) // LEFT JOIN
.where(MetaEmployee.id.whereGe(1L).and(MetaCity.id.whereGe(1L)))
.tail("ORDER BY", MetaEmployee.id)
.toList()
);
}Ujorm3 handles auto-assigned primary keys out of the box. Batch operations are explicitly supported for high-performance scenarios.
void insert(Connection connection) {
var employeeCrud = EMPLOYEE_EM.crud(connection);
var cityCrud = CITY_EM.crud(connection);
var cityOttawa = cityCrud.insert(new City(null, "Ottawa", "CA"));
var emplDave = Employee.of("Dave", cityOttawa, null);
var emplCarol = Employee.of("Carol", cityOttawa, null);
employeeCrud.insert(emplDave, emplCarol); // Batch insert
}You can explicitly define which attributes should be updated using type-safe keys. If the original domain object is provided, Ujorm3 detects changes and updates only modified columns automatically.
void update(Connection connection) {
var employeeCrud = EMPLOYEE_EM.crud(connection);
var emplIngrid = employeeCrud.findById(1L).orElseThrow();
emplIngrid.setBoss(newBoss);
employeeCrud.update(Stream.of(emplIngrid), MetaEmployee.boss);
}When deleting self-referencing relationships, use the tail method to ensure correct ordering (e.g., deleting subordinates before bosses).
void delete(Connection connection) {
var employeeCrud = EMPLOYEE_EM.crud(connection);
var qBossId = MetaEmployee.as("b").key(MetaEmployee.id);
try (var query = new SelectQuery<>(connection, EMPLOYEE_EM)) {
var employees = query
.column(MetaEmployee.id)
.column(MetaEmployee.boss, qBossId) // builds the self-relation
.tail("ORDER BY", qBossId, "DESC NULLS LAST") // subordinates first
.toList();
employeeCrud.delete(employees.stream());
}
}These snippets are extracted from the JUnit tutorial tests demonstrating the full entity lifecycle.
You can run and modify these tests locally:
QuickStartTutorialTest.java
(low-level SqlQuery basics),
TutorialTest.java
(full lifecycle with SelectQuery DSL),
and
AdvancedTutorialTest.java
(transactions, locking, migrations, and failure scenarios),
plus the
PATTERNS.md
index for quick pattern lookup.
- SelectQuery:
A type-safe builder for constructing SELECT statements directly from the domain model using a fluent API.
It automatically generates
FROMandJOINclauses based on the paths of used metamodel attributes. It integrates hierarchicalCriteriontrees for complex data filtering without manual SQL writing. - ResultSetMapper:
A universal tool for transforming
ResultSetrows into Record or JavaBean objects. It utilizes the Stream API for memory-efficient and lazy processing of query results. It provides automatic type conversion between JDBC types and target class attributes. - EntityManager:
The central component responsible for managing metadata and configuring the ORM mapping.
It serves as a factory for creating
Crudobjects used for standard database operations. It manages SQL logging configurations and defines rules for table and column quoting.
Ujorm3 derives mapping from JPA/Jakarta annotations (@Table, @Column, @Id).
M:1 relationships are recognized if an attribute's class has a @Table annotation.
There is no data caching for user queries. Metadata is cached to maximize speed:
- ResultSetMapper: Caches column mapping structures (limit: 512 distinct queries).
- EntityManager: Retains the database table metamodel for each entity.
Use
EntityManagerServicefor shared singleton instances.
The Ujorm3 annotation processor generates Meta classes at compile time for type safety without string literals.
/** Auto-generated metamodel for Employee */
public class MetaEmployee {
private static final DomainHandler<Employee> meta = DomainHandlerProvider.getHandler(Employee.class);
public static final Key<Employee, Long> id = meta.getKey("id");
public static final Key<Employee, String> name = meta.getKey("name");
public static final Key<Employee, City> city = meta.getKey("city");
public static final Key<Employee, Employee> boss = meta.getKey("boss");
}Metamodel keys are singletons.
Chaining them allows for strictly type-safe paths (e.g., MetaEmployee.city, MetaCity.name).
They implement CharSequence, meaning they can be used interchangeably with Strings in many parts of the API.
Ujorm3 is configured using the Config class, where each parameter is represented by a type-safe Key.
To ensure consistency and thread safety in a multi-threaded environment, it is highly recommended to make the configuration immutable by calling the lock() method before passing it to the ORM engine.
Configuration values are assembled dynamically from multiple sources.
When a parameter is requested, the mechanism evaluates these sources following a strict priority (from highest to lowest):
- Manual Settings: Values explicitly assigned using the
setValue(Key, Object)method on theConfiginstance. - System Properties: JVM system properties prefixed with
org.ujorm.(e.g.,-Dorg.ujorm.batchSize=1000). - Configuration File: Values loaded from the
ujorm-config.propertiesfile located on the classpath. - Internal Defaults: The initial fallback values defined directly in the library's source code.
See the JavaDoc for all available parameters.
Ujorm3 requires Java 17 or higher.
<dependencies>
<dependency>
<groupId>org.ujorm</groupId>
<artifactId>ujo-core</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.ujorm</groupId>
<artifactId>ujo-orm</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>However, if you prefer a safer, strongly-typed coding style, you can optionally configure the maven-compiler-plugin to include the Ujorm3 Meta Processor.
<build>
<plugins>
<!-- 2. Compiler Plugin Setup -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<annotationProcessorPaths>
<!-- Optional: APT configuration for Ujorm3 -->
<path>
<groupId>org.ujorm</groupId>
<artifactId>ujorm-meta-processor</artifactId>
<version>3.0.3</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<!-- Optional: attributes for APT Ujorm3 -->
<arg>-Aujorm.prefix=Meta</arg>
<arg>-Aujorm.suffix=</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>Currently, the library's codebase is fully covered by JUnit tests utilizing an in-memory H2 database. In addition, the project includes automated integration tests for basic CRUD operations across major relational databases using Testcontainers. Supported database engines are:
- PostgreSQL
- MySQL
- MariaDB
- Oracle Free
- MS SQL Server
Note for contributors: Integration tests require a running Docker daemon and up to 6 GB of local disk space for the database images. You can execute these tests using the provided Bash script: bin/docker-integration-test.sh.
To enable the Meta Processor, configure the maven-compiler-plugin.
The library includes automated integration tests for PostgreSQL, MySQL, MariaDB, Oracle, and MS SQL Server via Testcontainers.
-
Do domain objects need to implement
Serializable?
No, Ujorm3 works with stateless data structures andSerializableis not required. -
Is
@JoinColumnrequired?
No, it is optional. Relations are recognized by the@Tableannotation on the attribute type. -
Are core components thread-safe?
Yes,EntityManagerandMetaclasses are stateless and thread-safe.CrudandSqlQueryare stateful and scoped to a single thread or request. -
How should I handle database transactions?
Ujorm3 does not manage transactions β use the JDBCConnectiondirectly. Callconnection.commit()to persist changes orconnection.rollback()to discard them. EveryCrudandSqlQueryoperation executes immediately on the given connection within the current transaction.AdvancedTutorialTestcovers explicit commit, rollback, atomicity, and isolation-level patterns in detail. -
Does Ujorm3 support native SQL queries?
Yes, for complex or database-specific queries you can use theSqlQueryclass to execute native SQL. This also lowers the barrier to entry: developers transitioning from JDBC or Jdbi can start with plain SQL and gradually adopt the type-safeSelectQueryAPI. -
Is runtime bytecode generation secure?
Yes. It relies purely on your project's domain classes with no external data input β the same approach used by established libraries such as HikariCP or Spring. -
Is the library difficult to maintain?
Easy maintenance was one of the main goals of the project. The ORM library consists of a few well-defined components with clearly defined responsibilities. It does not use Java agents or rewrite arbitrary application bytecode at runtime; mapping helpers are generated from your domain model. The library has an extremely compact codebase and is completely independent of third-party libraries. -
How can I teach an AI to use the Ujorm3 ORM library?
For a quick understanding of the library, it is best to provide the AI directly with the source code of these tutorial files (e.g., by copying the text or uploading files) so they are immediately available in its active context: TutorialTest.java and QuickStartTutorialTest.java, and AdvancedTutorialTest.java, plus the pattern index: PATTERNS.md.
Join the conversation or report issues on GitHub:
π Join the Discussion on GitHub
- Ujorm Main Project β the core library this project is built on.
- Petstore Demo. An open-source project demonstrating the Ujorm3 library. It uses the ORM module for database queries and the Element class for building the GUI.
- ORM Benchmarks. A performance comparison of Ujorm3 across several key metrics. Tests are run on PostgreSQL (via Docker) and H2 (in in-memory mode) databases.
- Ujorm Element. An overview of the Element class for building HTML pages using Java code.
- HTML Builder Benchmarks. A comparison of libraries for rendering HTML pages.
- TopMovies. A prototype application for recommending movies based on the match between a user's ratings and a virtual movie group. It is a closed-source project derived from the open-source PetStore Demo. The application runs on Java 25 and uses only Ujorm3 and Vanilla JavaScript.
- JavaDoc for class descriptions.
- Ujorm Release Notes.
- Introduction to the Ujorm (blog): Native SQL in Java without JDBC boilerplate β meet Ujorm3
