Note
Most of the code used throughout this manual is available in the manual demo module included in the project.

1. Framework

1.1. Domain & Database

1.1.1. Domain Model

The domain model is based around five classes; Entity, which represents a row in a table, EntityType, which specifies an entity type, EntityDefinition which contains the meta-data for each entity type (properties and such), Attribute, which represents column types and Property, which represents columns. Domain models must extend the DefaultDomain class, which in turn provides a Entities instance via getEntities(), which serves as a container for the domain entity definitions and as a factory for Entity and Key instances.

Entities

The Entity class is a map like structure, which maps column values to the attribute representing each column. Each table is represented by a EntityType constant with a name unique within the domain.

Note
Typically, the entityType name contains the underlying table name, but you can use whatever identifying string you want and specify the table name via a tableName parameter when defining the entity.
Attributes

Each column in a table is associated with a Attribute constant, which has a name and denotes the value type. The EntityType class provides factory methods for constructing Attribute objects for that entity type.

Note
Typically, the attribute name is the underlying column name, but as with the entityType you can use whatever value you want and specify the column name via the columnName() method when instantiating the associated Property.
Supported data types

The following data types are supported out of the box:

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

  • String (java.sql.Types.VARCHAR)

  • Boolean (java.sql.Types.BOOLEAN)

  • Character (java.sql.Types.CHAR)

  • byte[] (java.sql.Types.BLOB)

Properties

Each column is represented by the Property class or one of its subclasses. The Properties class provides factory methods for constructing Property objects and is usually statically imported, as will be assumed in the following examples. Each Property is based on an Attribute, unique within the Entity.

Example

EntityType constants for the Entities and Attribute constants for the properties in the World demo (simplified).

public interface World {

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

  interface City extends Entity {
    EntityType<Entity> TYPE = DOMAIN.entityType("world.city");
    Attribute<Integer> ID = TYPE.integerAttribute("id");
    Attribute<String> NAME = TYPE.stringAttribute("name");
    Attribute<String> COUNTRY_CODE = TYPE.stringAttribute("countrycode");
    Attribute<Entity> COUNTRY_FK = TYPE.entityAttribute("country_fk");
    Attribute<String> DISTRICT = TYPE.stringAttribute("district");
    Attribute<Integer> POPULATION = TYPE.integerAttribute("population");
  }

  interface Country {
    EntityType<Entity> TYPE = DOMAIN.entityType("world.country");
    Attribute<String> CODE = TYPE.stringAttribute("code");
    Attribute<String> NAME = TYPE.stringAttribute("name");
    Attribute<String> CONTINENT = TYPE.stringAttribute("continent");
    Attribute<String> REGION = TYPE.stringAttribute("region");
    Attribute<Double> SURFACEAREA = TYPE.doubleAttribute("surfacearea");
    Attribute<Integer> INDEPYEAR = TYPE.integerAttribute("indepyear");
    Attribute<Integer> POPULATION = TYPE.integerAttribute("population");
    Attribute<Double> LIFE_EXPECTANCY = TYPE.doubleAttribute("lifeexpectancy");
    Attribute<Double> GNP = TYPE.doubleAttribute("gnp");
    Attribute<Double> GNPOLD = TYPE.doubleAttribute("gnpold");
    Attribute<String> LOCALNAME = TYPE.stringAttribute("localname");
    Attribute<String> GOVERNMENTFORM = TYPE.stringAttribute("governmentform");
    Attribute<String> HEADOFSTATE = TYPE.stringAttribute("headofstate");
    Attribute<Integer> CAPITAL = TYPE.integerAttribute("capital");
    Attribute<Entity> CAPITAL_FK = TYPE.entityAttribute("capital_fk");
    Attribute<String> CODE_2 = TYPE.stringAttribute("code2");
    Attribute<Integer> CAPITAL_POPULATION = TYPE.integerAttribute("capital_population");
    Attribute<Integer> NO_OF_CITIES = TYPE.integerAttribute("no_of_cities");
    Attribute<Integer> NO_OF_LANGUAGES = TYPE.integerAttribute("no_of_languages");
    Attribute<byte[]> FLAG = TYPE.byteArrayAttribute("flag");
  }

  interface CountryLanguage extends Entity {
    EntityType<Entity> TYPE = DOMAIN.entityType("world.countrylanguage");
    Attribute<String> COUNTRY_CODE = TYPE.stringAttribute("countrycode");
    Attribute<Entity> COUNTRY_FK = TYPE.entityAttribute("country_fk");
    Attribute<String> LANGUAGE = TYPE.stringAttribute("language");
    Attribute<Boolean> IS_OFFICIAL = TYPE.booleanAttribute("isofficial");
    Attribute<Double> PERCENTAGE = TYPE.doubleAttribute("percentage");
    Attribute<Integer> NO_OF_SPEAKERS = TYPE.integerAttribute("noOfSpeakers");
  }

  interface Continent extends Entity {
    EntityType<Entity> TYPE = DOMAIN.entityType("continent");
    Attribute<String> NAME = TYPE.stringAttribute("continent");
    Attribute<Integer> SURFACE_AREA = TYPE.integerAttribute("sum(surfacearea)");
    Attribute<Long> POPULATION = TYPE.longAttribute("sum(population)");
    Attribute<Double> MIN_LIFE_EXPECTANCY = TYPE.doubleAttribute("min(lifeexpectancy)");
    Attribute<Double> MAX_LIFE_EXPECTANCY = TYPE.doubleAttribute("max(lifeexpectancy)");
    Attribute<Integer> MIN_INDEPENDENCE_YEAR = TYPE.integerAttribute("min(indepyear)");
    Attribute<Integer> MAX_INDEPENDENCE_YEAR = TYPE.integerAttribute("max(indepyear)");
    Attribute<Double> GNP = TYPE.doubleAttribute("sum(gnp)");
  }
}
Property

Property and its subclasses are used to represent entity properties, these can be transient or based on table columns.

Primary key

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

The only requirement is that the primary key properties represent a unique column combination for the underlying table, it does not have to correspond to an actual table primary key, although that is of course preferable. The framework does not enforce uniqueness for these properties, so a unique or primary key on the corresponding table columns is strongly recommended.

If no primary key properties are specified the entity is somewhat restricted, it can not be referenced via a foreign key and equals() does not work (since it is based on the primary key). You can still use the Entity.valuesEqual() method to check if all values are equal in two entities.

primaryKeyProperty(Country.CODE, "Country code")
        .updatable(true)
        .maximumLength(3),

In case of composite primary keys you create a ColumnProperty and specify the primary key index.

columnProperty(CountryLanguage.COUNTRY_CODE)
        .primaryKeyIndex(0)
        .updatable(true),
columnProperty(CountryLanguage.LANGUAGE, "Language")
        .primaryKeyIndex(1)
        .updatable(true),
ColumnProperty

ColumnProperty is used to represent properties that are based on table columns.

columnProperty(Country.REGION, "Region")
        .nullable(false)
        .maximumLength(26),
columnProperty(Country.SURFACEAREA, "Surface area")
        .nullable(false)
        .numberFormatGrouping(true)
        .maximumFractionDigits(2),
columnProperty(Country.INDEPYEAR, "Indep. year")
        .minimumValue(-2000).maximumValue(2500),
columnProperty(Country.POPULATION, "Population")
        .nullable(false)
        .numberFormatGrouping(true),
columnProperty(Country.LIFE_EXPECTANCY, "Life expectancy")
        .maximumFractionDigits(1)
        .minimumValue(0).maximumValue(99),
BlobProperty

BlobProperty is a specific ColumnProperty for the Types.BLOB type which provides lazy loading for BLOB values. Using a standard ColumnProperty with a byte array type works fine, but for lazy loading you have to use BlobProperty.

blobProperty(Country.FLAG, "Flag")
        .eagerlyLoaded(true),
ForeignKeyProperty

ForeignKeyProperty is a property used to represent a foreign key relation. These foreign keys refer to a unique key combination of attributes in the referenced entity, usually the primary key attribute(s).

foreignKeyProperty(Country.CAPITAL_FK, "Capital")
        .reference(Country.CAPITAL, City.ID),

Referring to an entity via a composite key.

foreignKeyProperty(Detail.MASTER_FK, "Master")
        .reference(Detail.MASTER_ID_1, Master.ID_1)
        .reference(Detail.MASTER_ID_2, Master.ID_2);

The above assumes the below master definition.

define(Master.TYPE,
    ...
    columnProperty(Master.ID_1),
    columnProperty(Master.ID_2)
    ...
);
Boolean Properties

For databases supporting Types.BOOLEAN you simply use Properties.columnProperty.

columnProperty(CountryLanguage.IS_OFFICIAL, "Is official")
        .columnHasDefaultValue(true)
        .nullable(false),

For databases lacking native boolean support we use the Properties.booleanProperty method, specifying the underlying true/false values.

booleanProperty(Customer.IS_ACTIVE, "Is active", Integer.class, 1, 0)

For boolean columns using unconventional types you can specify the true and false values.

booleanProperty(Customer.IS_ACTIVE, "Is active", String.class, "true", "false")
booleanProperty(Customer.IS_ACTIVE, "Is active", Character.class, 'T', 'F')

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

entity.put(Customer.IS_ACTIVE, true);

boolean isActive = entity.get(Customer.IS_ACTIVE);
DenormalizedViewProperty

An entity can include a property value from a entity referenced via foreign key, by defining a denormalized view property.

denormalizedViewProperty(Country.CAPITAL_POPULATION, "Capital pop.",
        Country.CAPITAL_FK, City.POPULATION)
        .numberFormatGrouping(true),
DenormalizedProperty

DenormalizedProperty is used for columns that should automatically get their value from a column in a referenced table. This property automatically gets the value from the attribute in the referenced table when the corresponding reference property value is set.

denormalizedProperty(Customer.CITY, Customer.ADDRESS_FK, Address.CITY, "City")
Note
The property is not kept in sync if the value of the denormalized property is modified in the referenced entity.
Entities entities = getEntities();

Entity address = entities.entity(Address.TYPE);
address.put(Address.CITY, "Syracuse");

Entity customer = entities.entity(Customer.TYPE);
customer.put(Customer.ADDRESS_FK, address);

customer.get(Customer.CITY);//returns "Syracuse"

//NB
address.put(Address.CITY, "Canastota");
customer.get(Customer.CITY, still returns "Syracuse"

customer.put(Customer.ADDRESS_FK, address);//set the referenced value again
customer.get(Customer.CITY);//now this returns "Canastota"
SubqueryProperty

SubqueryProperty is used to represent a property which gets its value from a subquery returning a single value. Note that in the example below country.code must be available when the query is run, that is, the entity must include that column as a property.

subqueryProperty(Country.NO_OF_CITIES, "No. of cities",
        "select count(*) from world.city where city.countrycode = country.code"),
TransientProperty

TransientProperty is used to represent a property which is not based on an underlying column, these properties all have a default value of null and can be set and retrieved just like normal properties.

DerivedProperty

DerivedProperty is used to represent a transient property which value is derived from one or more properties in the same entity. The value of a derived property is provided via a DerivedProperty.Provider implementation as shown below.

derivedProperty(CountryLanguage.NO_OF_SPEAKERS, "No. of speakers",
        new NoOfSpeakersProvider(), CountryLanguage.COUNTRY_FK, CountryLanguage.PERCENTAGE)
        .numberFormatGrouping(true),
private static final class NoOfSpeakersProvider
        implements DerivedProperty.Provider<Integer> {

  private static final long serialVersionUID = 1;

  @Override
  public Integer get(final DerivedProperty.SourceValues sourceValues) {
    Double percentage = sourceValues.get(CountryLanguage.PERCENTAGE);
    Entity country = sourceValues.get(CountryLanguage.COUNTRY_FK);
    if (notNull(percentage, country) && country.isNotNull(Country.POPULATION)) {
      return Double.valueOf(country.get(Country.POPULATION) * (percentage / 100)).intValue();
    }

    return null;
  }
}
Domain

Each entity type is defined by calling DefaultDomain.define. The framework assumes the entityType name contains the table name, unless the tableName parameter is specified.

void city() {
  define(City.TYPE,
          primaryKeyProperty(City.ID),
          columnProperty(City.NAME, "Name")
                  .searchProperty(true)
                  .nullable(false)
                  .maximumLength(35),
          columnProperty(City.COUNTRY_CODE)
                  .nullable(false),
          foreignKeyProperty(City.COUNTRY_FK, "Country")
                  .reference(City.COUNTRY_CODE, Country.CODE),
          columnProperty(City.DISTRICT, "District")
                  .nullable(false)
                  .maximumLength(20),
          columnProperty(City.POPULATION, "Population")
                  .nullable(false)
                  .numberFormatGrouping(true))
          .keyGenerator(sequence("world.city_seq"))
          .validator(new CityValidator())
          .orderBy(orderBy().ascending(City.NAME))
          .stringFactory(stringFactory(City.NAME))
          .colorProvider(new CityColorProvider(getEntities()))
          .caption("City");
}
KeyGenerator

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

Auto-increment

This assumes the underlying primary key column is either an auto-increment column or is populated from a sequence using a trigger during insert. For auto-increment columns the valueSource parameter should be the table name and for a sequence/trigger it should be the sequence name.

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

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

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

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

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

//Using a query returning the new value
.keyGenerator(queried(
    "select new_id
     from store.id_values
     where table_name = 'store.customer'"));
Increment

The framework can automatically increment the primary key value by selecting the maximum value and add one, this is very simplistic, not transaction safe and is not recommended for use anywhere but the simplest demos.

.keyGenerator(increment("scott.emp", "empno"))
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, List<ColumnProperty<?>> primaryKeyProperties,
                           DatabaseConnection connection) throws SQLException {
    entity.put(Customer.ID, UUID.randomUUID().toString());
  }
}
StringFactory

The StringFactory class provides a builder for Function<Entity, String> which provides 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.

define(Address.TYPE,
        primaryKeyProperty(Address.ID),
        columnProperty(Address.STREET, "Street")
                .nullable(false).maximumLength(120),
        columnProperty(Address.CITY, "City")
                .nullable(false).maximumLength(50),
        columnProperty(Address.VALID, "Valid")
                .columnHasDefaultValue(true).nullable(false))
        .stringFactory(stringFactory(Address.STREET)
                .text(", ").value(Address.CITY))
        .keyGenerator(automatic("store.address"))
        .smallDataset(true)
        .caption("Address");

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

private static final class CustomerToString implements Function<Entity, String>, Serializable {

  private static final long serialVersionUID = 1;

  @Override
  public String apply(Entity customer) {
    StringBuilder builder =
            new StringBuilder(customer.get(Customer.LAST_NAME))
                    .append(", ")
                    .append(customer.get(Customer.FIRST_NAME));
    if (customer.isNotNull(Customer.EMAIL)) {
      builder.append(" <")
              .append(customer.get(Customer.EMAIL))
              .append(">");
    }

    return builder.toString();
  }
}
ColorProvider

ColorProvider is used to provide colors for entity properties, used as background color in table cells for example.

private static final class CityColorProvider implements ColorProvider {

  private static final long serialVersionUID = 1;

  private final Entities entities;

  public CityColorProvider(Entities entities) {
    this.entities = entities;
  }

  @Override
  public Object getColor(Entity cityEntity, Attribute<?> attribute) {
    if (attribute.equals(City.POPULATION) &&
            cityEntity.get(City.POPULATION) > 1_000_000) {
      return Color.YELLOW;
    }
    City city = entities.castTo(City.TYPE, cityEntity);
    if (attribute.equals(City.NAME) && city.isCapital()) {
      return Color.CYAN;
    }

    return null;
  }
}
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. Note that validation is performed quite often so it should not perform expensive operations. Validation requiring database access for example belongs in the application model or ui.

private static final class CityValidator
        extends DefaultEntityValidator implements Serializable {

  private static final long serialVersionUID = 1;

  @Override
  public void validate(Entity city, EntityDefinition cityDefinition) throws ValidationException {
    super.validate(city, cityDefinition);
    //after a call to super.validate() property values that are not nullable
    //(such as country and population) are guaranteed to be non-null
    Entity country = city.get(City.COUNTRY_FK);
    Integer cityPopulation = city.get(City.POPULATION);
    Integer countryPopulation = country.get(Country.POPULATION);
    if (countryPopulation != null && cityPopulation > countryPopulation) {
      throw new ValidationException(City.POPULATION,
              cityPopulation, "City population can not exceed country population");
    }
  }
}
Entities in action

Using the Entity class is rather straight forward.

EntityConnectionProvider connectionProvider = EntityConnectionProviders.connectionProvider()
        .setDomainClassName(Petstore.class.getName())
        .setClientTypeId("Manual")
        .setUser(Users.parseUser("scott:tiger"));

Entities store = connectionProvider.getEntities();

EntityConnection connection = connectionProvider.getConnection();

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

connection.insert(insects);

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

connection.insert(smallBeetles);

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

List<Entity> catProducts = connection.select(Product.CATEGORY_FK, categoryCats);

catProducts.forEach(System.out::println);
Unit Testing
Introduction

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

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.

EntityTestUnit

The following methods all have default implementations which are based on randomly created property values, based on the constraints set in the domain model, override if the default ones are not working.

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

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

  • modifyEntity 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 modifyEntity 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 EntityTestUnit {

  public StoreTest() {
    super(Store.class.getName());
  }

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

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

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

  @Override
  protected Entity initializeReferenceEntity(EntityType<?> entityType,
                                             Map<EntityType<?>, Entity> foreignKeyEntities) {
    //see if the currently running test requires an ADDRESS entity
    if (entityType.equals(Address.TYPE)) {
      Entity address = getEntities().entity(Address.TYPE);
      address.put(Address.ID, 21);
      address.put(Address.STREET, "One Way");
      address.put(Address.CITY, "Sin City");

      return address;
    }

    return super.initializeReferenceEntity(entityType, foreignKeyEntities);
  }

  @Override
  protected Entity initializeTestEntity(EntityType<?> entityType,
                                        Map<EntityType<?>, Entity> foreignKeyEntities) {
    if (entityType.equals(Address.TYPE)) {
      //Initialize a entity representing the table STORE.ADDRESS,
      //which can be used for the testing
      Entity address = getEntities().entity(Address.TYPE);
      address.put(Address.ID, 42);
      address.put(Address.STREET, "Street");
      address.put(Address.CITY, "City");

      return address;
    }
    else if (entityType.equals(Customer.TYPE)) {
      //Initialize a entity representing the table STORE.CUSTOMER,
      //which can be used for the testing
      Entity customer = getEntities().entity(Customer.TYPE);
      customer.put(Customer.ID, UUID.randomUUID().toString());
      customer.put(Customer.FIRST_NAME, "Robert");
      customer.put(Customer.LAST_NAME, "Ford");
      customer.put(Customer.IS_ACTIVE, true);

      return customer;
    }
    else if (entityType.equals(CustomerAddress.TYPE)) {
      Entity customerAddress = getEntities().entity(CustomerAddress.TYPE);
      customerAddress.put(CustomerAddress.CUSTOMER_FK, foreignKeyEntities.get(Customer.TYPE));
      customerAddress.put(CustomerAddress.ADDRESS_FK, foreignKeyEntities.get(Address.TYPE));

      return customerAddress;
    }

    return super.initializeTestEntity(entityType, foreignKeyEntities);
  }

  @Override
  protected void modifyEntity(Entity testEntity, Map<EntityType<?>, Entity> foreignKeyEntities) {
    if (testEntity.getEntityType().equals(Address.TYPE)) {
      testEntity.put(Address.STREET, "New Street");
      testEntity.put(Address.CITY, "New City");
    }
    else if (testEntity.getEntityType().equals(Customer.TYPE)) {
      //It is sufficient to change the value of a single property, but the more the merrier
      testEntity.put(Customer.FIRST_NAME, "Jesse");
      testEntity.put(Customer.LAST_NAME, "James");
      testEntity.put(Customer.IS_ACTIVE, false);
    }
  }
}

1.1.2. Conditions

The Conditions class is a factory for conditions, used when querying entities.

The Chinook domain model is used in the examples below.

Condition

Condition represents a where clause for a entity type.

Condition.Combination

Condition.Combination represents a combination of Conditions, which are either AND’ed or OR’ed together. These can be nested and combined with other condition combinations.

SelectCondition

SelectCondition represents a where clause for a entity type specifically for selecting.

UpdateCondition

UpdateCondition represents a where clause and values for updating one or more entities.

1.1.3. EntityConnection

The Codion database layer is extremely thin, it doesn’t perform any joins and provides no access to DBMS specific funtionality except primary key generation via KeyGenerator strategies. The framework provides implementations for the most common strategies, sequences (with or without triggers) and auto increment columns.

The database layer is specified by the EntityConnection class. It provides methods for selecting, inserting, updating and deleting entities, executing procedures and functions, filling reports as well as providing transaction control.

The Chinook domain model is used in the examples below.

Note
Static imports are used extensively in the examples, see full example code.
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 fetch depth of one. This means that selecting a track you get all the entities referenced via foreign keys as well.

The N+1 problem

This means that selecting tracks performs four queries, but that number of queries is the same whether you select one or a thousand tracks.

The fetch depth can be configured on a foreign key basis when defining entities. A fetch depth of zero means that foreign key reference is not 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 fetch depth means no limit. This limiting of foreign key fetch depth can be turned off, meaning the full reference graph is always fetched, via the codion.db.limitForeignKeyFetchDepth=false system property or LocalEntityConnection.LIMIT_FOREIGN_KEY_FETCH_DEPTH.set(false), or on a connection instance via connection.setLimitFetchDepth(false).

foreignKeyProperty(InvoiceLine.INVOICE_FK, "Invoice")
        .reference(InvoiceLine.INVOICE_ID, Invoice.ID)
        .fetchDepth(0)
        .nullable(false),
columnProperty(InvoiceLine.TRACK_ID),
foreignKeyProperty(Track.ALBUM_FK, "Album")
        .reference(Track.ALBUM_ID, Album.ID)
        .fetchDepth(2)
        .preferredColumnWidth(160),
List<Entity> tracks = connection.select(Track.NAME, "Bad%");

Entity track = tracks.get(0);

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

// fetch 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.getForeignKey(Album.ARTIST_FK);

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

SelectCondition selectCondition =
        condition(Track.NAME).equalTo("Bad%")
                .selectCondition().setFetchDepth(0);

List<Entity> tracks = connection.select(selectCondition);

Entity track = tracks.get(0);

// this 'genre' instance contains only the primary key, since the
// condition fetch depth limit prevented it from being selected
Entity genre = track.getForeignKey(Track.GENRE_FK);
SelectCondition selectCondition =
        condition(Track.NAME).equalTo("Bad%")
                .selectCondition().setFetchDepth(Track.ALBUM_FK, 0);

List<Entity> tracks = connection.select(selectCondition);

Entity track = tracks.get(0);

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

// this 'album' instance contains only the primary key, since the
// condition fetch depth limit prevented it from being selected
Entity album = track.getForeignKey(Track.ALBUM_FK);
select

For selecting one or more rows.

SelectCondition condition =
        condition(Artist.NAME).equalTo("The %").selectCondition();

List<Entity> artists = connection.select(condition);

condition =
        condition(Album.ARTIST_FK).equalTo(artists)
                .and(condition(Album.TITLE).notEqualTo("%live%")
                        .setCaseSensitive(false)).selectCondition();

List<Entity> nonLiveAlbums = connection.select(condition);
Entities entities = connection.getEntities();
Key key42 = entities.primaryKey(Artist.TYPE, 42L);
Key key43 = entities.primaryKey(Artist.TYPE, 43L);

List<Entity> artists = connection.select(asList(key42, key43));
Entity aliceInChains = connection.selectSingle(Artist.NAME, "Alice In Chains");

List<Entity> albums = connection.select(Album.ARTIST_FK, aliceInChains);
selectSingle

For selecting single rows.

Entity ironMaiden = connection.selectSingle(
        condition(Artist.NAME).equalTo("Iron Maiden").selectCondition());

Entity liveAlbum = connection.selectSingle(
        condition(Album.ARTIST_FK).equalTo(ironMaiden)
                .and(condition(Album.TITLE).equalTo("%live after%")
                        .setCaseSensitive(false)).selectCondition());
Key key42 = connection.getEntities().primaryKey(Artist.TYPE, 42L);

Entity artists = connection.selectSingle(key42);
Entity aliceInChains = connection.selectSingle(Artist.NAME, "Alice In Chains");

// we only have one album by Alice in Chains
// otherwise this would throw an exception
Entity albumFacelift = connection.selectSingle(Album.ARTIST_FK, aliceInChains);
select

For selecting the values of a single column.

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

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

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

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

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

For selecting the row count given a condition.

int numberOfItStaff = connection.rowCount(condition(Employee.TITLE).equalTo("IT Staff"));
Modifying
insert

For inserting rows.

Entities entities = connection.getEntities();

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

connection.insert(myBand);

Entity album = entities.entity(Album.TYPE);
album.put(Album.ARTIST_FK, myBand);
album.put(Album.TITLE, "First album");

connection.insert(album);
update

For updating one or more entity instances. These methods throw an exception if the entities are not modified.

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

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

connection.update(myBand);
Optimistic locking

The framework performs optimistic locking during updates of entity instances using the methods above, by selecting the entities being updated for update (when supported by the underlying database) and comparing all the original property values to the current row values, throwing an exception if any value differs or the record is missing. Optimistic locking can be turned off system wide using the codion.db.useOptimisticLocking=false system property or LocalEntityConnection.USE_OPTIMISTIC_LOCKING.set(false), or on a connection instance via connection.setOptimisticLockingEnabled(false).

For updating by where condition.

UpdateCondition updateCondition = condition(Artist.NAME).equalTo("Azymuth").updateCondition();

updateCondition.set(Artist.NAME, "Another Name");

connection.update(updateCondition);
delete

For deleting existing rows.

Entity myBand = connection.selectSingle(Artist.NAME, "Proper Name");

int deleteCount = connection.delete(condition(Album.ARTIST_FK).equalTo(myBand));
Entity myBand = connection.selectSingle(Artist.NAME, "Proper Name");

boolean deleted = connection.delete(myBand.getPrimaryKey());
Procedures & Functions
executeProcedure
connection.executeProcedure(Invoice.UPDATE_TOTALS);
executeFunction
List<Long> trackIds = asList(123L, 1234L);
BigDecimal priceIncrease = BigDecimal.valueOf(0.1);

List<Entity> modifiedTracks = connection.executeFunction(Track.RAISE_PRICE, asList(trackIds, priceIncrease));
Reporting
fillReport
Map<String, Object> reportParameters = new HashMap<>();
reportParameters.put("CUSTOMER_IDS", asList(42, 43, 45));

JasperPrint jasperPrint = connection.fillReport(Customer.REPORT, reportParameters);
Full example
Show code
package is.codion.framework.demos.chinook.manual;

import is.codion.common.db.database.Database;
import is.codion.common.db.database.Databases;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.db.reports.ReportException;
import is.codion.common.user.Users;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.condition.SelectCondition;
import is.codion.framework.db.condition.UpdateCondition;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.demos.chinook.domain.impl.ChinookImpl;
import is.codion.framework.domain.entity.Entities;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.Key;

import net.sf.jasperreports.engine.JasperPrint;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static is.codion.framework.db.condition.Conditions.condition;
import static is.codion.framework.demos.chinook.domain.Chinook.*;
import static java.util.Arrays.asList;

/**
 * When running this make sure the chinook demo module directory is the
 * working directory, due to a relative path to a db init script
 */
public final class EntityConnectionDemo {

  static void selectConditionDemo(EntityConnection connection) throws DatabaseException {
    // tag::selectCondition[]
    SelectCondition condition =
            condition(Artist.NAME).equalTo("The %").selectCondition();

    List<Entity> artists = connection.select(condition);

    condition =
            condition(Album.ARTIST_FK).equalTo(artists)
                    .and(condition(Album.TITLE).notEqualTo("%live%")
                            .setCaseSensitive(false)).selectCondition();

    List<Entity> nonLiveAlbums = connection.select(condition);
    // end::selectCondition[]
  }

  static void fetchDepthEntity(EntityConnection connection) throws DatabaseException {
    // tag::fetchDepthEntity[]
    List<Entity> tracks = connection.select(Track.NAME, "Bad%");

    Entity track = tracks.get(0);

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

    // fetch 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.getForeignKey(Album.ARTIST_FK);
    // end::fetchDepthEntity[]
  }

  static void fetchDepthCondition(EntityConnection connection) throws DatabaseException {
    // tag::fetchDepthCondition[]
    SelectCondition selectCondition =
            condition(Track.NAME).equalTo("Bad%")
                    .selectCondition().setFetchDepth(0);

    List<Entity> tracks = connection.select(selectCondition);

    Entity track = tracks.get(0);

    // this 'genre' instance contains only the primary key, since the
    // condition fetch depth limit prevented it from being selected
    Entity genre = track.getForeignKey(Track.GENRE_FK);
    // end::fetchDepthCondition[]
  }

  static void fetchDepthForeignKeyCondition(EntityConnection connection) throws DatabaseException {
    // tag::fetchDepthConditionForeignKey[]
    SelectCondition selectCondition =
            condition(Track.NAME).equalTo("Bad%")
                    .selectCondition().setFetchDepth(Track.ALBUM_FK, 0);

    List<Entity> tracks = connection.select(selectCondition);

    Entity track = tracks.get(0);

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

    // this 'album' instance contains only the primary key, since the
    // condition fetch depth limit prevented it from being selected
    Entity album = track.getForeignKey(Track.ALBUM_FK);
    // end::fetchDepthConditionForeignKey[]
  }

  static void selectKeys(EntityConnection connection) throws DatabaseException {
    // tag::selectKeys[]
    Entities entities = connection.getEntities();
    Key key42 = entities.primaryKey(Artist.TYPE, 42L);
    Key key43 = entities.primaryKey(Artist.TYPE, 43L);

    List<Entity> artists = connection.select(asList(key42, key43));
    // end::selectKeys[]
  }

  static void selectValue(EntityConnection connection) throws DatabaseException {
    // tag::selectValue[]
    Entity aliceInChains = connection.selectSingle(Artist.NAME, "Alice In Chains");

    List<Entity> albums = connection.select(Album.ARTIST_FK, aliceInChains);
    // end::selectValue[]
  }

  static void selectSingleCondition(EntityConnection connection) throws DatabaseException {
    // tag::selectSingleCondition[]
    Entity ironMaiden = connection.selectSingle(
            condition(Artist.NAME).equalTo("Iron Maiden").selectCondition());

    Entity liveAlbum = connection.selectSingle(
            condition(Album.ARTIST_FK).equalTo(ironMaiden)
                    .and(condition(Album.TITLE).equalTo("%live after%")
                            .setCaseSensitive(false)).selectCondition());
    // end::selectSingleCondition[]
  }

  static void selectSingleKeys(EntityConnection connection) throws DatabaseException {
    // tag::selectSingleKeys[]
    Key key42 = connection.getEntities().primaryKey(Artist.TYPE, 42L);

    Entity artists = connection.selectSingle(key42);
    // end::selectSingleKeys[]
  }

  static void selectSingleValue(EntityConnection connection) throws DatabaseException {
    // tag::selectSingleValue[]
    Entity aliceInChains = connection.selectSingle(Artist.NAME, "Alice In Chains");

    // we only have one album by Alice in Chains
    // otherwise this would throw an exception
    Entity albumFacelift = connection.selectSingle(Album.ARTIST_FK, aliceInChains);
    // end::selectSingleValue[]
  }

  static void selectValues(EntityConnection connection) throws DatabaseException {
    // tag::selectValues[]
    List<String> customerUsStates = connection.select(Customer.STATE,
            condition(Customer.COUNTRY).equalTo("USA"));
    // end::selectValues[]
  }

  static void selectDependencies(EntityConnection connection) throws DatabaseException {
    // tag::selectDependencies[]
    List<Entity> employees = connection.select(condition(Employee.TYPE).selectCondition());

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

    Collection<Entity> customersDependingOnEmployees = dependencies.get(Customer.TYPE);
    // end::selectDependencies[]
  }

  static void rowCount(EntityConnection connection) throws DatabaseException {
    // tag::rowCount[]
    int numberOfItStaff = connection.rowCount(condition(Employee.TITLE).equalTo("IT Staff"));
    // end::rowCount[]
  }

  static void insert(EntityConnection connection) throws DatabaseException {
    // tag::insert[]
    Entities entities = connection.getEntities();

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

    connection.insert(myBand);

    Entity album = entities.entity(Album.TYPE);
    album.put(Album.ARTIST_FK, myBand);
    album.put(Album.TITLE, "First album");

    connection.insert(album);
    // end::insert[]
  }

  static void update(EntityConnection connection) throws DatabaseException {
    // tag::update[]
    Entity myBand = connection.selectSingle(Artist.NAME, "My Band");

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

    connection.update(myBand);
    // end::update[]
  }

  static void updateConditionDemo(EntityConnection connection) throws DatabaseException {
    // tag::updateCondition[]
    UpdateCondition updateCondition = condition(Artist.NAME).equalTo("Azymuth").updateCondition();

    updateCondition.set(Artist.NAME, "Another Name");

    connection.update(updateCondition);
    // end::updateCondition[]
  }

  static void deleteCondition(EntityConnection connection) throws DatabaseException {
    // tag::deleteCondition[]
    Entity myBand = connection.selectSingle(Artist.NAME, "Proper Name");

    int deleteCount = connection.delete(condition(Album.ARTIST_FK).equalTo(myBand));
    // end::deleteCondition[]
  }

  static void deleteKey(EntityConnection connection) throws DatabaseException {
    // tag::deleteKey[]
    Entity myBand = connection.selectSingle(Artist.NAME, "Proper Name");

    boolean deleted = connection.delete(myBand.getPrimaryKey());
    // end::deleteKey[]
  }

  static void procedure(EntityConnection connection) throws DatabaseException {
    // tag::procedure[]
    connection.executeProcedure(Invoice.UPDATE_TOTALS);
    // end::procedure[]
  }

  static void function(EntityConnection connection) throws DatabaseException {
    // tag::function[]
    List<Long> trackIds = asList(123L, 1234L);
    BigDecimal priceIncrease = BigDecimal.valueOf(0.1);

    List<Entity> modifiedTracks = connection.executeFunction(Track.RAISE_PRICE, asList(trackIds, priceIncrease));
    // end::function[]
  }

  static void report(EntityConnection connection) throws ReportException, DatabaseException {
    // tag::report[]
    Map<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("CUSTOMER_IDS", asList(42, 43, 45));

    JasperPrint jasperPrint = connection.fillReport(Customer.REPORT, reportParameters);
    //end::report[]
  }

  static void transaction(EntityConnection connection) throws DatabaseException {
    // tag::transaction[]
    try {
      connection.beginTransaction();

      //perform insert/update/delete

      connection.commitTransaction();
    }
    catch (Exception e) {
      connection.rollbackTransaction();
      throw e;
    }
    // end::transaction[]
  }

  static void main(String[] args) throws DatabaseException, ReportException {
    Database.DATABASE_URL.set("jdbc:h2:mem:h2db");
    Database.DATABASE_INIT_SCRIPTS.set("src/main/sql/create_schema.sql");

    EntityConnectionProvider connectionProvider =
            new LocalEntityConnectionProvider(Databases.getInstance())
                    .setDomainClassName(ChinookImpl.class.getName())
                    .setUser(Users.parseUser("scott:tiger"));

    EntityConnection connection = connectionProvider.getConnection();
    selectConditionDemo(connection);
    fetchDepthEntity(connection);
    fetchDepthCondition(connection);
    fetchDepthForeignKeyCondition(connection);
    selectKeys(connection);
    selectValue(connection);
    selectSingleCondition(connection);
    selectSingleKeys(connection);
    selectSingleValue(connection);
    selectValues(connection);
    selectDependencies(connection);
    rowCount(connection);
    insert(connection);
    update(connection);
    updateConditionDemo(connection);
    deleteCondition(connection);
    deleteKey(connection);
    procedure(connection);
    function(connection);
    report(connection);
    transaction(connection);
  }
}
Transaction control
try {
  connection.beginTransaction();

  //perform insert/update/delete

  connection.commitTransaction();
}
catch (Exception e) {
  connection.rollbackTransaction();
  throw e;
}
LocalEntityConnection

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

RemoteEntityConnection

A EntityConnection implementation based on a RMI connection. Requires a server.

HttpEntityConnection

A EntityConnection implementation based on HTTP. Requires a server.

1.1.4. EntityConnectionProvider

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.

The EntityConnectionProvider manages a single connection, that is, the one returned by getConnection(). If a connection becomes invalid, i.e. due to a network outage or a server restart the EntityConnectionProvider is responsible for reconnecting and returning a new valid connection. If the EntityConnectionProvider is unable to connect to the underlying database or server getConnection() throws an exception.

A reference to the EntityConnection instance returned by getConnection() should only be kept for a short time, i.e. as a method field or parameter, and should not be cached or kept as a class field since it can become invalid and thereby unusable. Always use getConnection() to be sure you have a healthy EntityConnection.

LocalEntityConnectionProvider

Provides a connection based on a local JDBC connection.

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

LocalEntityConnectionProvider connectionProvider =
        new LocalEntityConnectionProvider(Databases.getInstance());

connectionProvider.setDomainClassName(ChinookImpl.class.getName());
connectionProvider.setUser(Users.parseUser("scott:tiger"));

LocalEntityConnection entityConnection =
        (LocalEntityConnection) connectionProvider.getConnection();

DatabaseConnection databaseConnection =
        entityConnection.getDatabaseConnection();

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

connectionProvider.disconnect();
RemoteEntityConnectionProvider

Provides a connection based on a remote RMI connection.

RemoteEntityConnectionProvider connectionProvider =
        new RemoteEntityConnectionProvider("localhost", -1, 1099);

connectionProvider.setDomainClassName(ChinookImpl.class.getName());
connectionProvider.setUser(Users.parseUser("scott:tiger"));
connectionProvider.setClientTypeId(EntityConnectionProviderDemo.class.getSimpleName());

EntityConnection entityConnection =
        connectionProvider.getConnection();

Entities entities = entityConnection.getEntities();

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

connectionProvider.disconnect();
HttpEntityConnectionProvider

Provides a connection based on a remote HTTP connection.

HttpEntityConnectionProvider connectionProvider =
        new HttpEntityConnectionProvider("localhost", 8080, ClientHttps.FALSE);

connectionProvider.setDomainClassName(ChinookImpl.class.getName());
connectionProvider.setClientTypeId(EntityConnectionProviderDemo.class.getSimpleName());
connectionProvider.setUser(Users.parseUser("scott:tiger"));

EntityConnection entityConnection = connectionProvider.getConnection();

Entities entities = entityConnection.getEntities();

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

connectionProvider.disconnect();

1.2. Framework Model

1.2.1. EntityModel

The EntityModel class links together and coordinates between a EntityEditModel and a EntityTableModel, where the EntityEditModel handles CRUD operations and the EntityTableModel provides a table representation of the underlying entities.

public class AddressModel extends SwingEntityModel {

  public AddressModel(EntityConnectionProvider connectionProvider) {
    super(Address.TYPE, connectionProvider);
  }
}
public class CustomerModel extends SwingEntityModel {

  public CustomerModel(EntityConnectionProvider connectionProvider) {
    super(new CustomerEditModel(connectionProvider),
            new CustomerTableModel(connectionProvider));
    bindEvents();
  }

  private void bindEvents() {
    getTableModel().addRefreshStartedListener(() ->
            System.out.println("Refresh is about to start"));

    getEditModel().addValueListener(Customer.FIRST_NAME, valueChange ->
            System.out.println("Attribute " + valueChange.getAttribute() +
                    " changed from " + valueChange.getPreviousValue() +
                    " to " + valueChange.getValue()));
  }
}
public class CustomerAddressModel extends SwingEntityModel {

  public CustomerAddressModel(EntityConnectionProvider connectionProvider) {
    super(new CustomerAddressTableModel(connectionProvider));
  }
}
Edit model

Each EntityModel contains a single EntityEditModel instance. This edit model can be created automatically by the EntityModel or supplied via a constructor argument in case of a custom implementation.

public class CustomerEditModel extends SwingEntityEditModel {

  public CustomerEditModel(EntityConnectionProvider connectionProvider) {
    super(Customer.TYPE, connectionProvider);
  }
}
Table model

Each EntityModel can contain a single EntityTableModel instance. This table model can be created automatically by the EntityModel or supplied via a constructor argument in case of a specialized implementation.

public class CustomerTableModel extends SwingEntityTableModel {

  public CustomerTableModel(EntityConnectionProvider connectionProvider) {
    super(Customer.TYPE, connectionProvider);
  }
}
Detail models

Detail models can be added to a model, this relies on a foreign key reference between the entities involved.

public class StoreAppModel extends SwingEntityApplicationModel {

  public StoreAppModel(EntityConnectionProvider connectionProvider) {
    super(connectionProvider);

    CustomerModel customerModel = new CustomerModel(connectionProvider);
    CustomerAddressModel customerAddressModel = new CustomerAddressModel(connectionProvider);

    customerModel.addDetailModel(customerAddressModel);

    addEntityModel(customerModel);
  }
}
Event binding

The EntityModel, EntityEditModel and EntityTableModel classes expose a number of addListener methods.

The following example prints, to the standard output, all changes made to a given property as well as a message indicating that a refresh has started.

private void bindEvents() {
  getTableModel().addRefreshStartedListener(() ->
          System.out.println("Refresh is about to start"));

  getEditModel().addValueListener(Customer.FIRST_NAME, valueChange ->
          System.out.println("Attribute " + valueChange.getAttribute() +
                  " changed from " + valueChange.getPreviousValue() +
                  " to " + valueChange.getValue()));
}

1.2.2. EntityEditModel

The EntityEditModel interface defines the CRUD business logic used by the EntityEditPanel class when entities are being edited. The EntityEditModel works with a single entity instance, called the active entity, which can be set via the setEntity(Entity entity) method and retrieved via getEntityCopy(). The EntityEditModel interface exposes a number of methods for manipulating as well as querying the property values of the active entity.

public class CustomerEditModel extends SwingEntityEditModel {

  public CustomerEditModel(EntityConnectionProvider connectionProvider) {
    super(Customer.TYPE, connectionProvider);
  }
}
EntityConnectionProvider connectionProvider =
        EntityConnectionProviders.connectionProvider()
                .setDomainClassName(Store.class.getName())
                .setUser(Users.parseUser("scott:tiger"))
                .setClientTypeId("StoreMisc");

CustomerEditModel editModel = new CustomerEditModel(connectionProvider);

editModel.put(Customer.ID, UUID.randomUUID().toString());
editModel.put(Customer.FIRST_NAME, "Björn");
editModel.put(Customer.LAST_NAME, "Sigurðsson");
editModel.put(Customer.IS_ACTIVE, true);

//inserts and returns the inserted entity
Entity customer = editModel.insert();

//modify some property values
editModel.put(Customer.FIRST_NAME, "John");
editModel.put(Customer.LAST_NAME, "Doe");

//updates and returns the updated entity
customer = editModel.update();

//deletes the active entity
editModel.delete();

1.2.3. EntityTableModel

The EntityTableModel class provides a table representation of the underlying entities.

public class CustomerTableModel extends SwingEntityTableModel {

  public CustomerTableModel(EntityConnectionProvider connectionProvider) {
    super(Customer.TYPE, connectionProvider);
  }
}
public class CustomerAddressTableModel extends SwingEntityTableModel {

  public CustomerAddressTableModel(final EntityConnectionProvider connectionProvider) {
    super(CustomerAddress.TYPE, connectionProvider);
  }
}

1.2.4. EntityApplicationModel

The EntityApplicationModel class serves as the base for the application. Its main purpose is to hold references to the root EntityModel instances used by the application.

When implementing this class you must provide a constructor with a single EntityConnectionProvider parameter, as seen below.

public class StoreAppModel extends SwingEntityApplicationModel {

  public StoreAppModel(EntityConnectionProvider connectionProvider) {
    super(connectionProvider);

    CustomerModel customerModel = new CustomerModel(connectionProvider);
    CustomerAddressModel customerAddressModel = new CustomerAddressModel(connectionProvider);

    customerModel.addDetailModel(customerAddressModel);

    addEntityModel(customerModel);
  }
}

1.2.5. Application load testing

The application load testing harness is used to see how your application, server and database handle multiple concurrent users. This is done by extending the abstract class EntityLoadTestModel.

public class StoreLoadTest extends EntityLoadTestModel<StoreAppModel> {

  public StoreLoadTest(User user) {
    super(user, singletonList(new UsageScenario()));
  }

  @Override
  protected StoreAppModel initializeApplication() {
    EntityConnectionProvider connectionProvider =
            new RemoteEntityConnectionProvider()
                    .setClientId(UUID.randomUUID())
                    .setUser(getUser())
                    .setDomainClassName(Store.class.getName());

    return new StoreAppModel(connectionProvider);
  }

  private static class UsageScenario extends
          EntityLoadTestModel.AbstractEntityUsageScenario<StoreAppModel> {

    @Override
    protected void perform(StoreAppModel application)
            throws ScenarioException {
      try {
        EntityModel customerModel = application.getEntityModel(Customer.TYPE);
        customerModel.refresh();
        selectRandomRow(customerModel.getTableModel());
      }
      catch (Exception e) {
        throw new ScenarioException(e);
      }
    }
  }
}

1.3. Framework UI

1.3.1. EntityPanel

The EntityPanel is the base UI class for working with entity instances. It usually consists of an EntityTablePanel, an EntityEditPanel, and a set of detail panels representing the entities having a master/detail relationship with the underlying entity.

Detail panels

Adding a detail panel is done with a single method call, but note that the underlying EntityModel must contain the correct detail model for the detail panel, in this case a CustomerModel instance, see detail models. See EntityApplicationPanel.

1.3.2. EntityEditPanel

The EntityEditPanel contains the controls (text fields, combo boxes and such) for editing an entity instance.

When extending an EntityEditPanel you must implement the initializeUI() method, which initializes the edit panel UI. The EntityEditPanel class exposes methods for creating input components and binding them with the underlying EntityEditModel instance.

public class CustomerEditPanel extends EntityEditPanel {

  public CustomerEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    //the firstName field should receive the focus whenever the panel is initialized
    setInitialFocusAttribute(Customer.FIRST_NAME);

    createTextField(Customer.FIRST_NAME).setColumns(15);
    createTextField(Customer.LAST_NAME).setColumns(15);
    createTextField(Customer.EMAIL).setColumns(15);
    createCheckBox(Customer.IS_ACTIVE, IncludeCaption.NO);

    setLayout(new GridLayout(4,1));
    //the addInputPanel method creates and adds a panel containing the
    //component associated with the property as well as a JLabel with the
    //property caption as defined in the domain model
    addInputPanel(Customer.FIRST_NAME);
    addInputPanel(Customer.LAST_NAME);
    addInputPanel(Customer.EMAIL);
    addInputPanel(Customer.IS_ACTIVE);
  }
}
public class AddressEditPanel extends EntityEditPanel {

  public AddressEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(Address.STREET);

    createTextField(Address.STREET).setColumns(25);
    createTextField(Address.CITY).setColumns(25);
    createCheckBox(Address.VALID, IncludeCaption.NO);

    setLayout(new GridLayout(3, 1, 5, 5));
    addInputPanel(Address.STREET);
    addInputPanel(Address.CITY);
    addInputPanel(Address.VALID);
  }
}
public class CustomerAddressEditPanel extends EntityEditPanel {

  public CustomerAddressEditPanel(final SwingEntityEditModel editModel) {
    super(editModel);
  }

  @Override
  protected void initializeUI() {
    setInitialFocusAttribute(CustomerAddress.ADDRESS_FK);

    EntityComboBox addressComboBox =
            createForeignKeyComboBox(CustomerAddress.ADDRESS_FK);
    Components.setPreferredWidth(addressComboBox, 200);
    Action newAddressAction = new EntityPanelBuilder(Address.TYPE).setEditPanelClass(AddressEditPanel.class)
            .createEditPanelAction(addressComboBox);
    JPanel addressPanel = Components.createEastButtonPanel(addressComboBox, newAddressAction);

    setLayout(new BorderLayout(5, 5));

    add(createInputPanel(CustomerAddress.ADDRESS_FK, addressPanel));
  }
}
Input controls
Boolean
JCheckBox checkBox = createCheckBox(Demo.BOOLEAN, IncludeCaption.NO);

NullableCheckBox nullableCheckBox = createNullableCheckBox(Demo.BOOLEAN);

JComboBox<Item<Boolean>> comboBox = createBooleanComboBox(Demo.BOOLEAN);
Foreign key
EntityComboBox comboBox = createForeignKeyComboBox(Demo.FOREIGN_KEY);

EntityLookupField lookupField = createForeignKeyLookupField(Demo.FOREIGN_KEY);

//readOnly
JTextField textField = createForeignKeyField(Demo.FOREIGN_KEY);
Temporal
JTextField textField = createTextField(Demo.LOCAL_DATE);

TemporalInputPanel<LocalDate> inputPanel = createTemporalInputPanel(Demo.LOCAL_DATE);
Numerical
IntegerField integerField = (IntegerField) createTextField(Demo.INTEGER);

LongField longField = (LongField) createTextField(Demo.LONG);

DoubleField doubleField = (DoubleField) createTextField(Demo.DOUBLE);

BigDecimalField bigDecimalField = (BigDecimalField) createTextField(Demo.BIG_DECIMAL);
Text
JTextField textField = createTextField(Demo.TEXT);

JTextArea textArea = createTextArea(Demo.LONG_TEXT, 5, 20);

TextInputPanel inputPanel = createTextInputPanel(Demo.LONG_TEXT);

JFormattedTextField formattedField =
        createMaskedTextField(Demo.FORMATTED_TEXT, "###:###",
                ValueContainsLiterals.YES);
Value list
SteppedComboBox<Item<String>> comboBox = createValueListComboBox(Demo.VALUE_LIST);
Panels & labels
JLabel label = createLabel(Demo.TEXT);

JPanel inputPanel = createInputPanel(Demo.TEXT);
Custom actions

The action mechanism used throughout the Codion framework is based on the Control class and its subclasses and the ControlList class which, as the name suggests, represents a list of controls. There are two static utility classes for creating and presenting controls, Controls and ControlProvider respectively.

1.3.3. EntityTablePanel

The EntityTablePanel provides a table view of entities.

Adding a print action

The most common place to add a custom control is the table popup menu, i.e. an action for printing reports. The table popup menu is based on the ControlList returned by the getPopupControls() method in the EntityTablePanel class which in turn uses the ControlList returned by the getPrintControls() method in the same class for constructing the print popup submenu. So, to add a custom print action you override the getPrintControls() method and return a ControlSet containing the action.

public class CustomerTablePanel extends EntityTablePanel {

  public CustomerTablePanel(final SwingEntityTableModel tableModel) {
    super(tableModel);
  }

  @Override
  protected ControlList getPrintControls() {
    ControlList printControls = super.getPrintControls();
    //add a Control which calls the viewCustomerReport method in this class
    //enabled only when the selection is not empty
    printControls.add(Controls.control(this::viewCustomerReport, "Customer report",
            getTable().getModel().getSelectionModel().getSelectionNotEmptyObserver()));

    return printControls;
  }

  private void viewCustomerReport() throws Exception {
    List<Entity> selectedCustomers = getTable().getModel().getSelectionModel().getSelectedItems();
    if (selectedCustomers.isEmpty()) {
      return;
    }

    Collection<String> customerIds = Entities.getValues(Customer.ID, selectedCustomers);
    Map<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("CUSTOMER_IDS", customerIds);

    EntityReports.viewJdbcReport(this, Store.CUSTOMER_REPORT,
            reportParameters, JRViewer::new,  "Customer Report",
            getTableModel().getConnectionProvider());
  }
}

1.3.4. EntityPanelBuilder

The EntityPanelBuilder class provides lazy initialization for EntityPanels. Using the EntityPanelBuilder class instead of instantiating EntityPanels directly means the panels are not initialized until made visible.

  @Override
  protected void setupEntityPanelBuilders() {
    final SwingEntityModelBuilder countryModelBuilder = new SwingEntityModelBuilder(Country.TYPE);
    countryModelBuilder.setModelClass(CountryModel.class);
    EntityPanelBuilder countryPanelBuilder = new EntityPanelBuilder(countryModelBuilder) {
      @Override
      protected void configurePanel(final EntityPanel entityPanel) {
        entityPanel.setDetailSplitPanelResizeWeight(0.7);
      }
    };
    countryPanelBuilder.setEditPanelClass(CountryEditPanel.class);
    countryPanelBuilder.setTablePanelClass(CountryTablePanel.class);

    final SwingEntityModelBuilder countryCustomModelBuilder = new SwingEntityModelBuilder(Country.TYPE);
    countryCustomModelBuilder.setModelClass(CountryCustomModel.class);
    EntityPanelBuilder countryCustomPanelBuilder = new EntityPanelBuilder(countryCustomModelBuilder)
            .setPanelClass(CountryCustomPanel.class)
            .setCaption("Custom Country");

    EntityPanelBuilder cityPanelBuilder = new EntityPanelBuilder(City.TYPE);
    cityPanelBuilder.setEditPanelClass(CityEditPanel.class);

    EntityPanelBuilder countryLanguagePanelBuilder = new EntityPanelBuilder(CountryLanguage.TYPE);
    countryLanguagePanelBuilder.setEditPanelClass(CountryLanguageEditPanel.class);

    countryPanelBuilder.addDetailPanelBuilder(cityPanelBuilder);
    countryPanelBuilder.addDetailPanelBuilder(countryLanguagePanelBuilder);

    EntityPanelBuilder continentPanelBuilder = new EntityPanelBuilder(Continent.TYPE)
            .setPanelClass(ContinentPanel.class);
    EntityPanelBuilder lookupPanelBuilder = new EntityPanelBuilder(Lookup.TYPE)
            .setTablePanelClass(LookupTablePanel.class)
            .setRefreshOnInit(false);

    addEntityPanelBuilders(countryPanelBuilder, countryCustomPanelBuilder, continentPanelBuilder, lookupPanelBuilder);
  }

1.3.5. EntityApplicationPanel

public class StoreAppPanel extends EntityApplicationPanel<StoreAppModel> {

  @Override
  protected List<EntityPanel> initializeEntityPanels(final StoreAppModel applicationModel) {
    CustomerModel customerModel =
            (CustomerModel) applicationModel.getEntityModel(Customer.TYPE);
    //populate model with rows from database
    customerModel.refresh();

    EntityPanel customerPanel = new EntityPanel(customerModel,
            new CustomerEditPanel(customerModel.getEditModel()),
            new CustomerTablePanel(customerModel.getTableModel()));

    CustomerAddressModel customerAddressModel =
            (CustomerAddressModel) customerModel.getDetailModel(CustomerAddress.TYPE);
    EntityPanel customerAddressPanel = new EntityPanel(customerAddressModel,
            new CustomerAddressEditPanel(customerAddressModel.getEditModel()));

    customerPanel.addDetailPanel(customerAddressPanel);

    return Collections.singletonList(customerPanel);
  }

  @Override
  protected void setupEntityPanelBuilders() {
    addSupportPanelBuilder(new EntityPanelBuilder(Address.TYPE)
            .setEditPanelClass(AddressEditPanel.class));
  }

  @Override
  protected StoreAppModel initializeApplicationModel(EntityConnectionProvider connectionProvider) {
    return new StoreAppModel(connectionProvider);
  }

  public static void main(String[] args) {
    Locale.setDefault(new Locale("en", "EN"));
    EntityEditModel.POST_EDIT_EVENTS.set(true);
    EntityPanel.TOOLBAR_BUTTONS.set(true);
    EntityPanel.COMPACT_ENTITY_PANEL_LAYOUT.set(true);
    ReferentialIntegrityErrorHandling.REFERENTIAL_INTEGRITY_ERROR_HANDLING.set(ReferentialIntegrityErrorHandling.DEPENDENCIES);
    ColumnConditionModel.AUTOMATIC_WILDCARD.set(ColumnConditionModel.AutomaticWildcard.POSTFIX);
    ColumnConditionModel.CASE_SENSITIVE.set(false);
    EntityConnectionProvider.CLIENT_DOMAIN_CLASS.set("is.codion.framework.demos.manual.store.domain.Store");
    Report.REPORT_PATH.set("http://test.io");
    new StoreAppPanel().startApplication("Store", null, MaximizeFrame.NO,
            Windows.getScreenSizeRatio(0.6), Users.parseUser("scott:tiger"));
  }
}

1.3.6. Reporting with JasperReports

Codion uses a plugin oriented approach to report viewing, and provides an implementation for JasperReports and NextReports.

With the Codion JasperReports plugin you can either design your report based on a SQL query in which case you use the JRReport class, which facilitates the report being filled using the active database connection or you can design your report around the JRDataSource implementation provided by the JasperReportsDataSource class, which is constructed around an iterator.

JDBC Reports

Using a report based on a SQL query, JRReport and EntityReports is the simplest way of viewing a report using Codion, just add a method similar to the one below to a EntityTablePanel subclass. You can then create an action calling that method and put it in for example the table popup menu as described in the adding a print action section.

public class CustomerTablePanel extends EntityTablePanel {

  public CustomerTablePanel(final SwingEntityTableModel tableModel) {
    super(tableModel);
  }

  @Override
  protected ControlList getPrintControls() {
    ControlList printControls = super.getPrintControls();
    //add a Control which calls the viewCustomerReport method in this class
    //enabled only when the selection is not empty
    printControls.add(Controls.control(this::viewCustomerReport, "Customer report",
            getTable().getModel().getSelectionModel().getSelectionNotEmptyObserver()));

    return printControls;
  }

  private void viewCustomerReport() throws Exception {
    List<Entity> selectedCustomers = getTable().getModel().getSelectionModel().getSelectedItems();
    if (selectedCustomers.isEmpty()) {
      return;
    }

    Collection<String> customerIds = Entities.getValues(Customer.ID, selectedCustomers);
    Map<String, Object> reportParameters = new HashMap<>();
    reportParameters.put("CUSTOMER_IDS", customerIds);

    EntityReports.viewJdbcReport(this, Store.CUSTOMER_REPORT,
            reportParameters, JRViewer::new,  "Customer Report",
            getTableModel().getConnectionProvider());
  }
}
JRDataSource Reports

The JRDataSource implementation provided by the JasperReportsDataSource simply iterates through the iterator received via the constructor and retrieves the field values from the underlying entities. The easiest way to make this work is to design the report using field names that correspond to the attribute names, so using the Store domain example from above the fields in a report showing the available items would have to be named 'name', 'is_active', 'category_code' etc.

EntityConnection connection = connectionProvider.getConnection();

EntityDefinition customerDefinition =
        connection.getEntities().getDefinition(Customer.TYPE);

Iterator<Entity> customerIterator =
        connection.select(condition(Customer.TYPE).selectCondition()).iterator();

JasperReportsDataSource<Entity> dataSource =
        new JasperReportsDataSource<>(customerIterator,
                (entity, reportField) ->
                        entity.get(customerDefinition.getAttribute(reportField.getName())));

JRReport customerReport = fileReport("reports/customer.jasper");

JasperPrint jasperPrint = JasperReports.fillReport(customerReport, dataSource);
Examples

2. Common

2.1. Common Model

The model layer in a Codion application contains the business logic, most of the events and application state.

2.1.1. Common classes

Three common classes used throughout the framework are the Event, State and Value classes, their respective observers EventObserver and StateObserver and listeners EventListener and EventDataListener.

Event

The Event class is a simple synchronous event implementation used throughout the framework. Classes typically publish their events via public addListener methods. Events are triggered by calling the onEvent() method, with or without a data parameter.

Events are instantiated via the Events factory class.

To listen to Events you use the EventListener or EventDataListener interfaces.

Event<String> event = Events.event();

// an observer handles the listeners for an Event but can not trigger it
EventObserver<String> eventObserver = event.getObserver();

// add a listener notified each time the event occurs
eventObserver.addListener(() -> System.out.println("Event occurred"));

event.onEvent();//output: 'Event occurred'

// data can be propagated by adding a EventDataListener
eventObserver.addDataListener(data -> System.out.println("Event: " + data));

event.onEvent("info");//output: 'Event: info'

// Event extends EventObserver so listeneres can be added
// directly without referring to the EventObserver
event.addListener(() -> System.out.println("Event"));
State

The State class encapsulates a boolean state and provides read only access and a change observer via StateObserver.

States are instantiated via the States factory class.

// a boolean state, false by default
State state = States.state();

// an observer handles the listeners for a State but can not change it
StateObserver stateObserver = state.getObserver();
// a reversed observer is always available
StateObserver reversedObserver = state.getReversedObserver();

// add a listener notified each time the state changes
stateObserver.addListener(() -> System.out.println("State changed"));

state.set(true);//output: 'State changed'

stateObserver.addDataListener(value -> System.out.println("State: " + value));

state.set(false);//output: 'State: false'

// State extends StateObserver so listeners can be added
// directly without referring to the StateObserver
state.addListener(() -> System.out.println("State changed"));

Any Action object can be linked to a State object via the Components.linkToEnabledState method, where the action’s enabled status is updated according to the state.

State state = States.state();

Action action = new AbstractAction("action") {
  public void actionPerformed(ActionEvent e) {}
};

Components.linkToEnabledState(state, action);

System.out.println(action.isEnabled());// output: false

state.set(true);

System.out.println(action.isEnabled());// output: true
Value

The Value interface is a value wrapper with a change listener.

Values are instantiated via the Values factory class.

Value<Integer> value = Values.value();

value.addDataListener(System.out::println);

value.set(2);

IntegerField integerField = new IntegerField();

ComponentValue<Integer, IntegerField> fieldValue =
        NumericalValues.integerValue(integerField);

value.link(fieldValue);

integerField.setInteger(3);//linked value is now 3

2.2. Common UI

2.2.1. Input Controls

Here are the basics of linking input controls to model values.

Boolean
CheckBox
//non-nullable boolean value
boolean initialValue = true;
boolean nullValue = false;

Value<Boolean> booleanValue = Values.value(initialValue, nullValue);

JToggleButton.ToggleButtonModel buttonModel = new JToggleButton.ToggleButtonModel();

booleanValue.link(BooleanValues.booleanButtonModelValue(buttonModel));

JCheckBox checkBox = new JCheckBox();
checkBox.setModel(buttonModel);
NullableCheckBox
//nullable boolean value
Value<Boolean> booleanValue = Values.value();

NullableToggleButtonModel buttonModel = new NullableToggleButtonModel();

booleanValue.link(BooleanValues.booleanButtonModelValue(buttonModel));

NullableCheckBox checkBox = new NullableCheckBox(buttonModel);
ComboBox
Value<Boolean> booleanValue = Values.value();

JComboBox<Item<Boolean>> comboBox = new JComboBox<>(new BooleanComboBoxModel());

booleanValue.link(BooleanValues.booleanComboBoxValue(comboBox));
Text
TextField
Value<String> stringValue = Values.value();

JTextField textField = new JTextField();

stringValue.link(TextValues.textValue(textField));
TextArea
Value<String> stringValue = Values.value();

JTextArea textArea = new JTextArea();

stringValue.link(TextValues.textValue(textArea));
Numbers
Integer
Value<Integer> integerValue = Values.value();

IntegerField integerField = new IntegerField();

integerValue.link(NumericalValues.integerValue(integerField));
Long
Value<Long> longValue = Values.value();

LongField longField = new LongField();

longValue.link(NumericalValues.longValue(longField));
Double
Value<Double> doubleValue = Values.value();

DoubleField doubleField = new DoubleField();

doubleValue.link(NumericalValues.doubleValue(doubleField));
BigDecimal
Value<BigDecimal> bigDecimalValue = Values.value();

BigDecimalField bigDecimalField = new BigDecimalField();

bigDecimalValue.link(NumericalValues.bigDecimalValue(bigDecimalField));
Date & Time
LocalTime
Value<LocalTime> localTimeValue = Values.value();

JFormattedTextField textField = new JFormattedTextField();

localTimeValue.link(TemporalValues.localTimeValue(textField, "HH:mm:ss"));
LocalDate
Value<LocalDate> localDateValue = Values.value();

JFormattedTextField textField = new JFormattedTextField();

localDateValue.link(TemporalValues.localDateValue(textField, "dd-MM-yyyy"));
LocalDateTime
Value<LocalDateTime> localDateTimeValue = Values.value();

JFormattedTextField textField = new JFormattedTextField();

localDateTimeValue.link(TemporalValues.localDateTimeValue(textField, "dd-MM-yyyy HH:mm"));
Selection
ComboBox
Value<String> stringValue = Values.value();

JComboBox<String> comboBox = new JComboBox<>(new String[] {"one", "two", "three"});

stringValue.link(SelectedValues.selectedValue(comboBox));
Custom
TextField

In the following example we link a value based on a Person class to a component value displaying text fields for a first and last name.

class Person {
  final String firstName;
  final String lastName;

  public Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  @Override
  public String toString() {
    return lastName + ", " + firstName;
  }
}

class PersonPanel extends JPanel {
  final JTextField firstNameField = new JTextField();
  final JTextField lastNameField = new JTextField();

  public PersonPanel() {
    setLayout(new GridLayout(2, 2, 5, 5));
    add(new JLabel("First name"));
    add(new JLabel("Last name"));
    add(firstNameField);
    add(lastNameField);
  }
}

class PersonPanelValue extends AbstractComponentValue<Person, PersonPanel> {

  public PersonPanelValue(PersonPanel component) {
    super(component);
    //We must call notifyValueChange each time this value changes,
    //that is, when either the first or last name changes.
    component.firstNameField.getDocument()
            .addDocumentListener((DocumentAdapter) e -> notifyValueChange());
    component.lastNameField.getDocument()
            .addDocumentListener((DocumentAdapter) e -> notifyValueChange());
  }

  @Override
  protected Person getComponentValue(PersonPanel component) {
    return new Person(component.firstNameField.getText(), component.lastNameField.getText());
  }

  @Override
  protected void setComponentValue(PersonPanel component, Person value) {
    component.firstNameField.setText(value == null ? null : value.firstName);
    component.lastNameField.setText(value == null ? null : value.lastName);
  }
}

Value<Person> personValue = Values.value();

PersonPanel personPanel = new PersonPanel();

Value<Person> personPanelValue = new PersonPanelValue(personPanel);

personValue.link(personPanelValue);
Property

Below we link the 'horizontalAlignment' property of a IntegerField to the integer value displayed in the field.

IntegerField horizontalAlignmentField = new IntegerField(5);

Value<Integer> horizontalAlignmentValue =
        Values.propertyValue(horizontalAlignmentField, "horizontalAlignment",
                int.class, Components.propertyChangeObserver(horizontalAlignmentField, "horizontalAlignment"));

Value<Integer> fieldValue =
        NumericalValues.integerValue(horizontalAlignmentField, Nullable.NO);

horizontalAlignmentValue.link(fieldValue);

JPanel panel = new JPanel();
panel.add(horizontalAlignmentField);

fieldValue.addListener(panel::revalidate);

Dialogs.displayInDialog(null, panel, "test");

2.3. Common Utilities

Codion provides a few classes with miscellanous utility functions.