1. Domain Model

1.1. Domain modelling

  • Declarative, not magical

  • Visible and localized behavior

  • Safe, testable, Java-native APIs

  • Avoiding runtime introspection/config injection

Codion’s domain model layer is a declarative, type-safe representation of the underlying database schema, designed to provide expressive CRUD functionality without annotation overhead. At its heart is the Entity interface — representing a single row of data and its modifiable state, providing access to attribute values via its get() and set() methods.

1.2. Core classes

Domain

Specifies a domain model, containing entity definitions, procedures, functions and reports. A Codion domain model is implemented by extending the DomainModel class and populating it with entity definitions.

DomainType

A unique identifier for a domain model and a factory for EntityType instances associated with that domain model.

EntityType

A unique identifier for an entity type and a factory for Attribute instances associated with that entity type.

Attribute

A typed identifier for a column, foreign key or transient attribute, usually a Column or ForeignKey, allowing for type safe access to the associated value. Attributes are usually wrapped in an interface, serving as a convenient namespace.

attribute diagram
Column

An Attribute subclass representing a table column.

ForeignKey

An attribute subclass representing a foreign key relationship.

EntityDefinition

Encapsulates the meta-data required for presenting and persisting an entity.

AttributeDefinition

Each Attribute has an associated AttributeDefinition (or one of its subclasses) which encapsulates the meta-data required for presenting and persisting the associated value.

Entity

Represents a row in a table (or query) and maps Attributes to their associated values while keeping track of values which have been modified since they were initially set.

entity diagram
Entity.Key

Represents a unique key for a given entity.

1.3. Domain API

To define a domain model API we:

  • Create a DomainType constant representing the domain.

  • Use the DomainType to create EntityType constants for each table, wrapped in a namespace interface.

  • Use the EntityTypes to create Column constants for each column and a ForeignKey constant for each foreign key.

These constants represent the domain API and are used when referring to tables, columns or foreign keys.

Note
The use of constant interfaces is discouraged in modern Java practice because all implementing classes inherit the constants, potentially polluting their namespaces. If this is a concern, use a public static final constants class instead — at the cost of slightly more typing.
public interface Store {

  DomainType DOMAIN = DomainType.domainType("Store");

  interface City {
    EntityType TYPE = DOMAIN.entityType("store.city"); //(1)

    Column<Integer> ID = TYPE.integerColumn("id"); //(2)
    Column<String> NAME = TYPE.stringColumn("name");
  }

  interface Customer {
    EntityType TYPE = DOMAIN.entityType("store.customer");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<Integer> CITY_ID = TYPE.integerColumn("city_id");

    ForeignKey CITY_FK = TYPE.foreignKey("city", CITY_ID, City.ID);
  }
}
  1. The DomainType instance serves as a factory for EntityTypes associated with that domain.

  2. Each EntityType instance serves as a factory for Columns and ForeignKeys associated with that entity.

Typically, the underlying table name is used as the EntityType name, but you can use whatever identifying string you want and specify the table name via the tableName() builder method when defining the entity.

The underlying column name is typically used as the Column name, but as with the EntityType you can use whatever value you want and specify the column name via the columnName() method when creating the associated ColumnDefinition.

1.4. Domain implementation

The domain model is implemented by extending the DomainModel class and populating it with EntityDefinitions based on the domain tables. An EntityDefinition consists of AttributeDefinitions based on the Attributes associated with the entity and the information required to persist and present the entity.

The EntityType and Attribute constants provide define() methods returning builders which allow for further configuration (such as nullability and maximum length for values and the caption and primary key generator for the entity definition).

Tip
Omitting a caption marks an attribute as hidden. Hidden attributes won’t appear in table views by default.
public static class StoreImpl extends DomainModel {

  public StoreImpl() {
    super(Store.DOMAIN); //(1)
    add(city(), customer());
  }

  EntityDefinition city() {
    return City.TYPE.define(
                    City.ID.define()
                            .primaryKey(),
                    City.NAME.define()
                            .column()
                            .caption("Name")
                            .nullable(false))
            .keyGenerator(KeyGenerator.identity())
            .caption("Cities")
            .build();
  }

  EntityDefinition customer() {
    return Customer.TYPE.define(
                    Customer.ID.define()
                            .primaryKey(),
                    Customer.NAME.define()
                            .column()
                            .caption("Name")
                            .maximumLength(42),
                    Customer.CITY_ID.define()
                            .column(),
                    Customer.CITY_FK.define()
                            .foreignKey()
                            .caption("City"))
            .keyGenerator(KeyGenerator.identity())
            .caption("Customers")
            .build();
  }
}
  1. The DomainType constant is a required constructor parameter.

Domain is a Service Provider Interface (SPI), and it is recommended to configure the domain implementation class for the Service Loader. Without the Service Loader you are restricted to a local JDBC connection, since you must manually provide a domain instance when establishing a connection, instead of just referring to the DomainType constant.

src/main/java/module-info.java

provides is.codion.framework.domain.Domain
          with is.codion.demos.store.domain.StoreImpl;

or if not using Java Modules (JPMS)

src/main/resources/META-INF/services/is.codion.framework.domain.Domain

is.codion.demos.store.domain.StoreImpl
Note
The domain model implementation must be on the classpath when running with a local JDBC connection, but when running with an RMI or HTTP connection the domain model API is sufficient. If you foresee using RMI or HTTP connections it is recommended to split your domain model into separate API and implementation modules, to simplify client configurations (see Chinook and World demo applications, see Petclinic for a simple single class domain model).

The domain model provides an Entities instance via entities(), which contains the entity definitions and serves as a factory for Entity and Entity.Key instances.

Domain store = new StoreImpl();

Entities entities = store.entities();

Entity city = entities.entity(City.TYPE)
        .with(City.NAME, "Reykjavík")
        .build();

Entity.Key customerKey = entities.key(Customer.TYPE)
        .with(Customer.ID, 42)
        .build();

Entity customer = Entity.builder(customerKey)
        .with(Customer.NAME, "John")
        .with(Customer.CITY_FK, city)
        .build();

EntityDefinition customerDefinition = entities.definition(Customer.TYPE);

EntityDefinition cityDefinition = customerDefinition.foreignKeys().referencedBy(Customer.CITY_FK);

List<Column<?>> cityPrimaryKeyColumns = cityDefinition.primaryKey().columns();

1.5. Data type mapping

Java type SQL type

Short

java.sql.Types.SMALLINT

Integer

java.sql.Types.INTEGER

Double

java.sql.Types.DOUBLE

Long

java.sql.Types.BIGINT

BigDecimal

java.sql.Types.DECIMAL

LocalDateTime

java.sql.Types.TIMESTAMP

LocalDate

java.sql.Types.DATE

LocalTime

java.sql.Types.TIME

OffsetTime

java.sql.Types.TIME_WITH_TIMEZONE

OffsetDateTime

java.sql.Types.TIMESTAMP_WITH_TIMEZONE

java.util.Date

java.sql.Types.DATE

java.sql.Time

java.sql.Types.TIME

java.sql.Date

java.sql.Types.DATE

java.sql.Timestamp

java.sql.Types.TIMESTAMP

String

java.sql.Types.VARCHAR

Boolean

java.sql.Types.BOOLEAN

Character

java.sql.Types.CHAR

byte[]

java.sql.Types.BLOB

1.6. Foreign keys

1.6.1. Examples

A simple foreign key based on a single column.

ForeignKey CAPITAL_FK = TYPE.foreignKey("capital_fk", CAPITAL, City.ID);

Foreign key based on a composite key.

interface Parent {
  EntityType<Entity> TYPE = DOMAIN.entityType("parent");

  Column<Integer> ID_1 = TYPE.integerColumn("id1");
  Column<Integer> ID_2 = TYPE.integerColumn("id2");
}

interface Child {
  EntityType<Entity> TYPE = DOMAIN.entityType("child");

  Column<Integer> PARENT_ID_1 = TYPE.integerColumn("parent_id1");
  Column<Integer> PARENT_ID_2 = TYPE.integerColumn("parent_id2");

  ForeignKey PARENT_FK = TYPE.foreignKey("parent",
          PARENT_ID_1, Parent.ID_1,
          PARENT_ID_2, Parent.ID_2);
}

Domain constant definitions for the World demo application (simplified).

public interface World {

  DomainType DOMAIN = DomainType.domainType(World.class);

  interface City {
    EntityType TYPE = DOMAIN.entityType("world.city");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<String> COUNTRY_CODE = TYPE.stringColumn("countrycode");
    Column<String> DISTRICT = TYPE.stringColumn("district");
    Column<Integer> POPULATION = TYPE.integerColumn("population");

    ForeignKey COUNTRY_FK = TYPE.foreignKey("country_fk", COUNTRY_CODE, Country.CODE);
  }

  interface Country {
    EntityType TYPE = DOMAIN.entityType("world.country");

    Column<String> CODE = TYPE.stringColumn("code");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<String> CONTINENT = TYPE.stringColumn("continent");
    Column<String> REGION = TYPE.stringColumn("region");
    Column<Double> SURFACEAREA = TYPE.doubleColumn("surfacearea");
    Column<Integer> INDEPYEAR = TYPE.integerColumn("indepyear");
    Column<Integer> POPULATION = TYPE.integerColumn("population");
    Column<Double> LIFE_EXPECTANCY = TYPE.doubleColumn("lifeexpectancy");
    Column<Double> GNP = TYPE.doubleColumn("gnp");
    Column<Double> GNPOLD = TYPE.doubleColumn("gnpold");
    Column<String> LOCALNAME = TYPE.stringColumn("localname");
    Column<String> GOVERNMENTFORM = TYPE.stringColumn("governmentform");
    Column<String> HEADOFSTATE = TYPE.stringColumn("headofstate");
    Column<Integer> CAPITAL = TYPE.integerColumn("capital");
    Column<String> CODE_2 = TYPE.stringColumn("code2");
    Column<Integer> CAPITAL_POPULATION = TYPE.integerColumn("capital_population");
    Column<Integer> NO_OF_CITIES = TYPE.integerColumn("no_of_cities");
    Column<Integer> NO_OF_LANGUAGES = TYPE.integerColumn("no_of_languages");
    Column<byte[]> FLAG = TYPE.byteArrayColumn("flag");

    ForeignKey CAPITAL_FK = TYPE.foreignKey("capital_fk", CAPITAL, City.ID);
  }

  interface CountryLanguage {
    EntityType TYPE = DOMAIN.entityType("world.countrylanguage");

    Column<String> COUNTRY_CODE = TYPE.stringColumn("countrycode");
    Column<String> LANGUAGE = TYPE.stringColumn("language");
    Column<Boolean> IS_OFFICIAL = TYPE.booleanColumn("isofficial");
    Column<Double> PERCENTAGE = TYPE.doubleColumn("percentage");
    Column<Integer> NO_OF_SPEAKERS = TYPE.integerColumn("noOfSpeakers");

    ForeignKey COUNTRY_FK = TYPE.foreignKey("country_fk", COUNTRY_CODE, Country.CODE);
  }

  interface Continent {
    EntityType TYPE = DOMAIN.entityType("continent");

    Column<String> NAME = TYPE.stringColumn("continent");
    Column<Integer> SURFACE_AREA = TYPE.integerColumn("sum(surfacearea)");
    Column<Long> POPULATION = TYPE.longColumn("sum(population)");
    Column<Double> MIN_LIFE_EXPECTANCY = TYPE.doubleColumn("min(lifeexpectancy)");
    Column<Double> MAX_LIFE_EXPECTANCY = TYPE.doubleColumn("max(lifeexpectancy)");
    Column<Integer> MIN_INDEPENDENCE_YEAR = TYPE.integerColumn("min(indepyear)");
    Column<Integer> MAX_INDEPENDENCE_YEAR = TYPE.integerColumn("max(indepyear)");
    Column<Double> GNP = TYPE.doubleColumn("sum(gnp)");
  }
}

1.7. Attributes

For the framework to know how to present and persist values, Attributes need further configuration. Each attribute is represented by the AttributeDefinition class or one of its subclasses, which encapsulates the required metadata.

The Attribute, Column and ForeignKey classes provide methods for creating AttributeDefinition.Builder instances, which can be used to configure the attributes.

An Attribute can be configured three ways, as transient, derived or denormalized.

1.7.1. Transient

Transient attributes are nullable by default and behave like regular fields, but do not map to any underlying column. Transient attributes are always initialized with a null value. Changing the value of a transient attribute renders the Entity instance modified by default, but can be configured to not do so.

1.7.2. Denormalized

An entity can include a read-only attribute value from an entity referenced via foreign key, by defining a denormalized attribute.

Country.CAPITAL_POPULATION.define()
        .denormalized()
        .from(Country.CAPITAL_FK)
        .attribute(City.POPULATION)
        .caption("Capital pop.")
        .numberGrouping(true),

1.7.3. Derived

A derived attribute is used to represent a value which is derived from one or more attributes in the same entity. The value of a derived attribute is provided via a DerivedValue implementation as shown below.

CountryLanguage.NO_OF_SPEAKERS.define()
        .derived()
        .from(CountryLanguage.COUNTRY_FK, CountryLanguage.PERCENTAGE)
        .value(new NoOfSpeakers())
        .caption("No. of speakers")
        .numberGrouping(true),
final class NoOfSpeakers implements DerivedValue<Integer> {

  @Serial
  private static final long serialVersionUID = 1;

  @Override
  public Integer get(SourceValues source) {
    Double percentage = source.get(CountryLanguage.PERCENTAGE);
    Entity country = source.get(CountryLanguage.COUNTRY_FK);
    if (percentage != null && country != null && !country.isNull(Country.POPULATION)) {
      return Double.valueOf(country.get(Country.POPULATION) * (percentage / 100)).intValue();
    }

    return null;
  }
}

1.8. Columns

1.8.1. Column

Column is used to represent attributes that are based on table columns.

Country.REGION.define()
        .column()
        .caption("Region")
        .nullable(false)
        .maximumLength(26),
Country.SURFACEAREA.define()
        .column()
        .caption("Surface area")
        .nullable(false)
        .numberGrouping(true)
        .fractionDigits(2),
Country.INDEPYEAR.define()
        .column()
        .caption("Indep. year")
        .range(-2000, 2500),
Country.INDEPYEAR_SEARCHABLE.define()
        .column()
        .expression("to_char(indepyear)")
        .searchable(true)
        .readOnly(true),
Country.POPULATION.define()
        .column()
        .caption("Population")
        .nullable(false)
        .numberGrouping(true),
Country.LIFE_EXPECTANCY.define()
        .column()
        .caption("Life expectancy")
        .fractionDigits(1)
        .range(0, 99),
Lazy loading

A column can be specified as being not selected, which means its value is not selected by default. Note that entitities contain a null value by default for lazy loaded columns.

Country.FLAG.define()
        .column()
        .caption("Flag")
        .selected(false),

1.8.2. Primary key

It is recommended that entities have a primary key defined, that is, one or more columns representing a unique combination.

The primary key defined in the domain model does not have to correspond to an actual table primary (or unique) key, although that is of course preferable.

If no primary key columns are specified, equals() will not work (since it is based on the primary key). You can still use the Entity.equalValues() method to check if all values are equal in two entities without primary keys.

Country.CODE.define()
        .primaryKey()
        .caption("Code")
        .updatable(true)
        .maximumLength(3),

In case of composite primary keys you simply specify the primary key index.

CountryLanguage.COUNTRY_CODE.define()
        .primaryKey(0)
        .updatable(true),
CountryLanguage.LANGUAGE.define()
        .primaryKey(1)
        .caption("Language")
        .updatable(true),

1.8.3. Subquery

A Column can represent a subquery returning a single value.

Country.NO_OF_CITIES.define()
        .subquery("""
                SELECT COUNT(*)
                FROM world.city
                WHERE city.countrycode = country.code""")
        .caption("No. of cities"),

1.8.4. Boolean

For databases supporting Types.BOOLEAN you simply specify a column.

CountryLanguage.IS_OFFICIAL.define()
        .column()
        .caption("Official")
        .withDefault(true)
        .nullable(false),

For databases lacking native boolean support we can define a boolean column and provide a converter.

Item.DISABLED.define()
        .column()
        .converter(Integer.class, new BooleanConverter())
        .caption(Item.DISABLED.name())
        .name("disabled")
        .defaultValue(false)
        .nullable(false)
private static final class BooleanConverter implements Converter<Boolean, Integer> {

  @Override
  public Integer toColumnValue(Boolean value, Statement statement) throws SQLException {
    return value ? 1 : 0;
  }

  @Override
  public Boolean fromColumnValue(Integer columnValue) throws SQLException {
    return columnValue.intValue() == 1;
  }
}

Note that boolean attributes always use the boolean Java type, the framework handles translating to and from the actual column values.

entity.set(Customer.ACTIVE, true);

Boolean isActive = entity.get(Customer.ACTIVE);

1.8.5. Item

A column based on a list of valid items.

private static final List<Item<String>> CONTINENT_ITEMS = List.of(
        item("Africa"), item("Antarctica"), item("Asia"),
        item("Europe"), item("North America"), item("Oceania"),
        item("South America"));
Country.CONTINENT.define()
        .column()
        .items(CONTINENT_ITEMS)
        .caption("Continent")
        .nullable(false),

1.8.6. Group by

Codion provides built-in support for grouped entities through column-level groupBy() and aggregate() configuration. This is the preferred approach for working with aggregated data, offering type-safe aggregate column handling and automatic HAVING clause generation in the model layer.

Defining grouped entities

A grouped entity is defined by marking columns as either group-by columns or aggregate columns:

EntityDefinition continent() {
  return Continent.TYPE.define(
                  Continent.NAME.define()
                          .column()
                          .caption("Continent")
                          .groupBy(true),
                  Continent.SURFACE_AREA.define()
                          .column()
                          .caption("Surface area")
                          .expression("sum(surfacearea)")
                          .aggregate(true)
                          .numberGrouping(true),
                  Continent.POPULATION.define()
                          .column()
                          .caption("Population")
                          .expression("sum(population)")
                          .aggregate(true)
                          .numberGrouping(true),
                  Continent.MIN_LIFE_EXPECTANCY.define()
                          .column()
                          .caption("Min. life expectancy")
                          .expression("min(lifeexpectancy)")
                          .aggregate(true),
                  Continent.MAX_LIFE_EXPECTANCY.define()
                          .column()
                          .caption("Max. life expectancy")
                          .expression("max(lifeexpectancy)")
                          .aggregate(true),
                  Continent.MIN_INDEPENDENCE_YEAR.define()
                          .column()
                          .caption("Min. ind. year")
                          .expression("min(indepyear)")
                          .aggregate(true),
                  Continent.MAX_INDEPENDENCE_YEAR.define()
                          .column()
                          .caption("Max. ind. year")
                          .expression("max(indepyear)")
                          .aggregate(true),
                  Continent.GNP.define()
                          .column()
                          .caption("GNP")
                          .expression("sum(gnp)")
                          .aggregate(true)
                          .numberGrouping(true))
          .table("world.country")
          .readOnly(true)
          .description("Continents of the World")
          .caption("Continent")
          .build();
}

Key points about grouped entities:

  • Group-by columns: Marked with .groupBy(true) - these columns appear in the GROUP BY clause

  • Aggregate columns: Marked with .aggregate(true) and must specify an .expression() containing the aggregate function (e.g., sum(surfacearea), min(lifeexpectancy))

  • All columns required: Every column must be either a group-by column or an aggregate column - the framework validates this at entity definition time

  • Read-only: Grouped entities are typically marked .readOnly(true) since aggregated data cannot be updated

  • Base table: Use .table() to specify the underlying table (e.g., .table("world.country")) in case the entityType name is different.

Querying grouped entities

When using grouped entities, the framework handles aggregate columns differently than regular columns:

In the connection layer (Select.Builder):

Conditions on aggregate columns must be explicitly added to the HAVING clause:

// Regular column condition - goes in WHERE clause automatically
connection.select(Select.where(Continent.NAME.equalTo("Europe")).build());

// Aggregate column condition - must use having() explicitly
connection.select(Select.having(Continent.POPULATION.greaterThan(100_000_000L)).build());

In the model layer (EntityTableConditionModel):

The framework model layer automatically handles aggregate columns - conditions on aggregate columns are automatically placed in the HAVING clause without explicit configuration:

SwingEntityTableModel continentModel = new SwingEntityTableModel(Continent.TYPE, connectionProvider);

// Condition on regular column (NAME) - automatically goes in WHERE
continentModel.queryModel().condition().get(Continent.NAME).set().equalTo("Asia");

// Condition on aggregate column (POPULATION) - automatically goes in HAVING
continentModel.queryModel().condition().get(Continent.POPULATION).set().greaterThan(1_000_000_000L);

// Both conditions work together correctly
continentModel.items().refresh();

This automatic HAVING clause handling makes grouped entities seamless to use in UI components.

When to use grouped entities vs. custom queries

Prefer grouped entities when:

  • Working with straightforward aggregations on a single table

  • Building interactive UIs where users filter aggregated data

  • You want automatic HAVING clause handling in table models

  • Type-safe access to aggregate columns is important

Use custom queries (EntitySelectQuery) when:

  • Aggregating across complex joins of multiple tables

  • The query requires custom SQL logic beyond standard GROUP BY

  • You need database-specific optimizations or hints

Prefer database views when:

  • The aggregation is complex and performance-critical

  • Multiple applications need the same aggregated data

  • DBAs need to optimize with indexes or materialized views

  • The aggregation logic is stable and rarely changes

1.8.7. Column templates

Column templates provide a way to define reusable column configurations that can be applied across multiple entities. This eliminates repetition and ensures consistency for common patterns like audit columns, required searchable fields, or domain-specific column types.

Templates are defined using the ColumnTemplate functional interface, which extends Function<Column<T>, ColumnDefinition.Builder<T, ?>>.

private static final ColumnTemplate<String> REQUIRED_SEARCHABLE =
        column -> column.define()
                .column()
                .nullable(false)
                .searchable(true);
private static final ColumnTemplate<LocalDateTime> INSERT_TIME =
        column -> column.define()
                .column()
                .readOnly(true)
                .captionResource(Chinook.class.getName(), "insert_time");
private static final ColumnTemplate<String> INSERT_USER =
        column -> column.define()
                .column()
                .readOnly(true)
                .captionResource(Chinook.class.getName(), "insert_user");

The templates above demonstrate common patterns:

  • REQUIRED_SEARCHABLE - Combines nullable(false) with searchable(true) for required fields that should be used in search fields.

  • INSERT_TIME and INSERT_USER - Audit columns that track when and by whom records were created, using shared resource bundle keys for consistent captions across entities

Templates are applied using the column(ColumnTemplate<T>) method:

Album.TITLE.define()
        .column(REQUIRED_SEARCHABLE)
        .maximumLength(160),
Album.INSERT_TIME.define()
        .column(INSERT_TIME),
Album.INSERT_USER.define()
        .column(INSERT_USER))

Templates can be chained with additional configuration methods, allowing you to apply a base configuration and then customize specific aspects as needed.

Templates can be based on constants or static methods for more flexibility.

public static final class Store extends DomainModel {

  public static final DomainType DOMAIN = DomainType.domainType("store");

  private static final ColumnTemplate<String> NAME = column ->
          column.define()
                  .column()
                  .nullable(false)
                  .maximumLength(50);

  private static <T extends Number> ColumnTemplate<T> positiveNumber(double maximum) {
    return column -> column.define()
            .column()
            .nullable(false)
            .minimum(0)
            .maximum(maximum);
  }

  interface Customer {
    EntityType TYPE = DOMAIN.entityType("store.customer");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> FIRST_NAME = TYPE.stringColumn("first_name");
    Column<String> LAST_NAME = TYPE.stringColumn("last_name");
    Column<Integer> BIRTH_YEAR = TYPE.integerColumn("age");
    Column<Double> DISCOUNT = TYPE.doubleColumn("discount");
  }

  public Store() {
    super(DOMAIN);
    add(customer());
  }

  EntityDefinition customer() {
    return Customer.TYPE.define(
                    Customer.ID.define()
                            .primaryKey(),
                    Customer.FIRST_NAME.define()
                            .column(NAME)
                            .caption("First Name"),
                    Customer.LAST_NAME.define()
                            .column(NAME)
                            .caption("Last Name"),
                    Customer.BIRTH_YEAR.define()
                            .column(positiveNumber(2100))
                            .caption("Age"),
                    Customer.DISCOUNT.define()
                            .column(positiveNumber(8))
                            .defaultValue(0d)
                            .caption("Discount"))
            .keyGenerator(identity())
            .build();
  }
}

This approach provides several benefits:

  • Parameterization - Templates can accept configuration parameters at usage time

  • Reusability - Common patterns can be shared across different domains with variations

  • Environment awareness - Templates can adapt based on runtime configuration

  • Type safety - The method signature ensures correct types for parameters

  • Documentation - Method parameters make the configuration options explicit

Real-world example: Geospatial column templates

Column templates are particularly powerful when working with complex data types that require converters and custom formatting. Here’s an example from production applications using PostGIS geometries:

public final class GeospatialTemplates {

    // Template for Point geometries with custom formatting
    public static final ColumnTemplate<Point> POINT = column ->
        column.define()
            .column()
            .converter(PGgeometry.class, new PGgeometryPointConverter(), new PGgeometryGetter())
            .format(new PointFormat());

    // Template for LineString geometries
    public static final ColumnTemplate<LineString> LINE = column ->
        column.define()
            .column()
            .converter(PGgeometry.class, new PGgeometryLineConverter(), new PGgeometryGetter());

    // Template for Polygon geometries
    public static final ColumnTemplate<Polygon> POLYGON = column ->
        column.define()
            .column()
            .converter(PGgeometry.class, new PGgeometryPolygonConverter(), new PGgeometryGetter());

    // Template for MultiPolygon geometries
    public static final ColumnTemplate<MultiPolygon> MULTI_POLYGON = column ->
        column.define()
            .column()
            .converter(PGgeometry.class, new PGgeometryMultiPolygonConverter(), new PGgeometryGetter());
}

// Usage in entity definitions:
EntityDefinition location() {
    return Location.TYPE.define(
        Location.ID.define()
            .primaryKey(),
        Location.NAME.define()
            .column()
            .nullable(false),
        Location.COORDINATES.define()
            .column(GeospatialTemplates.POINT),
        Location.BOUNDARY.define()
            .column(GeospatialTemplates.POLYGON),
        Location.ROUTE.define()
            .column(GeospatialTemplates.LINE))
    .build();
}

These templates demonstrate how column templates can:

  • Encapsulate complexity - The converter, getter, and formatter classes can remain private implementation details

  • Ensure consistency - All geometry columns of the same type use identical configuration

  • Reduce boilerplate - Complex type configuration is defined once and reused everywhere

  • Improve maintainability - Changes to geometry handling are centralized in the template definitions

1.9. Domain

Each entity is defined by creating a EntityDefinition.Builder instance via EntityType.define() and adding the resulting definition to the domain model, via the add(EntityDefinition) method in the DomainModel class. The framework assumes the entityType name is the underlying table name, but the tableName can be specified via EntityDefinition.Builder.tableName(String) method.

EntityDefinition city() {
  return City.TYPE.define(
                  City.ID.define()
                          .primaryKey(),
                  City.NAME.define()
                          .column()
                          .caption("Name")
                          .searchable(true)
                          .nullable(false)
                          .maximumLength(35),
                  City.COUNTRY_CODE.define()
                          .column()
                          .nullable(false),
                  City.COUNTRY_FK.define()
                          .foreignKey()
                          .caption("Country"),
                  City.DISTRICT.define()
                          .column()
                          .caption("District")
                          .nullable(false)
                          .maximumLength(20),
                  City.POPULATION.define()
                          .column()
                          .caption("Population")
                          .nullable(false)
                          .numberGrouping(true),
                  City.LOCATION.define()
                          .column()
                          .caption("Location")
                          .converter(String.class, new LocationConverter())
                          .comparator(new LocationComparator()))
          .keyGenerator(sequence("world.city_seq"))
          .validator(new CityValidator())
          .orderBy(ascending(City.NAME))
          .formatter(City.NAME)
          .description("Cities of the World")
          .caption("City")
          .build();
}

1.10. KeyGenerator

The framework provides implementations for most commonly used primary key generation strategies, identity column, sequence (with or without trigger) and auto-increment columns. The KeyGenerator class serves as a factory for KeyGenerator implementations. Static imports are assumed in the below examples.

1.10.1. Identity

Based on identity columns, supported by most DBMSs.

.keyGenerator(identity())

1.10.2. Automatic

This assumes the underlying primary key column is either an auto-increment column or is populated from a sequence using a trigger during insert.

//Auto increment column in the 'store.customer' table
.keyGenerator(automatic("store.customer"));

//Trigger and a sequence named 'store.customer_seq'
.keyGenerator(automatic("store.customer_seq"));

1.10.3. Sequence

When sequences are used without triggers the framework can fetch the value from a sequence before insert.

.keyGenerator(sequence("world.city_seq"))

1.10.4. Queried

The framework can select new primary key values from a query.

.keyGenerator(queried("""
        select next_id
        from store.id_values
        where table_name = 'store.customer'"""))

1.10.5. Custom

You can provide a custom key generator strategy by implementing a KeyGenerator.

private static final class UUIDKeyGenerator implements KeyGenerator {

  @Override
  public void beforeInsert(Entity entity, DatabaseConnection connection) {
    entity.set(Customer.ID, UUID.randomUUID().toString());
  }
}

1.11. EntityFormatter

The EntityFormatter class provides a builder for a Function<Entity, String> instance, which is then used to provide the toString() implementations for entities. This value is used wherever entities are displayed, for example in a ComboBox or as foreign key values in table views.

Entity.toString() values are cached by default and invalidated each time an attribute value changes. This caching can be turned off via EntityDefinition.Builder.cacheToString(boolean)

return Address.TYPE.define(
                Address.ID.define()
                        .primaryKey(),
                Address.STREET.define()
                        .column()
                        .caption("Street")
                        .nullable(false)
                        .maximumLength(120),
                Address.CITY.define()
                        .column()
                        .caption("City")
                        .nullable(false)
                        .maximumLength(50),
                Address.VALID.define()
                        .column()
                        .caption("Valid")
                        .withDefault(true)
                        .defaultValue(true)
                        .nullable(false))
        .formatter(EntityFormatter.builder()
                .value(Address.STREET)
                .text(", ")
                .value(Address.CITY)
                .build())
        .keyGenerator(identity())
        .smallDataset(true)
        .caption("Address")
        .build();

For more complex toString() implementations you can implement a custom Function<Entity, String>.

          .formatter(new CustomerFormatter())
private static final class CustomerFormatter implements Function<Entity, String>, Serializable {

  @Serial
  private static final long serialVersionUID = 1;

  @Override
  public String apply(Entity customer) {
    return new StringBuilder()
            .append(customer.get(Customer.LAST_NAME))
            .append(", ")
            .append(customer.get(Customer.FIRST_NAME))
            .append(customer.optional(Customer.EMAIL)
                    .map(email -> " <" + email + ">")
                    .orElse(""))
            .toString();
  }
}
          .formatter(new CustomerFormatter())
private static final class CustomerFormatter implements Function<Entity, String>, Serializable {

  @Serial
  private static final long serialVersionUID = 1;

  @Override
  public String apply(Entity customer) {
    return new StringBuilder()
            .append(customer.get(Customer.LAST_NAME))
            .append(", ")
            .append(customer.get(Customer.FIRST_NAME))
            .append(customer.optional(Customer.EMAIL)
                    .map(email -> " <" + email + ">")
                    .orElse(""))
            .toString();
  }
}

1.12. Validation

Custom validation of Entities is performed by implementing a EntityValidator.

The EntityValidator interface provides range, string length and null validation and can be extended to provide further validations.

Warning
EntityValidator logic runs frequently — avoid expensive operations like database queries. Use edit model listeners (such as beforeInsert or beforeUpdate) for validations that require cross-entity or remote checks.
final class CityValidator implements EntityValidator, Serializable {

  @Serial
  private static final long serialVersionUID = 1;

  @Override
  public void validate(Entity city, Attribute<?> attribute) {
    EntityValidator.super.validate(city, attribute);
    if (attribute.equals(City.POPULATION)) {
      // population is guaranteed to be non-null after the call to super.validate()
      Integer cityPopulation = city.get(City.POPULATION);
      if (!city.isNull(City.COUNTRY_FK)) {
        Entity country = city.get(City.COUNTRY_FK);
        Integer countryPopulation = country.get(Country.POPULATION);
        if (countryPopulation != null && cityPopulation > countryPopulation) {
          throw new ValidationException(City.POPULATION,
                  cityPopulation, "City population can not exceed country population");
        }
      }
    }
  }
}
.validator(new CityValidator())

1.14. Custom data types

When using a custom data type you must specify the columnClass of a ColumnDefinition and provide a Converter implementation.

Column<Location> LOCATION = TYPE.column("location", Location.class);
record Location(double latitude, double longitude) implements Serializable {

  @Override
  public String toString() {
    return "[" + latitude + "," + longitude + "]";
  }
}
Note
The custom type must be serializable for use in an application using the RMI connection.
City.LOCATION.define()
        .column()
        .caption("Location")
        .converter(String.class, new LocationConverter())
        .comparator(new LocationComparator()))
private static final class LocationConverter implements Converter<Location, String> {

  @Override
  public String toColumnValue(Location location,
                              Statement statement) {
    return "POINT (" + location.longitude() + " " + location.latitude() + ")";
  }

  @Override
  public Location fromColumnValue(String columnValue) {
    String[] latLon = columnValue
            .replace("POINT (", "")
            .replace(")", "")
            .split(" ");

    return new Location(parseDouble(latLon[1]), parseDouble(latLon[0]));
  }
}

When using the HTTP connection in an application using a custom data type, you must implement a EntityObjectMapperFactory, providing a EntityObjectMapper instance containing a serializer/deserializer for the custom types.

public final class WorldObjectMapperFactory extends DefaultEntityObjectMapperFactory {

  private static final String LATITUDE = "latitude";
  private static final String LONGITUDE = "longitude";

  public WorldObjectMapperFactory() {
    super(World.DOMAIN);
  }

  @Override
  public EntityObjectMapper entityObjectMapper(Entities entities) {
    EntityObjectMapper objectMapper = super.entityObjectMapper(entities);
    objectMapper.addSerializer(Location.class, new LocationSerializer());
    objectMapper.addDeserializer(Location.class, new LocationDeserializer());

    return objectMapper;
  }

  private static final class LocationSerializer extends StdSerializer<Location> {

    private LocationSerializer() {
      super(Location.class);
    }

    @Override
    public void serialize(Location location, JsonGenerator generator, SerializerProvider provider) throws IOException {
      generator.writeStartObject();
      generator.writeNumberField(LATITUDE, location.latitude());
      generator.writeNumberField(LONGITUDE, location.longitude());
      generator.writeEndObject();
    }
  }

  private static final class LocationDeserializer extends StdDeserializer<Location> {

    private LocationDeserializer() {
      super(Location.class);
    }

    @Override
    public Location deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException, JacksonException {
      JsonNode node = parser.getCodec().readTree(parser);

      return new Location(node.get(LATITUDE).asDouble(), node.get(LONGITUDE).asDouble());
    }
  }
}

This EntityObjectMapperFactory must be exposed to the ServiceLoader.

src/main/java/module-info.java

  provides is.codion.framework.json.domain.EntityObjectMapperFactory
          with is.codion.demos.world.domain.api.WorldObjectMapperFactory;

1.15. Custom select queries

When an entity’s data cannot be adequately represented by a single table, you may need to customize the SELECT query used to populate entities. The EntitySelectQuery class provides this capability through its builder API.

Important
Custom select queries should be used as an escape hatch when other approaches have been considered and rejected. In most cases, creating a database view is the preferred solution - views can be optimized by database administrators, tested independently, and changed without modifying application code. Use EntitySelectQuery when views are impractical due to deployment constraints (such as migration risks in production environments) or when the query logic needs to be version-controlled with the application code.

1.15.1. When to use custom select queries

Consider using EntitySelectQuery when:

  • Database views are impractical - Deployment environments where schema migrations carry significant risk (offline systems, mission-critical applications)

  • Application-specific logic - Query patterns that are tightly coupled to application features and likely to evolve with the code

  • Rapid iteration - During development when query logic is still being refined and database schema changes would slow progress

  • Multi-table joins - Denormalizing data from related tables without creating a view

Consider using database views instead when:

  • Stable, foundational queries - Well-established query patterns that rarely change

  • Cross-application usage - Queries needed by multiple applications or reporting tools

  • DBA optimization - Complex queries requiring database-specific hints, indexes, or performance tuning

  • Production stability - Mission-critical systems where application code changes are safer than schema migrations

1.15.2. Custom FROM clause

The most common use case is customizing the FROM clause to join additional tables, allowing you to include columns from related tables without using foreign key references.

  // Domain API
  interface Album {
    EntityType TYPE = DOMAIN.entityType("store.album");

    Column<Long> ID = TYPE.longColumn("id");
    Column<String> TITLE = TYPE.stringColumn("title");
    // Further attributes skipped for brevity
  }

  interface Track {
    EntityType TYPE = DOMAIN.entityType("store.track");

    Column<Long> ID = TYPE.longColumn("id");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<Long> ALBUM_ID = TYPE.longColumn("album_id");
    Column<String> ALBUM_TITLE = TYPE.stringColumn("album_title");
    Column<String> ARTIST_NAME = TYPE.stringColumn("artist_name");

    ForeignKey ALBUM_FK = TYPE.foreignKey("album_fk", ALBUM_ID, Album.ID);
  }

  // Domain implementation
  static class StoreDomain extends DomainModel {
    StoreDomain() {
      super(DOMAIN);
      // add(Album.TYPE.define(....
      add(Track.TYPE.define(
                      Track.ID.define()
                              .primaryKey(),
                      Track.NAME.define()
                              .column()
                              .caption("Name")
                              // set the expression since the column 'NAME' is ambiguous
                              .expression("track.name"),
                      Track.ALBUM_ID.define()
                              .column(),
                      Track.ALBUM_FK.define()
                              .foreignKey(),
                      // These columns come from the joined tables,
                      Track.ARTIST_NAME.define()
                              .column()
                              .caption("Artist")
                              // set the expression since the column 'NAME' is ambiguous
                              .expression("artist.name")
                              .readOnly(true), // always mark denormalized values as read-only
                      Track.ALBUM_TITLE.define()
                              .column()
                              .caption("Album")
                              // No need for an expression, since 'TITLE' is unambiguous
                              .readOnly(true)) // always mark denormalized values as read-only
              // Custom FROM clause to join album and artist tables
              .selectQuery(EntitySelectQuery.builder()
                      .from("store.track " +
                              "JOIN store.album ON track.album_id = album.id " +
                              "JOIN store.artist ON album.artist_id = artist.id")
                      .build())
              .build());
    }
  }
Note
Always mark denormalized values as readOnly(true).

In this example, the Track entity includes album_title and artist_name columns by joining the album and artist tables. The framework automatically generates the SELECT and WHERE clauses based on the column definitions.

Note
The columns() method is rarely needed - the framework constructs the SELECT clause from column definitions using their expression() values, which typically works correctly. Only specify columns() when you need non-standard column expressions or aliases.

1.15.3. GROUP BY and aggregation

Note
For aggregated data on a single table, use Codion’s built-in grouped entity support with .groupBy(true) and .aggregate(true) at the column level. This provides type-safe aggregate column handling and automatic HAVING clause generation. Use EntitySelectQuery with groupBy() only when aggregating across complex joins of multiple tables.

The framework includes aggregate expressions from column definitions in the SELECT clause. Entities representing aggregated data should be marked as read-only.

1.15.4. Static WHERE clause

Use the where() method to apply a static filter that applies to all queries for this entity:

  interface AvailableTrack {
    EntityType TYPE = DOMAIN.entityType("store.available_track");

    Column<Long> ID = TYPE.longColumn("id");
    Column<String> TITLE = TYPE.stringColumn("title");
  }

  static class AvailableTracksDomain extends DomainModel {
    AvailableTracksDomain() {
      super(DOMAIN);
      add(AvailableTrack.TYPE.define(
                      AvailableTrack.ID.define()
                              .primaryKey(),
                      AvailableTrack.TITLE.define()
                              .column()
                              .caption("Title"))
              // Static WHERE clause filters to available tracks only
              .selectQuery(EntitySelectQuery.builder()
                      .from("store.track")
                      .where("available = true")
                      .build())
              .readOnly(true)
              .build());
    }
  }
Important
The WHERE clause specified in EntitySelectQuery is always included and automatically combined (using AND) with any dynamic conditions from the Condition framework. This allows static filtering (like active = 1) to coexist with dynamic user-driven filters.

1.15.5. Best practices

When using custom select queries:

  • Mark entities as read-only - Entities based on joins or aggregations typically cannot be updated: .readOnly(true)

  • Avoid specifying columns - Let the framework generate the SELECT columns clause from column definitions unless you need special expressions

  • Watch out for ambiguous column names - Use .expression() when column names become ambiguous due to joins

  • Use table aliases carefully - Ensure column expressions in attribute definitions match the aliases used in your FROM clause

  • Test thoroughly - Custom queries bypass some framework validations, so verify behavior with actual data

  • Document the rationale - Include comments explaining why a custom query is used instead of a view

This pattern is useful when you frequently need data from related tables and want to avoid the overhead of foreign key lookups, while maintaining the option to use a database view in the future if the query becomes performance-critical.

1.16. Domain composition

Domain models can be composed of other domain models, allowing you to build modular, reusable domain definitions. This composition can be complete (including all entity definitions) or selective (cherry-picking specific entities or functionality).

1.16.1. Complete composition

The simplest form of composition is to include an entire domain model within another. This is done by passing a domain instance to the add() method in the constructor.

  // Base domain with product catalog
  static class Products extends DomainModel {

    static final DomainType DOMAIN = domainType("products");

    interface Product {
      EntityType TYPE = DOMAIN.entityType("products.product");

      Column<Integer> ID = TYPE.integerColumn("id");
      Column<String> NAME = TYPE.stringColumn("name");
    }

    public Products() {
      super(DOMAIN);
      add(product());
    }

    EntityDefinition product() {
      return Product.TYPE.define(
                      Product.ID.define()
                              .primaryKey(),
                      Product.NAME.define()
                              .column())
              .build();
    }
  }

  // Orders domain composes Products and adds customer/order entities
  static class Orders extends DomainModel {

    static final DomainType DOMAIN = domainType("orders");

    interface Customer {
      EntityType TYPE = DOMAIN.entityType("orders.customer");

      Column<Integer> ID = TYPE.integerColumn("id");
      Column<String> NAME = TYPE.stringColumn("first_name");
    }

    interface Order {
      EntityType TYPE = DOMAIN.entityType("orders.order");

      Column<Integer> ID = TYPE.integerColumn("id");
      Column<Integer> CUSTOMER_ID = TYPE.integerColumn("customer_id");
      Column<Integer> PRODUCT_ID = TYPE.integerColumn("product_id");

      ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
      // Foreign key referencing composed Products domain
      ForeignKey PRODUCT_FK = TYPE.foreignKey("product_fk", PRODUCT_ID, Product.ID);
    }

    public Orders() {
      super(DOMAIN);
      // Include entire Products domain
      add(new Products());
      add(customer(), order());
    }

    EntityDefinition customer() {
      return Customer.TYPE.define(
                      Customer.ID.define()
                              .primaryKey(),
                      Customer.NAME.define()
                              .column())
              .build();
    }

    EntityDefinition order() {
      return Order.TYPE.define(
                      Order.ID.define()
                              .primaryKey(),
                      Order.CUSTOMER_ID.define()
                              .column(),
                      Order.CUSTOMER_FK.define()
                              .foreignKey(),
                      Order.PRODUCT_ID.define()
                              .column(),
                      Order.PRODUCT_FK.define()
                              .foreignKey())
              .build();
    }
  }

  // Store domain composes Orders (which transitively includes Products)
  static class Store extends DomainModel {

    static final DomainType DOMAIN = domainType("store");

    interface Employee {
      EntityType TYPE = DOMAIN.entityType("store.employee");

      Column<Integer> ID = TYPE.integerColumn("id");
      Column<String> NAME = TYPE.stringColumn("first_name");
    }

    public Store() {
      super(DOMAIN);
      // Includes Orders domain (and transitively Products)
      add(new Orders());
      add(employee());
    }

    EntityDefinition employee() {
      return Employee.TYPE.define(
                      Employee.ID.define()
                              .primaryKey(),
                      Employee.NAME.define()
                              .column())
              .build();
    }
  }

In this example:

  • The Products domain defines a single Product entity

  • The Orders domain includes the entire Products domain via add(new Products()) and adds its own Customer and Order entities

  • The Store domain includes the entire Orders domain (which transitively includes Products) and adds an Employee entity

  • The resulting Store domain contains all entities from all three domains: Product, Customer, Order, and Employee

  • Foreign key references can cross domain boundaries - Order.PRODUCT_FK references Product.ID from the composed Products domain

This hierarchical composition allows you to:

  • Build complex domains from smaller, focused components

  • Reuse domain definitions across different applications

  • Maintain clear separation of concerns between different parts of your model

  • Establish cross-domain relationships through foreign keys

1.16.2. Selective composition

For more fine-grained control, you can selectively include specific entities or functionality from other domains:

  // Website domain selectively includes entities and functionality from other domains
  static class StoreWebSite extends DomainModel {

    static final DomainType DOMAIN = domainType("website");

    public StoreWebSite() {
      super(DOMAIN);
      Entities orderEntities = new Orders().entities();
      // Selectively add specific entities from Orders domain
      add(orderEntities.definition(Product.TYPE));
      add(orderEntities.definition(Customer.TYPE));
      // Include only reports from Products domain
      addReports(new Products());
      // Include only functions from Store domain
      addFunctions(new Store());
      // Include only procedures from Orders domain
      addProcedures(new Orders());
    }
  }

Selective composition provides several specialized methods:

  • add(EntityDefinition) - Adds a specific entity definition from another domain

  • addReports(Domain) - Includes only the reports from another domain

  • addFunctions(Domain) - Includes only the functions from another domain

  • addProcedures(Domain) - Includes only the procedures from another domain

This approach is useful when:

  • Building a subset of functionality for specific clients (e.g., a public website vs. internal admin application)

  • Sharing database functions/procedures across different domain models

  • Creating lightweight domain models that reference only the entities they actually use

  • Avoiding circular dependencies between domain modules

Note
When selectively adding entity definitions, ensure that any foreign key references are satisfied - if an entity references another via foreign key, both entities must be included in the domain.

1.17. Entities in action

Using the Entity class is rather straight forward.

EntityConnectionProvider connectionProvider = EntityConnectionProvider.builder()
        .domain(Petstore.DOMAIN)
        .user(User.parse("scott:tiger"))
        .build();

Entities entities = connectionProvider.entities();

EntityConnection connection = connectionProvider.connection();

//populate a new category
Entity insects = entities.entity(Category.TYPE)
        .with(Category.NAME, "Insects")
        .with(Category.DESCRIPTION, "Creepy crawlies")
        .build();

insects = connection.insertSelect(insects);

//populate a new product for the insect category
Entity smallBeetles = entities.entity(Product.TYPE)
        .with(Product.CATEGORY_FK, insects)
        .with(Product.NAME, "Small Beetles")
        .with(Product.DESCRIPTION, "Beetles on the smaller side")
        .build();

connection.insert(smallBeetles);

//see what products are available for the Cats category
Entity categoryCats = connection.selectSingle(Category.NAME.equalTo("Cats"));

List<Entity> cats = connection.select(Product.CATEGORY_FK.equalTo(categoryCats));

cats.forEach(System.out::println);

1.18. Unit Testing

1.18.1. Introduction

To unit test the CRUD operations on the domain model extend DomainTest.

The unit tests are run within a single transaction which is rolled back after the test finishes, so these tests are pretty much guaranteed to leave no junk data behind.

1.18.2. DomainTest

The DomainTest uses a default EntityFactory implementation which provides test entities with randomly created values, based on the value constraints set in the domain model. Extend this class and pass to the super constructor, overriding the required methods.

  • foreignKey should return an entity instance for the given foreign key to use for a foreign key reference required for inserting the entity being tested.

  • entity should return an entity to use as basis for the unit test, that is, the entity that should be inserted, selected, updated and finally deleted.

  • modify should simply leave the entity in a modified state so that it can be used for update test, since the database layer throws an exception if an unmodified entity is updated. If modify returns an unmodified entity, the update test is skipped.

To run the full CRUD test for a domain entity you need to call the test(EntityType entityType) method with the entity type as parameter. You can either create a single testDomain() method and call the test method in turn for each entityType or create a entityName method for each domain entity, as we do in the example below.

public class StoreTest extends DomainTest {

  private static final Store DOMAIN = new Store();

  public StoreTest() {
    super(DOMAIN, StoreEntityFactory::new);
  }

  @Test
  public void customer() {
    test(Customer.TYPE);
  }

  @Test
  public void address() {
    test(Address.TYPE);
  }

  @Test
  public void customerAddress() {
    test(CustomerAddress.TYPE);
  }

  private static final class StoreEntityFactory extends DefaultEntityFactory {

    private StoreEntityFactory(EntityConnection connection) {
      super(connection);
    }

    @Override
    public Optional<Entity> entity(ForeignKey foreignKey) {
      // See if the currently running test requires an ADDRESS entity
      if (foreignKey.referencedType().equals(Address.TYPE)) {
        return Optional.of(connection().insertSelect(entities().entity(Address.TYPE)
                .with(Address.ID, 21L)
                .with(Address.STREET, "One Way")
                .with(Address.CITY, "Sin City")
                .build()));
      }

      return super.entity(foreignKey);
    }

    @Override
    public Entity entity(EntityType entityType) {
      if (entityType.equals(Address.TYPE)) {
        // Initialize an entity representing a record in the
        // STORE.ADDRESS table, to use for testing
        return entities().entity(Address.TYPE)
                .with(Address.ID, 42L)
                .with(Address.STREET, "Street")
                .with(Address.CITY, "City")
                .with(Address.VALID, true)
                .build();
      }
      else if (entityType.equals(Customer.TYPE)) {
        // Initialize an entity representing a record in the
        // STORE.CUSTOMER table, to use for testing
        return entities().entity(Customer.TYPE)
                .with(Customer.ID, UUID.randomUUID().toString())
                .with(Customer.FIRST_NAME, "Robert")
                .with(Customer.LAST_NAME, "Ford")
                .with(Customer.ACTIVE, true)
                .build();
      }
      else if (entityType.equals(CustomerAddress.TYPE)) {
        return entities().entity(CustomerAddress.TYPE)
                .with(CustomerAddress.CUSTOMER_FK, entity(CustomerAddress.CUSTOMER_FK).orElseThrow())
                .with(CustomerAddress.ADDRESS_FK, entity(CustomerAddress.ADDRESS_FK).orElseThrow())
                .build();
      }

      return super.entity(entityType);
    }

    @Override
    public void modify(Entity entity) {
      if (entity.type().equals(Address.TYPE)) {
        entity.set(Address.STREET, "New Street");
        entity.set(Address.CITY, "New City");
      }
      else if (entity.type().equals(Customer.TYPE)) {
        // It is sufficient to change the value of a
        // single attribute, but the more, the merrier
        entity.set(Customer.FIRST_NAME, "Jesse");
        entity.set(Customer.LAST_NAME, "James");
        entity.set(Customer.ACTIVE, false);
      }
    }
  }
}

2. Conditions

Conditions in Codion are composable, strongly-typed query filters used to construct WHERE or HAVING clauses for select, update, and count operations. They are typically created via domain attributes (like Column or ForeignKey), and can be freely combined using logical operators like AND and OR.

The Chinook domain model is used in the examples below.

2.1. Condition

Condition

Represents a query condition and contains factory methods for creating Condition instances.

ColumnCondition

Represents a column based Condition.

Note
Column and ForeignKey implement their respective condition factory interfaces (ColumnConditionFactory and ForeignKeyConditionFactory), so you can create Condition instances directly from them using fluent methods like .equalTo(), .isNull(), .in(), etc.
Condition allArtistsCondition =
        Condition.all(Artist.TYPE);

List<Entity> artists =
        connection.select(allArtistsCondition);
Condition liveAlbums =
        Album.TITLE.likeIgnoreCase("%Live%");

List<Entity> albums =
        connection.select(liveAlbums);
Entity metallica =
        connection.selectSingle(
                Artist.NAME.equalTo("Metallica"));

Condition albums =
        Album.ARTIST_FK.equalTo(metallica);
CustomCondition

A CustomCondition can be used when your logic can’t be expressed through column-based or foreign-key-based conditions — for example, when writing native SQL fragments or using DB-specific syntax.

List<Long> classicalPlaylistIds =
        List.of(42L, 43L);

Condition noneClassical =
        Track.NOT_IN_PLAYLIST.get(
                Playlist.ID, classicalPlaylistIds);

List<Entity> tracks =
        connection.select(noneClassical);
Condition.Combination

Allows you to combine multiple conditions using logical AND / OR operators. Conditions can be nested to build expressive and complex query logic.

Condition liveMetallicaAlbums =
        Condition.and(liveAlbums, metallicaAlbums);

List<Entity> albums =
        connection.select(liveMetallicaAlbums);

2.2. Select, Update, Count

The EntityConnection.Select, EntityConnection.Update, and EntityConnection.Count classes each provide a .where(Condition) factory method returning a builder object for further configuration.

2.2.1. Select

EntityConnection.Select

Represents a WHERE condition as well as extended configuration specifically for selecting, such as orderBy, limit, offset and referenceDepth.

Select selectLiveMetallicaAlbums =
        Select.where(liveMetallicaAlbums)
                .orderBy(OrderBy.descending(Album.NUMBER_OF_TRACKS))
                .build();

List<Entity> albums =
        connection.select(selectLiveMetallicaAlbums);

2.2.2. Update

EntityConnection.Update

Represents a WHERE condition as well as the columns and values for updating one or more entities.

Update removeLiveMetallicaAlbumCovers =
        Update.where(liveMetallicaAlbums)
                .set(Album.COVER, null)
                .build();

int updateCount =
        connection.update(removeLiveMetallicaAlbumCovers);

2.2.3. Count

EntityConnection.Count

Represents a WHERE condition specifically for counting records.

Count countAlbumsWithCover =
        Count.where(Album.COVER.isNotNull());

int count = connection.count(countAlbumsWithCover);

3. EntityConnection

Codion’s EntityConnection is the primary interface for executing database operations — including querying, modifying, transaction control, calling procedures and functions and filling reports. It exposes a small, explicit API for working with Entity instances and makes no assumptions about your database engine or schema design.

Codion’s database layer is intentionally minimal. It does not perform SQL joins, nor does it rely on DB-specific features — except where needed for primary key generation via KeyGenerator strategies.

Instead, it gives you predictable, queryable access to individual Entity objects and their associated foreign keys — controlled through a feature called reference depth.

The Chinook domain model is used in the examples below.

3.1. Selecting

By default, when you select a row using EntityConnection you receive an Entity instance along with a single level of foreign key references, that is a so-called reference depth of one. This means that selecting a track you get all the entities referenced via foreign keys as well.

The reference depth can be configured on a foreign key basis when defining entities. A reference depth of zero means that no foreign key references are fetched, and a value larger than one means that not only is the foreign key reference fetched but also its foreign key references, until the defined depth has been reached. A negative reference depth means no limit with the whole dependency graph fetched. This limiting of foreign key reference depth can be turned off, meaning the full reference graph is always fetched, via a system property:

codion.db.limitForeignKeyReferenceDepth=false
LocalEntityConnection.LIMIT_FOREIGN_KEY_REFERENCE_DEPTH.set(false);
connection.limitForeignKeyReferenceDepth(false);

You can specify that the foreign key should not be populated by default by using a reference depth of 0.

InvoiceLine.INVOICE_FK.define()
        .foreignKey()
        .referenceDepth(0)
        .hidden(true),

Or you can specify that the foreign key should be populated along with one more level by using a reference depth of 2.

Track.ALBUM_FK.define()
        .foreignKey()
        .referenceDepth(2)
        .include(Album.ARTIST_FK, Album.TITLE),
EntityConnection connection = connectionProvider.connection();

List<Entity> tracks = connection.select(Track.NAME.like("Bad%"));

Entity track = tracks.get(0);

Entity genre = track.get(Track.GENRE_FK);
Entity mediaType = track.get(Track.MEDIATYPE_FK);
Entity album = track.get(Track.ALBUM_FK);

// reference depth for Track.ALBUM_FK is 2, which means two levels of
// references are fetched, so we have the artist here as well
Entity artist = album.get(Album.ARTIST_FK);

The reference depth can also be configured on a query basis, either for the whole query or one or more foreign keys.

EntityConnection connection = connectionProvider.connection();

List<Entity> tracks = connection.select(
        Select.where(Track.NAME.like("Bad%"))
                .referenceDepth(0)
                .build());

Entity track = tracks.get(0);

// reference depth is 0, so this 'genre' instance is null
Entity genre = track.get(Track.GENRE_FK);

// using track.entity(Track.GENRE_FK) you get a 'genre'
// instance containing only the primary key, since the condition
// reference depth limit prevented it from being selected
genre = track.entity(Track.GENRE_FK);
EntityConnection connection = connectionProvider.connection();

List<Entity> tracks = connection.select(
        Select.where(Track.NAME.like("Bad%"))
                .referenceDepth(Track.ALBUM_FK, 0)
                .build());

Entity track = tracks.get(0);

Entity genre = track.get(Track.GENRE_FK);
Entity mediaType = track.get(Track.MEDIATYPE_FK);

// this 'album' instance is null, since the condition
// reference depth limit prevented it from being selected
Entity album = track.get(Track.ALBUM_FK);

// using track.entity(Track.ALBUM_FK) you get an 'album'
// instance containing only the primary key, since the condition
// reference depth limit prevented it from being selected
album = track.entity(Track.ALBUM_FK);

3.1.1. Reference Depth Values:

  • 0 – Do not fetch foreign key references

  • 1 (default) – Fetch directly referenced foreign key entities

  • N – Fetch up to N levels deep

  • -1 – Fetch entire reference graph (no limit)

3.1.2. The N+1 problem

Selecting tracks performs four queries (track + album, mediatype and genre), but that number of queries is the same whether you select one or a thousand tracks.

3.1.3. Selecting entities

EntityConnection connection = connectionProvider.connection();

List<Entity> artists = connection.select(
        Artist.NAME.like("The %"));

List<Entity> nonLiveAlbums = connection.select(and(
        Album.ARTIST_FK.in(artists),
        Album.TITLE.likeIgnoreCase("%live%")));

Entity aliceInChains = connection.selectSingle(
        Artist.NAME.equalTo("Alice In Chains"));

List<Entity> aliceInChainsAlbums = connection.select(
        Album.ARTIST_FK.equalTo(aliceInChains));

Entity metal = connection.selectSingle(
        Genre.NAME.equalToIgnoreCase("metal"));

List<Entity> metalTracks = connection.select(
        Select.where(Track.GENRE_FK.equalTo(metal))
                .attributes(Track.NAME, Track.ALBUM_FK)
                .orderBy(descending(Track.NAME))
                .build());

Long classicalPlaylistId = connection.select(
        Playlist.ID, Playlist.NAME.equalTo("Classical")).get(0);

List<Entity> nonClassicalTracks = connection.select(
        Track.NOT_IN_PLAYLIST.get(Playlist.ID, classicalPlaylistId));
EntityConnection connection = connectionProvider.connection();

Entities entities = connection.entities();

Entity.Key key = entities.primaryKey(Artist.TYPE, 42L);

Entity artist = connection.select(key);
EntityConnection connection = connectionProvider.connection();

Entities entities = connection.entities();

Entity.Key key42 = entities.primaryKey(Artist.TYPE, 42L);
Entity.Key key43 = entities.primaryKey(Artist.TYPE, 43L);

Collection<Entity> artists = connection.select(List.of(key42, key43));

3.1.4. Selecting values

For selecting the values of a single column.

EntityConnection connection = connectionProvider.connection();

List<String> customerUsStates =
        connection.select(Customer.STATE,
                Customer.COUNTRY.equalTo("USA"));

3.1.5. iterator

For iterating over a result set instead of loading it entirely into memory. This is useful when processing large result sets or when memory is constrained.

Important
When using remote connections, each call to hasNext() and next() involves a network round-trip. For large result sets, consider using select() instead to load entities in a single batch. Iterators over remote connections that remain idle for longer than the configured timeout (codion.db.remote.iteratorTimeout, default 5 minutes) are automatically closed server-side.
Note
iterator() is not supported on HTTP based connections, and throws UnsupportedOperationException.
EntityConnection connection = connectionProvider.connection();

try (EntityResultIterator iterator =
             connection.iterator(Customer.EMAIL.isNotNull())) {
  while (iterator.hasNext()) {
    System.out.println(iterator.next().get(Customer.EMAIL));
  }
}

3.1.6. dependencies

For selecting entities that depend on a set of entities via foreign keys.

EntityConnection connection = connectionProvider.connection();

List<Entity> employees = connection.select(all(Employee.TYPE));

Map<EntityType, Collection<Entity>> dependencies = connection.dependencies(employees);

Collection<Entity> customersDependingOnEmployees = dependencies.get(Customer.TYPE);

3.1.7. count

For selecting the row count given a count condition.

EntityConnection connection = connectionProvider.connection();

int numberOfItStaff = connection.count(
        Count.where(Employee.TITLE.equalTo("IT Staff")));

3.2. Modifying

3.2.1. insert

For inserting rows.

EntityConnection connection = connectionProvider.connection();

Entities entities = connection.entities();

Entity myBand = entities.entity(Artist.TYPE)
        .with(Artist.NAME, "My Band")
        .build();

myBand = connection.insertSelect(myBand);

Entity firstAlbum = entities.entity(Album.TYPE)
        .with(Album.ARTIST_FK, myBand)
        .with(Album.TITLE, "First album")
        .build();
Entity secondAlbum = entities.entity(Album.TYPE)
        .with(Album.ARTIST_FK, myBand)
        .with(Album.TITLE, "Second album")
        .build();

Collection<Entity.Key> albumKeys =
        connection.insert(List.of(firstAlbum, secondAlbum));

3.2.2. update

For updating one or more entity instances.

Important
These methods throw an exception if any of the entities is unmodified.
EntityConnection connection = connectionProvider.connection();

Entity myBand = connection.selectSingle(
        Artist.NAME.equalTo("My Band"));

myBand.set(Artist.NAME, "Proper Name");

myBand = connection.updateSelect(myBand);

List<Entity> customersWithoutPhoneNo =
        connection.select(Customer.PHONE.isNull());

customersWithoutPhoneNo.forEach(customer ->
        customer.set(Customer.PHONE, "<none>"));

connection.update(customersWithoutPhoneNo);
Optimistic locking

The framework performs optimistic locking during updates using the methods above. This is done by selecting the entities being updated FOR UPDATE (when supported by the underlying database) and comparing all original values to the current row values, throwing an exception if one or more values differ or the row is missing. Optimistic locking is field-based: any difference between original and current values causes an update to fail.

entity.set(Album.TITLE, "New Title");
connection.update(entity); // fails if the row has been changed by someone else
Note
Excluding attributes when selecting entities results in those attributes (lazy loaded ones for example) not being included when optimistic locking is performed on subsequent updates, since optimistic locking relies on the original attribute value being available for making a comparison.

Optimistic locking can be turned off system-wide using a system property:

codion.db.optimisticLocking=false

or by using the LocalEntityConnection.OPTIMISTIC_LOCKING configuration value:

LocalEntityConnection.OPTIMISTIC_LOCKING.set(false);

or on a connection instance via optimisticLocking():

connection.optimisticLocking(false);

For updating by condition.

EntityConnection connection = connectionProvider.connection();

connection.update(
        Update.where(Artist.NAME.equalTo("Azymuth"))
                .set(Artist.NAME, "Azymouth")
                .build());

int updateCount = connection.update(
        Update.where(Customer.EMAIL.isNull())
                .set(Customer.EMAIL, "<none>")
                .build());

3.2.3. delete

For deleting existing rows.

EntityConnection connection = connectionProvider.connection();

Entity aquaman = connection.selectSingle(
        Artist.NAME.equalTo("Aquaman"));

List<Long> aquamanAlbumIds = connection.select(Album.ID,
        Album.ARTIST_FK.equalTo(aquaman));

List<Long> aquamanTrackIds = connection.select(Track.ID,
        Track.ALBUM_ID.in(aquamanAlbumIds));

int playlistTracksDeleted = connection.delete(
        PlaylistTrack.TRACK_ID.in(aquamanTrackIds));

int tracksDeleted = connection.delete(
        Track.ALBUM_ID.in(aquamanAlbumIds));

int albumsDeleted = connection.delete(
        Album.ARTIST_FK.equalTo(aquaman));
EntityConnection connection = connectionProvider.connection();

Entity audioslave = connection.selectSingle(
        Artist.NAME.equalTo("Audioslave"));

List<Entity> albums = connection.select(
        Album.ARTIST_FK.equalTo(audioslave));
List<Entity> tracks = connection.select(
        Track.ALBUM_FK.in(albums));
List<Entity> playlistTracks = connection.select(
        PlaylistTrack.TRACK_FK.in(tracks));
List<Entity> invoiceLines = connection.select(
        InvoiceLine.TRACK_FK.in(tracks));

List<Entity> toDelete = new ArrayList<>();
toDelete.addAll(invoiceLines);
toDelete.addAll(playlistTracks);
toDelete.addAll(tracks);
toDelete.addAll(albums);
toDelete.add(audioslave);

connection.delete(Entity.primaryKeys(toDelete));

3.3. Procedures & Functions

  • Functions return a single value.

  • Procedures perform logic with no return value.

  • Both are executed through the same API: EntityConnection.execute(…​).

3.3.1. Function

EntityConnection connection = connectionProvider.connection();

List<Long> trackIds = List.of(123L, 1234L);
BigDecimal priceIncrease = BigDecimal.valueOf(0.1);

Collection<Entity> modifiedTracks =
        connection.execute(Track.RAISE_PRICE,
                new RaisePriceParameters(trackIds, priceIncrease));

String playlistName = "Random playlist";
int numberOfTracks = 100;
Collection<Entity> playlistGenres = connection.select(
        Genre.NAME.in("Classical", "Soundtrack"));

Entity playlist = connection.execute(Playlist.RANDOM_PLAYLIST,
        new RandomPlaylistParameters(playlistName, numberOfTracks, playlistGenres));

3.3.2. Procedure

EntityConnection connection = connectionProvider.connection();

connection.execute(Invoice.UPDATE_TOTALS, List.of(1234L, 3412L));

3.4. Reporting

3.4.1. report

EntityConnection connection = connectionProvider.connection();

Map<String, Object> reportParameters = new HashMap<>();
reportParameters.put("CUSTOMER_IDS", List.of(42, 43, 45));

JasperPrint jasperPrint = connection.report(Customer.REPORT, reportParameters);

3.5. Transaction control

3.5.1. Transactional

Codion encourages declarative transaction boundaries using lambdas or anonymous classes. This ensures transaction safety (commit/rollback) with minimal boilerplate.

Most use cases are covered by:

These methods perform a commit on success and rollback on failure.

Note
Nested transactions are not supported and will cause an IllegalStateException to be thrown, causing the outer transaction to be rolled back.
Transaction without a result
EntityConnection connection = connectionProvider.connection();

EntityConnection.transaction(connection, () -> {
  Entities entities = connection.entities();

  Entity artist = entities.entity(Artist.TYPE)
          .with(Artist.NAME, "The Band")
          .build();
  artist = connection.insertSelect(artist);

  Entity album = entities.entity(Album.TYPE)
          .with(Album.ARTIST_FK, artist)
          .with(Album.TITLE, "The Album")
          .build();

  connection.insert(album);
});
Same example using an anonymous class
EntityConnection connection = connectionProvider.connection();

Transactional transactional = new Transactional() {

  @Override
  public void execute() {
    Entities entities = connection.entities();

    Entity artist = entities.entity(Artist.TYPE)
            .with(Artist.NAME, "The Band")
            .build();
    artist = connection.insertSelect(artist);

    Entity album = entities.entity(Album.TYPE)
            .with(Album.ARTIST_FK, artist)
            .with(Album.TITLE, "The Album")
            .build();

    connection.insert(album);
  }
};

EntityConnection.transaction(connection, transactional);
Transaction with a result
EntityConnection connection = connectionProvider.connection();

Entity.Key albumKey = EntityConnection.transaction(connection, () -> {
  Entities entities = connection.entities();

  Entity artist = entities.entity(Artist.TYPE)
          .with(Artist.NAME, "The Band")
          .build();
  artist = connection.insertSelect(artist);

  Entity album = entities.entity(Album.TYPE)
          .with(Album.ARTIST_FK, artist)
          .with(Album.TITLE, "The Album")
          .build();

  return connection.insert(album);
});
Same example using an anonymous class
EntityConnection connection = connectionProvider.connection();

TransactionalResult<Entity.Key> transactional = new TransactionalResult<Entity.Key>() {

  @Override
  public Entity.Key execute() {
    Entities entities = connection.entities();

    Entity artist = entities.entity(Artist.TYPE)
            .with(Artist.NAME, "The Band")
            .build();
    artist = connection.insertSelect(artist);

    Entity album = entities.entity(Album.TYPE)
            .with(Album.ARTIST_FK, artist)
            .with(Album.TITLE, "The Album")
            .build();

    return connection.insert(album);
  }
};

Entity.Key albumKey = EntityConnection.transaction(connection, transactional);

3.5.2. Transaction

For a more fine-grained transaction control and the ability to rollback, transactions can be started and ended manually, note that this is more complex and thereby error-prone and should not be used unless the method described above does not work for your use-case.

// This example demonstrates full manual transaction control, including rollback safety
// and protection against leaving transactions open in the presence of unexpected failures.
EntityConnection connection = connectionProvider.connection();

Entities entities = connection.entities();

// It is very important to start the transaction here, outside the try/catch block,
// otherwise, trying to start a transaction on a connection already with an open transaction
// (which is a bug in itself), would cause the current transaction to be rolled back
// in the Exception catch block, which is probably not what you want.
connection.startTransaction();
try {
  Entity artist = entities.entity(Artist.TYPE)
          .with(Artist.NAME, "The Band")
          .build();
  connection.insert(artist);

  Entity album = entities.entity(Album.TYPE)
          .with(Album.ARTIST_FK, artist)
          .with(Album.TITLE, "The Album")
          .build();
  connection.insert(album);

  connection.commitTransaction();
}
catch (DatabaseException e) {
  connection.rollbackTransaction();
  throw e;
}
catch (RuntimeException e) {
  // It is a good practice, but not necessary, to catch RuntimeException,
  // in order to not wrap a RuntimeException in another RuntimeException.
  connection.rollbackTransaction();
  throw e;
}
catch (Exception e) {
  // Always include a catch for the top level Exception, otherwise unexpected
  // exceptions may cause a transaction to remain open, which is a very serious bug.
  connection.rollbackTransaction();
  throw new RuntimeException(e);
}
catch (Throwable e) {
  // It's rare, but including a catch for Throwable ensures rollback safety
  // even in the face of serious errors (e.g., OutOfMemoryError, LinkageError).
  connection.rollbackTransaction();
  throw e;
}

3.6. LocalEntityConnection

An EntityConnection implementation based on a direct connection to the database, provides access to the underlying JDBC connection.

3.7. RemoteEntityConnection

An EntityConnection implementation based on an RMI connection. Requires a server.

3.8. HttpEntityConnection

An EntityConnection implementation based on HTTP. Requires a server.

4. EntityConnectionProvider

An EntityConnectionProvider is a factory and lifecycle manager for EntityConnection instances — ensuring reliable access to the database regardless of protocol (JDBC, RMI, HTTP).

In most cases EntityConnections are retrieved from a EntityConnectionProvider, which is responsible for establishing a connection to the underlying database. The EntityConnectionProvider class is central to the framework and is a common constructor parameter in classes requiring database access.

Each call to connection() returns the current active connection. If the existing connection is invalid (e.g., due to network failure or server restart), a new one is transparently established. If the EntityConnectionProvider is unable to connect to the underlying database or server, connection() throws an exception.

Important
Do NOT cache the EntityConnection instance. The instance returned by connection() should only be kept for a short time, such as a local variable or method parameter since it can become invalid and thereby unusable. Always use connection() to make sure you have a healthy EntityConnection.

4.1. LocalEntityConnectionProvider

Provides a connection based on a local JDBC connection.

Database.URL.set("jdbc:h2:mem:h2db");
Database.INIT_SCRIPTS.set("src/main/sql/create_schema.sql");

Database database = Database.instance();

LocalEntityConnectionProvider connectionProvider =
        LocalEntityConnectionProvider.builder()
                .database(database)
                .domain(new ChinookImpl())
                .user(User.parse("scott:tiger"))
                .build();

LocalEntityConnection entityConnection =
        connectionProvider.connection();

DatabaseConnection databaseConnection =
        entityConnection.databaseConnection();

// the underlying JDBC connection is available in a local connection
Connection connection = databaseConnection.getConnection();

connectionProvider.close();

4.2. RemoteEntityConnectionProvider

Provides a connection based on a remote RMI connection.

RemoteEntityConnectionProvider connectionProvider =
        RemoteEntityConnectionProvider.builder()
                .domain(Chinook.DOMAIN)
                .user(User.parse("scott:tiger"))
                .hostName("localhost")
                .registryPort(1099)
                .build();

EntityConnection entityConnection =
        connectionProvider.connection();

Entities entities = entityConnection.entities();

Entity track = entityConnection.select(entities.primaryKey(Track.TYPE, 42L));

connectionProvider.close();

4.3. HttpEntityConnectionProvider

Provides a connection based on a remote HTTP connection.

HttpEntityConnectionProvider connectionProvider =
        HttpEntityConnectionProvider.builder()
                .domain(Chinook.DOMAIN)
                .user(User.parse("scott:tiger"))
                .hostName("localhost")
                .port(8080)
                .https(false)
                .build();

EntityConnection entityConnection = connectionProvider.connection();

Entities entities = entityConnection.entities();

entityConnection.select(entities.primaryKey(Track.TYPE, 42L));

connectionProvider.close();