RathORM
RathORM is a lightweight ORM + schema migration helper for PostgreSQL. It scans annotated classes, builds table metadata, and applies schema changes in a safe order (create tables, add columns, then constraints). It also provides a small SQL builder layer (WhereCriteria, OrderBy, GroupBy, LimitOffset, SqlQuery) and a composable SQL function/expression module under com.tranztechnologies.rathorm.functions.
Requirements
- Java
21 - PostgreSQL (JDBC)
- Maven
Key dependencies used internally:
- Spring Boot (web/security)
- ActiveJDBC (
org.javalite) - Reflections (
org.reflections)
Installation
This project is published as an internal package (see pom.xml distributionManagement).
If you consume it from another Maven project, configure GitHub Packages (or your internal repository) and add:
<dependency>
<groupId>com.tranztechnologies</groupId>
<artifactId>rathorm</artifactId>
<version><!-- your version --></version>
</dependency>
Configuration (rathorm.properties)
RathORM loads configuration from a rathorm.properties file on the classpath.
Database
Used by DbProperties.load():
# Required
rathorm.db.url=jdbc:postgresql://localhost:5432/mydb
rathorm.db.username=postgres
rathorm.db.password=postgres
# Optional (database used to bootstrap/create target DB when missing)
rathorm.db.bootstrap=postgres
Package Scanning
Used by ModelScanner.loadConfiguredPackages() to locate ActiveJDBC Model classes for schema sync:
# Comma-separated packages to scan for org.javalite.activejdbc.Model subclasses
rathorm.entity.packages=com.tranztechnologies.model
Schema Migration
Entry point: com.tranztechnologies.rathorm.RathORM.
What It Does
RathORM.create(...) performs:
- Ensures the configured database exists (creates it if missing, using
rathorm.db.bootstrap). - Scans configured packages for
org.javalite.activejdbc.Modelclasses and/or fields annotated with@DbColumn. - Synchronizes schema by creating missing tables (without constraints first), adding missing columns (optionally dropping extra columns), then applying constraints (PK/UK/FK/check/not null/defaults) after tables/columns exist.
Running Migration
import com.tranztechnologies.rathorm.RathORM;
// create/alter tables, do not drop anything
RathORM.create(false, false, false);
// drop everything first, then create
RathORM.create(false, true, false);
// drop-only mode
RathORM.create(true, false, false);
// drop extra columns not in models (use carefully)
RathORM.create(false, false, true);
Defining Tables (ActiveJDBC Models + @DbColumn)
RathORM derives table definitions from ActiveJDBC Model classes. Columns are declared via @DbColumn on fields.
The @DbColumn annotation supports:
- Name/type/length/precision/scale
nullable,unique,primaryKey,autoIncrementdefaultValue,check- Foreign keys:
referencesTable,referencesColumn,onUpdate,onDelete
See: src/main/java/com/tranztechnologies/rathorm/annotation/DbColumn.java.
Entities (Read Models) and Query Generation
RathORM differentiates between:
- Models (
org.javalite.activejdbc.Model): used primarily for schema metadata and persistence. - Entities (
com.tranztechnologies.rathorm.Entity): read/query models that describe how to select and join data and how to expose it as JSON-friendly maps.
How Entities Work
An Entity typically contains static fields describing:
- Base table columns via
EntityTable<T extends Model> - Optional child joins via
@ChildTable - Optional default query constraints via
@BaseCriteria - Optional default ordering via
@BaseOrderBy - Optional default grouping via
@BaseGroupBy - Optional one-to-many relationship subqueries via
@SubEntity
At runtime, QueryGenerator uses these annotations to build a SELECT with:
- Selected columns from base and child tables
- JOINs for
@ChildTable - JSON aggregation subqueries for
@SubEntityrelationships
Example Entity
Adapted from src/main/java/com/tranztechnologies/entity/UserEntity.java:
public class UserEntity extends Entity {
@BaseTable(alias = "users")
public static EntityTable<User> user = () -> new EntityTableColumn[] {
new EntityTableColumn("users", User.user_id, "user_id"),
new EntityTableColumn("users", User.first_name, "first_name"),
new EntityTableColumn("users", User.last_name, "last_name"),
};
@BaseCriteria
public static WhereCriteria criteria = WhereCriteria.eq("users.type_id", 1);
@ChildTable(alias = "user_type", join = Join.INNER, conditions = "user_type.Id = Users.Type_Id")
public static EntityTable<UserType> user_type = () -> new EntityTableColumn[] {
new EntityTableColumn("user_type", UserType.type, "user_type")
};
@SubEntity(conditions = "user_logs.user_Id = Users.Id")
public Class<UserLogEntity> logs = UserLogEntity.class;
@BaseOrderBy
public static OrderBy orderBy = OrderBy.asc("users.id");
}
Querying Entities
Entity exposes static helpers (which rely on instrumentation; see below):
// List all
EntityList<UserEntity> all = UserEntity.findAll();
// Filter
EntityList<UserEntity> admins = UserEntity.find(
WhereCriteria.eq("users.role", "admin")
.and(WhereCriteria.isNotNull("users.email"))
);
// First row
UserEntity first = UserEntity.findFirst(WhereCriteria.eq("users.id", 1));
// Count
long count = UserEntity.count(WhereCriteria.eq("users.active", true));
SQL Builder Primitives
WhereCriteria
- Holds a SQL fragment and an ordered parameter list
- Supports fluent composition (
and,or, grouping, common operators) - Encourages use of
?placeholders instead of string interpolation
WhereCriteria criteria = WhereCriteria.builder()
.eq("status", "active")
.and()
.startGroup()
.gte("created_at", "2025-01-01")
.or()
.isNull("created_at")
.endGroup()
.build();
OrderBy, GroupBy, LimitOffset, SqlQuery
These classes provide structured generation of:
ORDER BY ...GROUP BY ... [HAVING ...](including rollup/cube/grouping sets)LIMIT/OFFSET- Full query composition via
SqlQuery
SQL Functions & Expressions (com.tranztechnologies.rathorm.functions)
This module is a small OOP “expression tree” for generating SQL fragments.
Concepts
SqlExpression: anything that can render itself into SQL viabuild()DbFunction: aSqlExpressionthat specifically represents a function callAbstractSqlExpression: base class with shared alias/cast handlingAggregateFunction: base class for aggregates withDISTINCT,ORDER BY, andFILTER (WHERE ...)
All expressions are immutable: chaining .as(...), .cast(...), .distinct(), etc. returns a new instance.
Safety Note
These builders generate SQL strings. Do not pass untrusted user input into raw(...) (or any “raw string as SQL” method). Prefer parameterized queries (WhereCriteria with ?) for user input.
Factories
Use DbFunctions for convenience:
import static com.tranztechnologies.rathorm.functions.DbFunctions.*;
SqlExpression col = column("users.id"); // quoted when safe, otherwise left as-is
SqlExpression lit = literal("active"); // 'active'
SqlExpression sql = raw("NOW()"); // NOW()
SqlFunction fn = function("LOWER", column("users.email"));
JsonBuildObject
Create PostgreSQL Json_Build_Object(...):
JsonBuildObject obj = DbFunctions.jsonBuildObject()
.putColumn("id", "users.id")
.putLiteral("status", "active")
.putRaw("ts", "NOW()")
.build()
.as("payload");
ArrayAgg
Aggregate into arrays with optional DISTINCT, ORDER BY, and FILTER:
ArrayAgg agg = DbFunctions.arrayAgg(obj)
.distinct()
.orderBy(OrderBy.asc("users.created_at"))
.filter(WhereCriteria.isNotNull("users.id"))
.as("items");
Coalesce
Build Coalesce(a, b, ...) with optional cast:
Coalesce c = DbFunctions.coalesce(
DbFunctions.column("users.nickname"),
DbFunctions.literal("anonymous")
).cast("text");
Using Expressions in Selects
You can place expressions into EntityTableColumn:
import com.tranztechnologies.rathorm.EntityTableColumn;
import com.tranztechnologies.rathorm.functions.DbFunctions;
EntityTableColumn col = new EntityTableColumn(
DbFunctions.function("LOWER", DbFunctions.column("users.email")).as("email_lc"),
"email_lc"
);
Instrumentation (Required for Static Entity Helpers)
Entity contains static helpers like UserEntity.findAll(), but they require a generated method entityClass() to exist on each subclass. This is provided by the rathorm-instrumentation Maven plugin.
What The Plugin Does
During process-classes, it scans compiled classes and for each Entity subclass it:
- Adds missing static methods from
Entityinto the subclass - Rewrites
entityClass()to return the correct subclass type
Without instrumentation, calls will fail with:
failed to determine Entity class name, are you sure models have been instrumented?
Maven Configuration
Your consuming project should apply the plugin (already configured in this repository’s pom.xml):
<plugin>
<groupId>com.tranztechnologies</groupId>
<artifactId>rathorm-instrumentation</artifactId>
<version><!-- plugin version --></version>
<executions>
<execution>
<goals>
<goal>instrument</goal>
</goals>
</execution>
</executions>
</plugin>
Spring Integration (Optional)
EntityService is a Spring @Service that supports basic CRUD-style operations over Entity classes (typically from a controller/router). It parses criteria/order/limit/offset from the HTTP request and returns a MapResponse.
If you want to expose dynamic entity endpoints, wire a controller that delegates to EntityService.
HTTP Query Parameters
When using EntityService.listAll(...), the following query parameters are recognized:
criteria: repeatable filter clauses, formatcolumn:op:valueorderBy(orsort): repeatable order clauses, supports comma-separated valueslimit/offset: classic paginationpage/pageSize(orper_page): page-based pagination (translated to limit/offset)
Supported criteria operators:
=/==/eq!=/<>/ne/neq<,<=/lte,>,>=/gtelike,ilikein,notin/nin(values are comma-separated:status:in:active,pending)isnull,isnotnull(value part is ignored)
Examples:
GET /users?criteria=users.type_id:eq:1&criteria=users.active:eq:true
GET /users?orderBy=users.created_at:desc&limit=50&offset=0
GET /users?criteria=users.id:in:1,2,3&sort=users.id:asc
Troubleshooting
Could not find rathorm.properties on the classpath: Ensurerathorm.propertiesexists undersrc/main/resources/(or otherwise in runtime classpath).No entity classes found under packages [...]: Confirmrathorm.entity.packagespoints to packages containing your ActiveJDBCModelclasses.failed to determine Entity class name...: Ensurerathorm-instrumentationplugin ran (check build logs duringprocess-classes).
Development
Build:
./mvnw -q clean package
Notes:
- This repository currently does not include automated tests under
src/test/java. - When changing instrumentation behaviour, validate by running a build and inspecting that entity subclasses contain the generated static methods.