Skip to main content

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:

  1. Ensures the configured database exists (creates it if missing, using rathorm.db.bootstrap).
  2. Scans configured packages for org.javalite.activejdbc.Model classes and/or fields annotated with @DbColumn.
  3. 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, autoIncrement
  • defaultValue, 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 @SubEntity relationships

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 via build()
  • DbFunction: a SqlExpression that specifically represents a function call
  • AbstractSqlExpression: base class with shared alias/cast handling
  • AggregateFunction: base class for aggregates with DISTINCT, ORDER BY, and FILTER (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 Entity into 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, format column:op:value
  • orderBy (or sort): repeatable order clauses, supports comma-separated values
  • limit / offset: classic pagination
  • page / pageSize (or per_page): page-based pagination (translated to limit/offset)

Supported criteria operators:

  • = / == / eq
  • != / <> / ne / neq
  • <, <= / lte, >, >= / gte
  • like, ilike
  • in, 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: Ensure rathorm.properties exists under src/main/resources/ (or otherwise in runtime classpath).
  • No entity classes found under packages [...]: Confirm rathorm.entity.packages points to packages containing your ActiveJDBC Model classes.
  • failed to determine Entity class name...: Ensure rathorm-instrumentation plugin ran (check build logs during process-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.