Skip to content

faucamp/litebridge

Repository files navigation

Litebridge

Coverage Branches

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.

Key Features

  • 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.

Shortcuts

Quick Introduction

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
}

Setup: Register DTO-table mappings

// 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 Person class:
    • It is mapped to table LB.PERSON.
    • The id field is mapped to the PERSON_ID column. This column is "auto-incrementing", but with a value generated by the using the LB.PERSON_SEQ sequence.
    • The name field is mapped to the FIRST_NAME column.
    • The surname field is mapped to the SURNAME column.
    • The age field is mapped to the AGE column.
    • The eyeColour property is mapped to the EYE_COLOUR column. The DTO value is accessed via Java property accessors (getEyeColour()/setEyeColour()).
    • The accounts field is a one-to-many relationship, mapped by the owner field in the target class. It is the reverse mapping of Account.owner.
  • For the Account class:
    • It is mapped to table LB.ACCOUNT.
    • The id field is mapped to the ACCOUNT_ID column. Values for this field are generated using the LB.PERSON_SEQ sequence.
    • The name field is mapped to the ACCOUNT_NAME column.
    • The balance field is mapped to the BALANCE column.
    • The owner field is a many-to-one mapping, with the foreign key mapped to column PERSON_ID. The related DTO is specified by a JOIN USING clause (i.e. joining on PERSON_ID in both tables) to fetch the target Person instance.

Data types, nullability and other column details are inferred from reflection and database metadata.

There are different mechanisms available for registering a DTO

Persisting 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.

Cascading persistence

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);

Querying

Litebridge provides a fluent API for constructing queries using a familiar SQL-like syntax:

Retrieving a single DTO

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()

Retrieving multiple DTOs

Query results are available as streams:

litebridge.select(Person.class)
        .where("eyeColour").isNull()
        .stream()
        .map(groupedPerson -> groupedPerson.setEyeColour("unknown"))
        // etc

Results 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));

Retrieving related DTOs

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 object

Retrieving 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 null

Arbitrary SQL queries

The 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));

Supported databases

Litebridge currently supports the following databases:

  • H2: litebridge-db-h2
  • Oracle: litebridge-db-oracle

Documentation

Litebridge documentation

Project Structure

Litebridge is modular, allowing you to include only the components you need.

Core modules

litebridge-orm

Coverage Branches

The core engine. This is the primary dependency required for all applications using the ORM.

litebridge-db

A collection of database provider modules. You only need to include the specific implementation for your database (or multiple if needed).

  • litebridge-db-h2:

    Coverage Branches

    H2 database provider.

  • litebridge-db-oracle:

    Coverage Branches

    Oracle database provider.

  • litebridge-db-spi:

    Coverage Branches

    The Service Provider Interface (SPI) for implementing custom database providers. Not required for client use.

  • litebridge-db-impl:

    Coverage Branches

    Provides a default implementation of the DatabaseProvider SPI. Used to simplify the creation of new database providers.

litebridge-tracking

Coverage Branches

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.

Supporting modules

litebridge-converter

Coverage Branches

Simple type conversion support for translating between Java types and SQL-specific types.

litebridge-commons

Coverage Branches

Internal utilities. Litebridge implements internal versions of common patterns to avoid bloating your project with large 3rd-party utility suites.

spring

Spring Framework integration for Litebridge.

  • litebridge-spring:

    Coverage Branches

    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:

    Coverage Branches

    Spring Boot Autoconfiguration implementation.

Documentation and examples

docs

Litebridge documentation

example

Examples demonstrating how to use Litebridge.

About

Lightweight Java ORM focused on DTOs, with fast and simple data updates and change tracking

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages