DEV Community

Pavel Ponec
Pavel Ponec

Posted on

Ujorm3: A New Lightweight ORM for JavaBeans and Records

"Do the simplest thing that could possibly work."
— Kent Beck, creator of Extreme Programming and pioneer of Test-Driven Development.

I believe the Java language architects didn't exactly hit the mark when designing the API for the original JDBC library for database operations. As a result, a significant number of various libraries and frameworks have emerged in the Java ecosystem, differing in their approach, level of complexity, and quality. I would like to introduce you to a brand new lightweight ORM library, Ujorm3, which I believe beats its competitors with its simplicity, transparent behavior, and low overhead. The goal of this project is to offer a reliable, safe, efficient, and easy-to-understand tool for working with relational databases without hidden magic and complex abstractions that often complicate both debugging and performance. The first release candidate (RC1) is now available in the Maven Central Repository, released under the free Apache License 2.0.

The library builds on the familiar principles of JDBC but adds a thin layer of a user-friendly API on top of them. It works with clean, stateless objects and native SQL, so the developer has full control over what is actually executed in the database. Ujorm3 deliberately avoids implementing SQL dialects and instead uses native SQL complemented by type-safe tools for mapping database results to Java objects. It does not cache the results of any user queries. To achieve maximum speed, however, Ujorm3 retains certain metadata.

Application API Classes

The core class for database operations is SqlQuery (originally named SqlParamBuilder), which acts as a facade over PreparedStatement. The object supports named parameters for SQL statements, eliminates checked exceptions, and provides the result of a SELECT operation as an efficient Stream<ResultSet>. The mapping of data from a ResultSet to domain objects is then handled by a separate class called ResultSetMapper<DOMAIN>. Its instance prepares the mapping model upon first use and subsequently reuses it, which significantly reduces the overhead when processing a large volume of queries.

Mapping class attributes to database columns can be specified using annotations from the jakarta.persistence package (@Table, @Column, @Id), but the library can infer some properties even without them. Both mutable JavaBeans and immutable Records are fully supported. Ujorm3 only works with M:1 relations—1:M collections are intentionally omitted to prevent the generation of hidden queries and N+1 problems. Relational columns of a SELECT statement can be mapped using their labels in the format "city.name", which contain a dot-chained list of Java attributes of the domain objects. However, it is better to use a type-safe metamodel.

Automatically generated Meta* classes enable safe column mapping without the use of typo-prone text strings. The use of a SELECT statement can then look like this, for example:

static final ResultSetMapper<Employee> EMPLOYEE_MAPPER =
        ResultSetMapper.of(Employee.class);

void select() {
    var sql = """
            SELECT ${COLUMNS}
            FROM employee e
            JOIN city c ON c.id = e.city_id
            LEFT JOIN employee b ON b.id = e.boss_id
            WHERE e.id > :employeeId
            """;

    var employees = SqlQuery.run(connection(), query -> query
            .sql(sql)
            .column("e.id", MetaEmployee.id)
            .column("e.name", MetaEmployee.name)
            .column("c.name", MetaEmployee.city, MetaCity.name)
            .column("c.country_code", MetaEmployee.city, MetaCity.countryCode)
            .column("b.name", MetaEmployee.boss, MetaEmployee.name)
            .bind("employeeId", 0L)
            .streamMap(EMPLOYEE_MAPPER.mapper())
            .toList());
}
Enter fullscreen mode Exit fullscreen mode

Please note that the domain class does not need to be registered anywhere in advance. For efficient work, however, I recommend creating a static mapper, whose implementation is prepared for multithreaded access. The column() method adds a database column with a label to the SQL template at the position of the ${COLUMNS} placeholder. An alternative label() method is also supported, allowing you to explicitly declare only column labels, thereby keeping the SQL query in the Java code closer to its native notation. However, these two approaches cannot be combined in a single query.

The EntityManager is used for working with entities, providing simple CRUD operations—including batch commands—through a Crud object. An interesting feature is the possibility of partial updates—the developer can specify an enumeration of columns to be updated, or pass the original object to the library, from which it will infer the changes itself. The mentioned classes are illustrated in a simplified class diagram. All listed methods are public:

Class diagram

Performance

Ujorm3 achieves very good results in benchmark tests, where it is compared with some popular ORM libraries. The mechanism of writing values to domain objects also contributes to the good score. Instead of the traditional approach using Java reflection, the library generates and compiles its own classes at runtime. Such an approach generally reduces memory requirements, minimizes overhead, and saves work for the Garbage Collector. The library has no dependencies on external libraries, and the compiled benchmark module (including the Ujorm3 library itself) is less than 3 MB, which is advantageous for microservices and embedded environments. However, it is good to keep in mind that in a production environment, in conjunction with slower databases, the differences in performance may partially blur.

Getting Started

To try the library in your Java 17+ project, simply add the dependency to your Maven configuration:

<dependency>
    <groupId>org.ujorm</groupId>
    <artifactId>ujorm-orm</artifactId>
    <version>3.0.0-RC1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

To automatically generate metamodel classes, add the optional APT configuration to the build element:

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.14.1</version>
        <configuration>
            <annotationProcessorPaths>
                <path>
                    <groupId>org.ujorm</groupId>
                    <artifactId>ujorm-meta-processor</artifactId>
                    <version>3.0.0-RC1</version>
                </path>
            </annotationProcessorPaths>
        </configuration>
    </plugin>
</plugins>
Enter fullscreen mode Exit fullscreen mode

The Ujorm module from the Benchmark project can be used as a template for a sample implementation. The library's codebase is currently covered by JUnit tests that utilize an in-memory H2 database (in addition to mocked objects). Before releasing the final version, I plan to add integration tests for PostgreSQL, MySQL, Oracle, and MS SQL Server databases.

When to Choose the Ujorm3 Library?

If you are working for a corporate client expecting standards or portability of abstractions between databases, use JPA/Hibernate instead. If you have already found an ORM framework that meets your expectations and needs, stick with it. However, if you are looking for a fast and transparent alternative without hidden mechanisms for your new project, the Ujorm3 library is definitely worth a try.

Useful Links:

Top comments (0)