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 is comprised of AttributeDefinitions based on the Attributes associated with 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 does not render the Entity instance modified by default, but can be configured to 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(Country.CAPITAL_FK, City.POPULATION)
        .caption("Capital pop.")
        .numberFormatGrouping(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 DerivedAttribute.Provider implementation as shown below.

CountryLanguage.NO_OF_SPEAKERS.define()
        .derived(CountryLanguage.COUNTRY_FK, CountryLanguage.PERCENTAGE)
        .provider(new NoOfSpeakersProvider())
        .caption("No. of speakers")
        .numberFormatGrouping(true),
final class NoOfSpeakersProvider implements DerivedAttribute.Provider<Integer> {

  @Serial
  private static final long serialVersionUID = 1;

  @Override
  public Integer get(SourceValues values) {
    Double percentage = values.get(CountryLanguage.PERCENTAGE);
    Entity country = values.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)
        .numberFormatGrouping(true)
        .maximumFractionDigits(2),
Country.INDEPYEAR.define()
        .column()
        .caption("Indep. year")
        .valueRange(-2000, 2500),
Country.INDEPYEAR_SEARCHABLE.define()
        .column()
        .expression("to_char(indepyear)")
        .searchable(true)
        .readOnly(true),
Country.POPULATION.define()
        .column()
        .caption("Population")
        .nullable(false)
        .numberFormatGrouping(true),
Country.LIFE_EXPECTANCY.define()
        .column()
        .caption("Life expectancy")
        .maximumFractionDigits(1)
        .valueRange(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.columnValuesEqual() method to check if all column based 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")
        .hasDatabaseDefault(true)
        .nullable(false),

For databases lacking native boolean support we can define a boolean column, specifying the underlying type and the true/false values.

Customer.ACTIVE.define()
        .booleanColumn("Is active", Integer.class, 1, 0)
Customer.ACTIVE.define()
        .booleanColumn("Is active", String.class, "true", "false")
Customer.ACTIVE.define()
        .booleanColumn("Is active", Character.class, 'T', 'F')

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.9. Foreign key

Country.CAPITAL_FK.define()
        .foreignKey()
        .caption("Capital"),

1.10. 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)
                          .numberFormatGrouping(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))
          .stringFactory(City.NAME)
          .description("Cities of the World")
          .caption("City")
          .build();
}

1.11. 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.11.1. Identity

Based on identity columns, supported by most DBMSs.

.keyGenerator(identity())

1.11.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.11.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.11.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.11.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.12. StringFactory

The StringFactory 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.

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")
                        .hasDatabaseDefault(true)
                        .defaultValue(true)
                        .nullable(false))
        .stringFactory(StringFactory.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>.

.stringFactory(new CustomerToString())
private static final class CustomerToString 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.13. Validation

Custom validation of Entities is performed by implementing a EntityValidator.

The DefaultEntityValidator implementation provides basic range 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 extends DefaultEntityValidator implements Serializable {

  @Serial
  private static final long serialVersionUID = 1;

  @Override
  public <T> void validate(Entity city, Attribute<T> attribute) {
    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.15. 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.16. Entities in action

Using the Entity class is rather straight forward.

EntityConnectionProvider connectionProvider = EntityConnectionProvider.builder()
        .domain(Petstore.DOMAIN)
        .clientType("Manual")
        .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.17. Unit Testing

1.17.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.17.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 property, 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.

ForeignKeyCondition

Represents a foreign key based Condition.

Note
Column and ForeignKey implement their respective condition factory interfaces (ColumnCondition.Factory and ForeignKeyCondition.Factory), 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)
        .attributes(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

LocalEntityConnection provides a way to iterate over a result set, instead of loading it into memory.

LocalEntityConnection connection = connectionProvider.connection();

try (ResultIterator<Entity> iterator =
             connection.iterator(Customer.EMAIL.isNotNull())) {
  while (iterator.hasNext()) {
    System.out.println(iterator.next().get(Customer.EMAIL));
  }
}
catch (SQLException e) {
  throw new DatabaseException(e);
}

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

Collection<Entity> updatedInvoices =
        connection.execute(Invoice.UPDATE_TOTALS, List.of(1234L, 3412L));

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.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"))
                .clientType(EntityConnectionProviderDemo.class.getSimpleName())
                .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)
                .clientType(EntityConnectionProviderDemo.class.getSimpleName())
                .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();