Litebridge is a fast, lightweight Object-Relational Mapper (ORM) for Java 21+.
It simplifies persistence by treating SQL as a first-class citizen, balancing relational power with the flexibility of plain Java objects - without requiring annotations or complex toolchains.
Philosophy: SQL-like, minimal magic. Litebridge favours programmatic configuration and developer intent over code-to-table generation and heavy abstraction.
- Lightweight: A pure Java library with minimal external dependencies.
- Modern: Built for Java 21+, leveraging modern idioms and features extensively.
- Efficient: Focuses on performance and minimising database round-trips via built-in DTO change tracking.
- Transparent Mapping: Map DTOs to databases without modifying your domain classes. Use a fluent API, programmatic
Map-based configuration or optional annotations. - Fluent API: Compose queries using a natural, SQL-like fluent builder that stays out of your way.
- Spring Integration: Use the Litebridge Spring Boot starter to easily integrate Litebridge with your Spring Boot application.
Given the following example Person and Account DTO classes:
public class Person {
private Long id;
private String name;
private String surname;
private int age;
private String eyeColour;
private List<Account> accounts;
// Getters and setters, etc
}
public class Account {
private Long id;
private String name;
private BigInteger balance;
private Person owner;
// Getters and setters, etc
}// Create a litebridgedb instance
Litebridge litebridge = new Litebridge(new H2DatabaseProvider(connection));
// Register the table mapping for the Person DTO class
litebridge.register(Person.class, rc -> rc.mapToTable("LB.PERSON")
.mapField("id").toColumn("PERSON_ID").autoIncrement().usingSequence("LB.PERSON_SEQ")
.mapField("name").toColumn("FIRST_NAME")
.mapField("surname").toColumn("SURNAME")
.mapField("age").toColumn("AGE")
.mapProperty("eyeColour").toColumn("EYE_COLOUR")
.mapField("accounts").oneToMany(c -> c.mappedByField("owner")));
// Register the table mapping for the Account DTO class
litebridge.register(Account.class, rc -> rc.mapToTable("LB.ACCOUNT")
.mapField("id").toColumn("ACCOUNT_ID").autoIncrement().usingSequence("LB.ACCOUNT_SEQ")
.mapField("name").toColumn("ACCOUNT_NAME")
.mapField("balance").toColumn("BALANCE")
.mapField("owner").toColumn("PERSON_ID").joinUsing());The register() method is used to register a DTO-table mapping. It takes a DTO class and a callback that
provides a fluent API for configuring the mapping.
mapField() and mapProperty() are used to specify a DTO field (and how to access it), while toColumn()
allows specification of a target mapped database table column, which can itself
be modified with further chained calls (such as the autoIncrement() and usingSequence() methods).
The table mappings above specify the following:
- For the
Personclass:- It is mapped to table
LB.PERSON. - The
idfield is mapped to thePERSON_IDcolumn. This column is "auto-incrementing", but with a value generated by the using theLB.PERSON_SEQsequence. - The
namefield is mapped to theFIRST_NAMEcolumn. - The
surnamefield is mapped to theSURNAMEcolumn. - The
agefield is mapped to theAGEcolumn. - The
eyeColourproperty is mapped to theEYE_COLOURcolumn. The DTO value is accessed via Java property accessors (getEyeColour()/setEyeColour()). - The
accountsfield is a one-to-many relationship, mapped by theownerfield in the target class. It is the reverse mapping ofAccount.owner.
- It is mapped to table
- For the
Accountclass:- It is mapped to table
LB.ACCOUNT. - The
idfield is mapped to theACCOUNT_IDcolumn. Values for this field are generated using theLB.PERSON_SEQsequence. - The
namefield is mapped to theACCOUNT_NAMEcolumn. - The
balancefield is mapped to theBALANCEcolumn. - The
ownerfield is a many-to-one mapping, with the foreign key mapped to columnPERSON_ID. The related DTO is specified by aJOIN USINGclause (i.e. joining onPERSON_IDin both tables) to fetch the targetPersoninstance.
- It is mapped to table
Data types, nullability and other column details are inferred from reflection and database metadata.
There are different mechanisms available for registering a DTO
final Person groupedPerson = new Person();
groupedPerson.setName("Alice");
groupedPerson.setSurname("Smith");
// Executes an INSERT statement for PERSON_ID, FIRST_NAME and SURNAME columns
litebridge.save(groupedPerson);Change tracking is initialised for the persisted DTO after the initial call to save() (if it wasn't already).
Subsequent calls to save() will result in UPDATEs as required (or no action if the DTO has not changed).
This is typically initialised directly using the track() method.
It allows manipulation of how Litebridge determines which SQL statement to use for persisting the DTO,
while avoiding the overhead of requiring a database round-trip:
final Person groupedPerson = new Person();
groupedPerson.setId(123L);
groupedPerson.setName("Alice");
groupedPerson.setSurname("Smith");
// Start tracking changes to the Person DTO
litebridge.track(groupedPerson);
groupedPerson.setName("Bob");
// Executes an UPDATE statement to set FIRST_NAME to 'Bob'
litebridge.save(groupedPerson);
groupedPerson.setAge(30);
groupedPerson.setEyeColour("brown");
// Executes an UPDATE statement to set AGE to 30 and EYE_COLOUR to 'brown'
litebridge.save(groupedPerson);Given it's "SQL-first" nature and focus on programmer intent, Litebridge allows explicitly inserting/updating a record,
instead of the default save():
final Person groupedPerson = new Person();
groupedPerson.setId(123L);
groupedPerson.setName("Alice");
groupedPerson.setSurname("Smith");
// Executes an INSERT statement for the populated DTO fields
litebridge.insert(groupedPerson);Similarly, an update() method is available to explicitly specify the use of an UPDATE statement.
Litebridge automatically cascades saves to related DTOs:
Person groupedPerson = new Person();
groupedPerson.setName("Alice");
groupedPerson.setSurname("Smith");
groupedPerson.setAge(20);
groupedPerson.setEyeColour("blue");
Account account = new Account();
account.setName("Account 1");
account.setBalance(BigInteger.valueOf(1000));
account.setOwner(groupedPerson);
// Save DTOs ("groupedPerson" will also be saved due to cascading)
litebridge.save(account);Litebridge provides a fluent API for constructing queries using a familiar SQL-like syntax:
final Optional<Person> alice = litebridge.select(Person.class)
.where("name").eq("Alice")
.and("surname").eq("Smith")
.orderBy("id").asc()
.first();If null is preferred as an empty response:
final Person alice = litebridge.select(Person.class)
.where("name").eq("Alice")
.and("surname").eq("Smith")
.orderBy("id").asc()
.firstOrNull();If exactly one result is expected from a query, the one(), oneOrNull() or oneOrThrow() terminals avoid boilerplate:
final Person alice = litebridge.select(Person.class)
.where("surname").eq("Smith")
.oneOrThrow(() -> new IllegalStateException("More than one groupedPerson with surname 'Smith'"));
// or simply oneOrThrow()Query results are available as streams:
litebridge.select(Person.class)
.where("eyeColour").isNull()
.stream()
.map(groupedPerson -> groupedPerson.setEyeColour("unknown"))
// etcResults can also be returned as a List:
final List<Person> allPersons = litebridge.select(Person.class)
.orderBy("id").asc()
.list();Or they can be looped through directly:
litebridge.select(Person.class)
.orderBy("id").asc()
.forEach(groupedPerson -> logger.info("Found groupedPerson: {}", groupedPerson));When selecting a DTO with that is related to another DTO (e.g. via a one-to-many expressed as a Collection in Java),
the fetch behaviour is specified by using JOINs.
If no JOINs are specified (or some are omitted), the fields for the corresponding related/nested DTOs will be null. This allows control over query behaviour, allowing only necessary data to be retrieved when dealing with complex object graphs.
To select an Account and also retrieve the related Person object in its owner field:
Account account = litebridge.select(Account.class)
.join(Person.class).on("owner")
.where("id").eq(234L)
.oneOrThrow();
// account.owner contains the related Person object
// and its "accounts" field will contain this Account objectRetrieving the reverse (a Person and their collection of associated Accounts) works the same way:
Person groupedPerson = litebridge.select(Person.class)
.join(Account.class).on("accounts")
.where("id").eq(123L)
.oneOrThrow();
// groupedPerson.accounts is nullThe same fluent API can be used to perform any SQL query, without requiring a DTO mapping:
litebridge.select("PERSON_ID", "FIRST_NAME", "SURNAME", "AGE").from("LB.PERSON")
.where("AGE").gt(18)
.and("AGE").lt(25)
.orderBy("PERSON_ID").asc()
.stream()
// Result rows are records containing column metadata and values
.peek(row -> row.column("PERSON_ID").ifPresent(column -> logger.info("Found PERSON_ID column: " + column.value())))
// SQL result rows can easily be converted to DTOs
.map(row -> litebridge.toDto(row, Person.class))
.forEach(p -> logger.info("Person DTO: " + p));Litebridge currently supports the following databases:
- H2:
litebridge-db-h2 - Oracle:
litebridge-db-oracle
Litebridge is modular, allowing you to include only the components you need.
The core engine. This is the primary dependency required for all applications using the ORM.
A collection of database provider modules. You only need to include the specific implementation for your database (or multiple if needed).
-
litebridge-db-h2:H2 database provider.
-
litebridge-db-oracle:Oracle database provider.
-
litebridge-db-spi:The Service Provider Interface (SPI) for implementing custom database providers. Not required for client use.
-
litebridge-db-impl:Provides a default implementation of the
DatabaseProviderSPI. Used to simplify the creation of new database providers.
Exposes the ChangeTracker API. This provides lightweight change tracking for arbitrary DTOs. While the ORM uses this internally for SQL optimisation, it can be used independently for other state-tracking needs.
Simple type conversion support for translating between Java types and SQL-specific types.
Internal utilities. Litebridge implements internal versions of common patterns to avoid bloating your project with large 3rd-party utility suites.
Spring Framework integration for Litebridge.
-
litebridge-spring:Core Litebridge-Spring integration.
Used for manual configuration of Litebridge Spring beans.
-
litebridge-spring-boot-starter:Spring Boot autoconfiguration for Litebridge. Used to integrate Litebridge with typical Spring Boot applications.
This is the only dependency needed for autoconfigured Spring Boot applications.
-
litebridge-spring-boot-autoconfigure:Spring Boot Autoconfiguration implementation.
Examples demonstrating how to use Litebridge.