This tutorial assumes you have at least skimmed the Domain model part of the Codion manual.

Each section of the tutorial below is based on a single table and has the following subsections:

SQL

The underlying table DDL.

Domain

The domain model specification for an entity based on the table.

Model

The application model components associated with the entity (if any).

UI

The application UI components associated with the entity.

Domain model

The domain model is created by extending the DefaultDomain class and defining a DomainType constant identifying the domain model.

In the constructor we call methods each adding a single Entity definition to the domain model. The Domain subsections below continue the Petclinic class.

public final class Petclinic extends DomainModel {

  public static final DomainType DOMAIN = domainType("Petclinic");

  public Petclinic() {
    super(DOMAIN);
    add(vet(), specialty(), vetSpecialty(), petType(), owner(), pet(), visit());
  }
Display full Petclinic domain model class
/*
 * This file is part of Codion Petclinic Demo.
 *
 * Codion Petclinic Demo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Codion Petclinic Demo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Codion Petclinic Demo.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright (c) 2004 - 2024, Björn Darri Sigurðsson.
 */
package is.codion.demos.petclinic.domain;

import is.codion.demos.petclinic.domain.Petclinic.Owner.PhoneType;
import is.codion.framework.domain.DomainModel;
import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.EntityDefinition;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.OrderBy;
import is.codion.framework.domain.entity.StringFactory;
import is.codion.framework.domain.entity.attribute.Column;
import is.codion.framework.domain.entity.attribute.Column.Converter;
import is.codion.framework.domain.entity.attribute.ForeignKey;

import java.sql.Statement;
import java.time.LocalDate;

import static is.codion.framework.domain.DomainType.domainType;
import static is.codion.framework.domain.entity.KeyGenerator.identity;
import static is.codion.framework.domain.entity.OrderBy.ascending;

public final class Petclinic extends DomainModel {

  public static final DomainType DOMAIN = domainType("Petclinic");

  public Petclinic() {
    super(DOMAIN);
    add(vet(), specialty(), vetSpecialty(), petType(), owner(), pet(), visit());
  }

  public interface Vet {
    EntityType TYPE = DOMAIN.entityType("petclinic.vet");

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

  private EntityDefinition vet() {
    return Vet.TYPE.define(
                    Vet.ID.define()
                            .primaryKey(),
                    Vet.FIRST_NAME.define()
                            .column()
                            .caption("First name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false),
                    Vet.LAST_NAME.define()
                            .column()
                            .caption("Last name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false))
            .keyGenerator(identity())
            .caption("Vets")
            .stringFactory(StringFactory.builder()
                    .value(Vet.LAST_NAME)
                    .text(", ")
                    .value(Vet.FIRST_NAME)
                    .build())
            .orderBy(ascending(Vet.LAST_NAME, Vet.FIRST_NAME))
            .smallDataset(true)
            .build();
  }

  public interface Specialty {
    EntityType TYPE = DOMAIN.entityType("petclinic.specialty");

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

  private EntityDefinition specialty() {
    return Specialty.TYPE.define(
                    Specialty.ID.define()
                            .primaryKey(),
                    Specialty.NAME.define()
                            .column()
                            .caption("Name")
                            .searchable(true)
                            .maximumLength(80)
                            .nullable(false))
            .keyGenerator(identity())
            .caption("Specialties")
            .stringFactory(Specialty.NAME)
            .smallDataset(true)
            .build();
  }

  public interface VetSpecialty {
    EntityType TYPE = DOMAIN.entityType("petclinic.vet_specialty");

    Column<Integer> VET = TYPE.integerColumn("vet");
    Column<Integer> SPECIALTY = TYPE.integerColumn("specialty");

    ForeignKey VET_FK = TYPE.foreignKey("vet_fk", VET, Vet.ID);
    ForeignKey SPECIALTY_FK = TYPE.foreignKey("specialty_fk", SPECIALTY, Specialty.ID);
  }

  private EntityDefinition vetSpecialty() {
    return VetSpecialty.TYPE.define(
                    VetSpecialty.VET.define()
                            .primaryKey(0)
                            .updatable(true),
                    VetSpecialty.SPECIALTY.define()
                            .primaryKey(1)
                            .updatable(true),
                    VetSpecialty.VET_FK.define()
                            .foreignKey()
                            .caption("Vet"),
                    VetSpecialty.SPECIALTY_FK.define()
                            .foreignKey()
                            .caption("Specialty"))
            .caption("Vet specialties")
            .stringFactory(StringFactory.builder()
                    .value(VetSpecialty.VET_FK)
                    .text(" - ")
                    .value(VetSpecialty.SPECIALTY_FK)
                    .build())
            .build();
  }

  public interface PetType {
    EntityType TYPE = DOMAIN.entityType("petclinic.pet_type");

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

  private EntityDefinition petType() {
    return PetType.TYPE.define(
                    PetType.ID.define()
                            .primaryKey(),
                    PetType.NAME.define()
                            .column()
                            .caption("Name")
                            .searchable(true)
                            .maximumLength(80)
                            .nullable(false))
            .keyGenerator(identity())
            .caption("Pet types")
            .stringFactory(PetType.NAME)
            .orderBy(ascending(PetType.NAME))
            .smallDataset(true)
            .build();
  }

  public interface Owner {
    EntityType TYPE = DOMAIN.entityType("petclinic.owner");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> FIRST_NAME = TYPE.stringColumn("first_name");
    Column<String> LAST_NAME = TYPE.stringColumn("last_name");
    Column<String> ADDRESS = TYPE.stringColumn("address");
    Column<String> CITY = TYPE.stringColumn("city");
    Column<String> TELEPHONE = TYPE.stringColumn("telephone");
    Column<PhoneType> PHONE_TYPE = TYPE.column("phone_type", PhoneType.class);

    enum PhoneType {
      MOBILE, HOME, WORK
    }
  }

  private EntityDefinition owner() {
    return Owner.TYPE.define(
                    Owner.ID.define()
                            .primaryKey(),
                    Owner.FIRST_NAME.define()
                            .column()
                            .caption("First name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false),
                    Owner.LAST_NAME.define()
                            .column()
                            .caption("Last name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false),
                    Owner.ADDRESS.define()
                            .column()
                            .caption("Address")
                            .maximumLength(255),
                    Owner.CITY.define()
                            .column()
                            .caption("City")
                            .maximumLength(80),
                    Owner.TELEPHONE.define()
                            .column()
                            .caption("Telephone")
                            .maximumLength(20),
                    Owner.PHONE_TYPE.define()
                            .column()
                            .caption("Phone type")
                            .columnClass(String.class, new PhoneTypeConverter()))
            .keyGenerator(identity())
            .caption("Owners")
            .stringFactory(StringFactory.builder()
                    .value(Owner.LAST_NAME)
                    .text(", ")
                    .value(Owner.FIRST_NAME)
                    .build())
            .orderBy(ascending(Owner.LAST_NAME, Owner.FIRST_NAME))
            .build();
  }

  private static final class PhoneTypeConverter implements Converter<PhoneType, String> {

    @Override
    public String toColumnValue(PhoneType value, Statement statement) {
      return value.name();
    }

    @Override
    public PhoneType fromColumnValue(String columnValue) {
      return PhoneType.valueOf(columnValue);
    }
  }

  public interface Pet {
    EntityType TYPE = DOMAIN.entityType("petclinic.pet");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<LocalDate> BIRTH_DATE = TYPE.localDateColumn("birth_date");
    Column<Integer> PET_TYPE_ID = TYPE.integerColumn("type_id");
    Column<Integer> OWNER_ID = TYPE.integerColumn("owner_id");

    ForeignKey PET_TYPE_FK = TYPE.foreignKey("type_fk", PET_TYPE_ID, PetType.ID);
    ForeignKey OWNER_FK = TYPE.foreignKey("owner_fk", OWNER_ID, Owner.ID);
  }

  private EntityDefinition pet() {
    return Pet.TYPE.define(
                    Pet.ID.define()
                            .primaryKey(),
                    Pet.NAME.define()
                            .column()
                            .caption("Name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false),
                    Pet.BIRTH_DATE.define()
                            .column()
                            .caption("Birth date")
                            .nullable(false),
                    Pet.PET_TYPE_ID.define()
                            .column()
                            .nullable(false),
                    Pet.PET_TYPE_FK.define()
                            .foreignKey()
                            .caption("Pet type"),
                    Pet.OWNER_ID.define()
                            .column()
                            .nullable(false),
                    Pet.OWNER_FK.define()
                            .foreignKey()
                            .caption("Owner"))
            .keyGenerator(identity())
            .caption("Pets")
            .stringFactory(Pet.NAME)
            .orderBy(ascending(Pet.NAME))
            .build();
  }

  public interface Visit {
    EntityType TYPE = DOMAIN.entityType("petclinic.visit");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<Integer> PET_ID = TYPE.integerColumn("pet_id");
    Column<LocalDate> VISIT_DATE = TYPE.localDateColumn("visit_date");
    Column<String> DESCRIPTION = TYPE.stringColumn("description");

    ForeignKey PET_FK = TYPE.foreignKey("pet_fk", PET_ID, Pet.ID);
  }

  private EntityDefinition visit() {
    return Visit.TYPE.define(
                    Visit.ID.define()
                            .primaryKey(),
                    Visit.PET_ID.define()
                            .column()
                            .nullable(false),
                    Visit.PET_FK.define()
                            .foreignKey()
                            .caption("Pet"),
                    Visit.VISIT_DATE.define()
                            .column()
                            .caption("Date")
                            .nullable(false),
                    Visit.DESCRIPTION.define()
                            .column()
                            .caption("Description")
                            .maximumLength(255))
            .keyGenerator(identity())
            .orderBy(OrderBy.builder()
                    .ascending(Visit.PET_ID)
                    .descending(Visit.VISIT_DATE)
                    .build())
            .caption("Visits")
            .build();
  }
}

Owners

owners

Owner

SQL

CREATE TABLE petclinic.owner (
  id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  first_name VARCHAR(30) NOT NULL,
  last_name VARCHAR(30) NOT NULL,
  address VARCHAR(255),
  city VARCHAR(80),
  telephone VARCHAR(20),
  phone_type VARCHAR(20) NOT NULL
);

Domain

API

We start by creating an Owner interface and defining the domain API constants for the table and its columns, providing the table and column names as parameters.

We also define a PhoneType enum, which we use as a custom type for the phone_type column.

  public interface Owner {
    EntityType TYPE = DOMAIN.entityType("petclinic.owner");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> FIRST_NAME = TYPE.stringColumn("first_name");
    Column<String> LAST_NAME = TYPE.stringColumn("last_name");
    Column<String> ADDRESS = TYPE.stringColumn("address");
    Column<String> CITY = TYPE.stringColumn("city");
    Column<String> TELEPHONE = TYPE.stringColumn("telephone");
    Column<PhoneType> PHONE_TYPE = TYPE.column("phone_type", PhoneType.class);

    enum PhoneType {
      MOBILE, HOME, WORK
    }
  }
Implementation

We then define an Entity along with its columns based on the domain API constants, configuring each for persistance and presentation.

For the Owner.PHONE_TYPE column we call columnClass() where we specify the underlying column type and provide a Column.Converter implementation, for converting the enum to and from the underlying column value.

We use the StringFactory.builder() method to build a Function<Entity, String> instance which provides the toString() implementation for entities of this type.

We also specify a default OrderBy clause to use when selecting entities of this type.

  private EntityDefinition owner() {
    return Owner.TYPE.define(
                    Owner.ID.define()
                            .primaryKey(),
                    Owner.FIRST_NAME.define()
                            .column()
                            .caption("First name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false),
                    Owner.LAST_NAME.define()
                            .column()
                            .caption("Last name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false),
                    Owner.ADDRESS.define()
                            .column()
                            .caption("Address")
                            .maximumLength(255),
                    Owner.CITY.define()
                            .column()
                            .caption("City")
                            .maximumLength(80),
                    Owner.TELEPHONE.define()
                            .column()
                            .caption("Telephone")
                            .maximumLength(20),
                    Owner.PHONE_TYPE.define()
                            .column()
                            .caption("Phone type")
                            .columnClass(String.class, new PhoneTypeConverter()))
            .keyGenerator(identity())
            .caption("Owners")
            .stringFactory(StringFactory.builder()
                    .value(Owner.LAST_NAME)
                    .text(", ")
                    .value(Owner.FIRST_NAME)
                    .build())
            .orderBy(ascending(Owner.LAST_NAME, Owner.FIRST_NAME))
            .build();
  }

  private static final class PhoneTypeConverter implements Converter<PhoneType, String> {

    @Override
    public String toColumnValue(PhoneType value, Statement statement) {
      return value.name();
    }

    @Override
    public PhoneType fromColumnValue(String columnValue) {
      return PhoneType.valueOf(columnValue);
    }
  }

UI

We extend the EntityEditPanel class, with a constructor taking a SwingEntityEditModel parameter, which we simply propagate to the super constructor. We implement the initializeUI() method in which we create the components using the create…​() methods and add them to the panel using addInputPanel().

NOTE

The create…​() methods return a ComponentBuilder instance, providing a way to configure the input fields.

  • Set the initial focus attribute, which specifies which input field should receive the focus when the panel is cleared or initialized.

  • Create input fields for the columns.

  • Set a layout and add the input fields to the panel.

package is.codion.demos.petclinic.ui;

import is.codion.demos.petclinic.domain.Petclinic.Owner;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

public final class OwnerEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(Owner.FIRST_NAME);

    createTextField(Owner.FIRST_NAME);
    createTextField(Owner.LAST_NAME);
    createTextField(Owner.ADDRESS);
    createTextField(Owner.CITY);
    createTextField(Owner.TELEPHONE);
    createComboBox(Owner.PHONE_TYPE);

    setLayout(gridLayout(3, 2));

    addInputPanel(Owner.FIRST_NAME);
    addInputPanel(Owner.LAST_NAME);
    addInputPanel(Owner.ADDRESS);
    addInputPanel(Owner.CITY);
    addInputPanel(Owner.TELEPHONE);
    addInputPanel(Owner.PHONE_TYPE);
  }
}

Pet

SQL

CREATE TABLE petclinic.pet (
  id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name VARCHAR(30) NOT NULL,
  birth_date DATE,
  type_id INTEGER NOT NULL,
  owner_id INTEGER NOT NULL,
  CONSTRAINT fk_pet_owners FOREIGN KEY (owner_id)
      REFERENCES owner (id),
  CONSTRAINT fk_pet_pet_type FOREIGN KEY (type_id)
      REFERENCES pet_type (id)
);

Domain

API

For the Pet Entity we define a ForeignKey domain API constant for each foreign key, providing a name along with the columns comprising the underlying reference.

  public interface Pet {
    EntityType TYPE = DOMAIN.entityType("petclinic.pet");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> NAME = TYPE.stringColumn("name");
    Column<LocalDate> BIRTH_DATE = TYPE.localDateColumn("birth_date");
    Column<Integer> PET_TYPE_ID = TYPE.integerColumn("type_id");
    Column<Integer> OWNER_ID = TYPE.integerColumn("owner_id");

    ForeignKey PET_TYPE_FK = TYPE.foreignKey("type_fk", PET_TYPE_ID, PetType.ID);
    ForeignKey OWNER_FK = TYPE.foreignKey("owner_fk", OWNER_ID, Owner.ID);
  }
Implementation

We define the foreign keys the same way we define columns.

NOTE

The underlying foreign key reference columns do not have a caption specified, which means they will be hidden in table views by default.

  private EntityDefinition pet() {
    return Pet.TYPE.define(
                    Pet.ID.define()
                            .primaryKey(),
                    Pet.NAME.define()
                            .column()
                            .caption("Name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false),
                    Pet.BIRTH_DATE.define()
                            .column()
                            .caption("Birth date")
                            .nullable(false),
                    Pet.PET_TYPE_ID.define()
                            .column()
                            .nullable(false),
                    Pet.PET_TYPE_FK.define()
                            .foreignKey()
                            .caption("Pet type"),
                    Pet.OWNER_ID.define()
                            .column()
                            .nullable(false),
                    Pet.OWNER_FK.define()
                            .foreignKey()
                            .caption("Owner"))
            .keyGenerator(identity())
            .caption("Pets")
            .stringFactory(Pet.NAME)
            .orderBy(ascending(Pet.NAME))
            .build();
  }

UI

For the PetEditPanel we use createForeignKeyComboBox() to create a EntityComboBox for the Pet.OWNER_FK foreign key, which provides a ComboBox populated with the underlying entities.

For the Pet.PET_TYPE_FK foreign key we use createForeignKeyComboBoxPanel() which creates a EntityComboBox on a panel, which can also include buttons for adding a new item or editing the selected one. In this case we provide a button for adding a new PetType by calling add(true). The createForeignKeyComboBoxPanel() method takes a Supplier<EntityEditPanel> parameter, responsible for providing the EntityEditPanel instance to display when adding or editing items.

The createTemporalFieldPanel() method creates a temporal field for editing dates, on a panel which includes a button for displaying a calendar.

package is.codion.demos.petclinic.ui;

import is.codion.demos.petclinic.domain.Petclinic.Pet;
import is.codion.demos.petclinic.domain.Petclinic.PetType;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

public final class PetEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(Pet.NAME);

    createForeignKeyComboBox(Pet.OWNER_FK);
    createTextField(Pet.NAME);
    createForeignKeyComboBoxPanel(Pet.PET_TYPE_FK, this::createPetTypeEditPanel)
            .includeAddButton(true);
    createTemporalFieldPanel(Pet.BIRTH_DATE);

    setLayout(gridLayout(2, 2));

    addInputPanel(Pet.OWNER_FK);
    addInputPanel(Pet.NAME);
    addInputPanel(Pet.PET_TYPE_FK);
    addInputPanel(Pet.BIRTH_DATE);
  }

  private PetTypeEditPanel createPetTypeEditPanel() {
    return new PetTypeEditPanel(new SwingEntityEditModel(PetType.TYPE, editModel().connectionProvider()));
  }
}

Visit

SQL

CREATE TABLE petclinic.visit (
  id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  pet_id INTEGER NOT NULL,
  visit_date DATE NOT NULL,
  description VARCHAR(255),
  CONSTRAINT fk_visit_pets FOREIGN KEY (pet_id)
      REFERENCES pet (id)
);

Domain

API
  public interface Visit {
    EntityType TYPE = DOMAIN.entityType("petclinic.visit");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<Integer> PET_ID = TYPE.integerColumn("pet_id");
    Column<LocalDate> VISIT_DATE = TYPE.localDateColumn("visit_date");
    Column<String> DESCRIPTION = TYPE.stringColumn("description");

    ForeignKey PET_FK = TYPE.foreignKey("pet_fk", PET_ID, Pet.ID);
  }
Implementation

Here we create a OrderBy instance using a builder instead of a factory method.

  private EntityDefinition visit() {
    return Visit.TYPE.define(
                    Visit.ID.define()
                            .primaryKey(),
                    Visit.PET_ID.define()
                            .column()
                            .nullable(false),
                    Visit.PET_FK.define()
                            .foreignKey()
                            .caption("Pet"),
                    Visit.VISIT_DATE.define()
                            .column()
                            .caption("Date")
                            .nullable(false),
                    Visit.DESCRIPTION.define()
                            .column()
                            .caption("Description")
                            .maximumLength(255))
            .keyGenerator(identity())
            .orderBy(OrderBy.builder()
                    .ascending(Visit.PET_ID)
                    .descending(Visit.VISIT_DATE)
                    .build())
            .caption("Visits")
            .build();
  }

UI

When using a JTextArea requiring a JScrollPane we supply the scroll pane containing the text area, retrived via component(Attribute), as a parameter when calling addInputPanel().

package is.codion.demos.petclinic.ui;

import is.codion.demos.petclinic.domain.Petclinic.Visit;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import javax.swing.JPanel;
import javax.swing.JScrollPane;
import java.awt.BorderLayout;

import static is.codion.swing.common.ui.component.Components.gridLayoutPanel;
import static is.codion.swing.common.ui.layout.Layouts.borderLayout;

public final class VisitEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(Visit.PET_FK);

    createForeignKeyComboBox(Visit.PET_FK);
    createTemporalFieldPanel(Visit.VISIT_DATE);
    createTextArea(Visit.DESCRIPTION)
            .rowsColumns(4, 20);

    JPanel northPanel = gridLayoutPanel(1, 2)
            .add(createInputPanel(Visit.PET_FK))
            .add(createInputPanel(Visit.VISIT_DATE))
            .build();

    setLayout(borderLayout());
    add(northPanel, BorderLayout.NORTH);
    addInputPanel(Visit.DESCRIPTION, new JScrollPane(component(Visit.DESCRIPTION).get()), BorderLayout.CENTER);
  }
}

Support tables

Pet Type

pet types

SQL

CREATE TABLE petclinic.pet_type (
  id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name VARCHAR(80) NOT NULL,
  CONSTRAINT pet_type_uk UNIQUE (name)
);

Domain

API

  public interface PetType {
    EntityType TYPE = DOMAIN.entityType("petclinic.pet_type");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> NAME = TYPE.stringColumn("name");
  }
Implementation
  private EntityDefinition petType() {
    return PetType.TYPE.define(
                    PetType.ID.define()
                            .primaryKey(),
                    PetType.NAME.define()
                            .column()
                            .caption("Name")
                            .searchable(true)
                            .maximumLength(80)
                            .nullable(false))
            .keyGenerator(identity())
            .caption("Pet types")
            .stringFactory(PetType.NAME)
            .orderBy(ascending(PetType.NAME))
            .smallDataset(true)
            .build();
  }

UI

package is.codion.demos.petclinic.ui;

import is.codion.demos.petclinic.domain.Petclinic.PetType;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

public final class PetTypeEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(PetType.NAME);

    createTextField(PetType.NAME);

    setLayout(gridLayout(1, 1));

    addInputPanel(PetType.NAME);
  }
}

Specialty

specialties

SQL

CREATE TABLE petclinic.specialty (
  id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name VARCHAR(80) NOT NULL
);

Domain

API
  public interface Specialty {
    EntityType TYPE = DOMAIN.entityType("petclinic.specialty");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> NAME = TYPE.stringColumn("name");
  }
Implementation
  private EntityDefinition specialty() {
    return Specialty.TYPE.define(
                    Specialty.ID.define()
                            .primaryKey(),
                    Specialty.NAME.define()
                            .column()
                            .caption("Name")
                            .searchable(true)
                            .maximumLength(80)
                            .nullable(false))
            .keyGenerator(identity())
            .caption("Specialties")
            .stringFactory(Specialty.NAME)
            .smallDataset(true)
            .build();
  }

UI

package is.codion.demos.petclinic.ui;

import is.codion.demos.petclinic.domain.Petclinic.Specialty;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

public final class SpecialtyEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(Specialty.NAME);

    createTextField(Specialty.NAME);

    setLayout(gridLayout(1, 1));

    addInputPanel(Specialty.NAME);
  }
}

Vet

vets

SQL

CREATE TABLE petclinic.vet (
  id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  first_name VARCHAR(30) NOT NULL,
  last_name VARCHAR(30) NOT NULL
);

Domain

API
  public interface Vet {
    EntityType TYPE = DOMAIN.entityType("petclinic.vet");

    Column<Integer> ID = TYPE.integerColumn("id");
    Column<String> FIRST_NAME = TYPE.stringColumn("first_name");
    Column<String> LAST_NAME = TYPE.stringColumn("last_name");
  }
Implementation
  private EntityDefinition vet() {
    return Vet.TYPE.define(
                    Vet.ID.define()
                            .primaryKey(),
                    Vet.FIRST_NAME.define()
                            .column()
                            .caption("First name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false),
                    Vet.LAST_NAME.define()
                            .column()
                            .caption("Last name")
                            .searchable(true)
                            .maximumLength(30)
                            .nullable(false))
            .keyGenerator(identity())
            .caption("Vets")
            .stringFactory(StringFactory.builder()
                    .value(Vet.LAST_NAME)
                    .text(", ")
                    .value(Vet.FIRST_NAME)
                    .build())
            .orderBy(ascending(Vet.LAST_NAME, Vet.FIRST_NAME))
            .smallDataset(true)
            .build();
  }

UI

package is.codion.demos.petclinic.ui;

import is.codion.demos.petclinic.domain.Petclinic.Vet;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

public final class VetEditPanel extends EntityEditPanel {

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

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(Vet.FIRST_NAME);

    createTextField(Vet.FIRST_NAME);
    createTextField(Vet.LAST_NAME);

    setLayout(gridLayout(2, 1));

    addInputPanel(Vet.FIRST_NAME);
    addInputPanel(Vet.LAST_NAME);
  }
}

Vet Specialty

SQL

CREATE TABLE petclinic.vet_specialty (
  vet INTEGER NOT NULL,
  specialty INTEGER NOT NULL,
  CONSTRAINT fk_vet_specialty_vet FOREIGN KEY (vet)
      REFERENCES petclinic.vet (id),
  CONSTRAINT fk_vet_specialty_specialty FOREIGN KEY (specialty)
      REFERENCES petclinic.specialty (id)
);

Domain

API
  public interface VetSpecialty {
    EntityType TYPE = DOMAIN.entityType("petclinic.vet_specialty");

    Column<Integer> VET = TYPE.integerColumn("vet");
    Column<Integer> SPECIALTY = TYPE.integerColumn("specialty");

    ForeignKey VET_FK = TYPE.foreignKey("vet_fk", VET, Vet.ID);
    ForeignKey SPECIALTY_FK = TYPE.foreignKey("specialty_fk", SPECIALTY, Specialty.ID);
  }
Implementation
  private EntityDefinition vetSpecialty() {
    return VetSpecialty.TYPE.define(
                    VetSpecialty.VET.define()
                            .primaryKey(0)
                            .updatable(true),
                    VetSpecialty.SPECIALTY.define()
                            .primaryKey(1)
                            .updatable(true),
                    VetSpecialty.VET_FK.define()
                            .foreignKey()
                            .caption("Vet"),
                    VetSpecialty.SPECIALTY_FK.define()
                            .foreignKey()
                            .caption("Specialty"))
            .caption("Vet specialties")
            .stringFactory(StringFactory.builder()
                    .value(VetSpecialty.VET_FK)
                    .text(" - ")
                    .value(VetSpecialty.SPECIALTY_FK)
                    .build())
            .build();
  }

Model

Here we extend SwingEntityEditModel in order to provide validation for a Vet/Specialty combination. And yes, this should obviously be done with a unique key in the underlying table, but let’s assume we can’t do that for some reason.

The validate() method is called before inserting or updating the given entity instance.

In the constructor we initialize the ComboBox models used, creating and populating them, in order to prevent it from happening automatically when the UI is initialized on the EDT.

We also specify that the VetSpecialty.VET_FK and VetSpecialty.SPECIALTY_FK values should not persist when the edit model is cleared.

package is.codion.demos.petclinic.model;

import is.codion.demos.petclinic.domain.Petclinic.VetSpecialty;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.domain.entity.DefaultEntityValidator;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.exception.ValidationException;
import is.codion.swing.framework.model.SwingEntityEditModel;

import static is.codion.framework.db.EntityConnection.Count.where;
import static is.codion.framework.domain.entity.condition.Condition.and;

public final class VetSpecialtyEditModel extends SwingEntityEditModel {

  public VetSpecialtyEditModel(EntityConnectionProvider connectionProvider) {
    super(VetSpecialty.TYPE, connectionProvider);
    initializeComboBoxModels(VetSpecialty.VET_FK, VetSpecialty.SPECIALTY_FK);
    value(VetSpecialty.VET_FK).persist().set(false);
    value(VetSpecialty.SPECIALTY_FK).persist().set(false);
    entity().validator().set(new VetSpecialtyValidator());
  }

  private final class VetSpecialtyValidator extends DefaultEntityValidator {

    @Override
    public void validate(Entity entity) {
      super.validate(entity);
      int rowCount = connection().count(where(and(
              VetSpecialty.SPECIALTY.equalTo(entity.get(VetSpecialty.SPECIALTY)),
              VetSpecialty.VET.equalTo(entity.get(VetSpecialty.VET)))));
      if (rowCount > 0) {
        throw new ValidationException(VetSpecialty.SPECIALTY_FK,
                entity.get(VetSpecialty.SPECIALTY_FK), "Vet/specialty combination already exists");
      }
    }
  }
}

UI

package is.codion.demos.petclinic.ui;

import is.codion.demos.petclinic.domain.Petclinic.Specialty;
import is.codion.demos.petclinic.domain.Petclinic.VetSpecialty;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.ui.EntityEditPanel;

import static is.codion.swing.common.ui.layout.Layouts.gridLayout;

public final class VetSpecialtyEditPanel extends EntityEditPanel {

  public VetSpecialtyEditPanel(SwingEntityEditModel editModel) {
    super(editModel);
    defaults().foreignKeyComboBoxPreferredWidth().set(200);
  }

  @Override
  protected void initializeUI() {
    initialFocusAttribute().set(VetSpecialty.VET_FK);

    createForeignKeyComboBox(VetSpecialty.VET_FK);
    createForeignKeyComboBoxPanel(VetSpecialty.SPECIALTY_FK, this::createSpecialtyEditPanel)
            .includeAddButton(true);

    setLayout(gridLayout(2, 1));

    addInputPanel(VetSpecialty.VET_FK);
    addInputPanel(VetSpecialty.SPECIALTY_FK);
  }

  private SpecialtyEditPanel createSpecialtyEditPanel() {
    return new SpecialtyEditPanel(new SwingEntityEditModel(Specialty.TYPE, editModel().connectionProvider()));
  }
}

PetclinicAppModel

The application model holds the SwingEntityModel instances used by the application, here we create a setupEntityModels() method for creating and configuring the application model layer. Note that we initialize ComboBox models and refresh the Owners table model, in order for that not to happen on the EDT when the UI is created.

package is.codion.demos.petclinic.model;

import is.codion.common.version.Version;
import is.codion.demos.petclinic.domain.Petclinic.Owner;
import is.codion.demos.petclinic.domain.Petclinic.Pet;
import is.codion.demos.petclinic.domain.Petclinic.Visit;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.swing.framework.model.SwingEntityApplicationModel;
import is.codion.swing.framework.model.SwingEntityModel;

public final class PetclinicAppModel extends SwingEntityApplicationModel {

  public static final Version VERSION = Version.parse(PetclinicAppModel.class, "/version.properties");

  public PetclinicAppModel(EntityConnectionProvider connectionProvider) {
    super(connectionProvider, VERSION);
    setupEntityModels(connectionProvider);
  }

  private void setupEntityModels(EntityConnectionProvider connectionProvider) {
    SwingEntityModel ownersModel = new SwingEntityModel(Owner.TYPE, connectionProvider);
    SwingEntityModel petsModel = new SwingEntityModel(Pet.TYPE, connectionProvider);
    petsModel.editModel().initializeComboBoxModels(Pet.OWNER_FK, Pet.PET_TYPE_FK);
    SwingEntityModel visitModel = new SwingEntityModel(Visit.TYPE, connectionProvider);
    visitModel.editModel().initializeComboBoxModels(Visit.PET_FK);

    ownersModel.addDetailModel(petsModel);
    petsModel.addDetailModel(visitModel);

    ownersModel.tableModel().refresh();

    addEntityModel(ownersModel);
  }
}

PetclinicAppPanel

The application panel holds the EntityPanel instances used by the application, which are created in the createEntityPanels() method. These EntityPanels are based on the SwingEntityModels we get from the application model.

We also override createSupportPanelBuilders(), where we create EntityPanel.Builder instances on which to base the Support tables main menu.

package is.codion.demos.petclinic.ui;

import is.codion.common.model.CancelException;
import is.codion.common.user.User;
import is.codion.demos.petclinic.domain.Petclinic;
import is.codion.demos.petclinic.domain.Petclinic.Owner;
import is.codion.demos.petclinic.domain.Petclinic.Pet;
import is.codion.demos.petclinic.domain.Petclinic.PetType;
import is.codion.demos.petclinic.domain.Petclinic.Specialty;
import is.codion.demos.petclinic.domain.Petclinic.Vet;
import is.codion.demos.petclinic.domain.Petclinic.VetSpecialty;
import is.codion.demos.petclinic.domain.Petclinic.Visit;
import is.codion.demos.petclinic.model.PetclinicAppModel;
import is.codion.demos.petclinic.model.VetSpecialtyEditModel;
import is.codion.swing.common.ui.laf.LookAndFeelProvider;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityApplicationPanel;
import is.codion.swing.framework.ui.EntityPanel;
import is.codion.swing.framework.ui.ReferentialIntegrityErrorHandling;

import com.formdev.flatlaf.intellijthemes.FlatAllIJThemes;

import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public final class PetclinicAppPanel extends EntityApplicationPanel<PetclinicAppModel> {

  private static final String DEFAULT_FLAT_LOOK_AND_FEEL = "com.formdev.flatlaf.intellijthemes.FlatArcIJTheme";

  public PetclinicAppPanel(PetclinicAppModel appModel) {
    super(appModel);
  }

  @Override
  protected List<EntityPanel> createEntityPanels() {
    SwingEntityModel ownersModel = applicationModel().entityModel(Owner.TYPE);
    SwingEntityModel petsModel = ownersModel.detailModel(Pet.TYPE);
    SwingEntityModel visitsModel = petsModel.detailModel(Visit.TYPE);

    EntityPanel ownersPanel = new EntityPanel(ownersModel,
            new OwnerEditPanel(ownersModel.editModel()));
    EntityPanel petsPanel = new EntityPanel(petsModel,
            new PetEditPanel(petsModel.editModel()));
    EntityPanel visitsPanel = new EntityPanel(visitsModel,
            new VisitEditPanel(visitsModel.editModel()));

    ownersPanel.addDetailPanel(petsPanel);
    petsPanel.addDetailPanel(visitsPanel);

    return List.of(ownersPanel);
  }

  @Override
  protected List<EntityPanel.Builder> createSupportEntityPanelBuilders() {
    EntityPanel.Builder petTypePanelBuilder =
            EntityPanel.builder(PetType.TYPE)
                    .editPanel(PetTypeEditPanel.class)
                    .caption("Pet types");
    EntityPanel.Builder specialtyPanelBuilder =
            EntityPanel.builder(Specialty.TYPE)
                    .editPanel(SpecialtyEditPanel.class)
                    .caption("Specialties");

    SwingEntityModel.Builder vetSpecialtyModelBuilder =
            SwingEntityModel.builder(VetSpecialty.TYPE)
                    .editModel(VetSpecialtyEditModel.class);
    SwingEntityModel.Builder vetModelBuilder =
            SwingEntityModel.builder(Vet.TYPE)
                    .detailModel(vetSpecialtyModelBuilder);

    EntityPanel.Builder vetSpecialtyPanelBuilder =
            EntityPanel.builder(vetSpecialtyModelBuilder)
                    .editPanel(VetSpecialtyEditPanel.class)
                    .caption("Specialty");
    EntityPanel.Builder vetPanelBuilder =
            EntityPanel.builder(vetModelBuilder)
                    .editPanel(VetEditPanel.class)
                    .detailPanel(vetSpecialtyPanelBuilder)
                    .caption("Vets");

    return List.of(petTypePanelBuilder, specialtyPanelBuilder, vetPanelBuilder);
  }

  public static void main(String[] args) throws CancelException {
    Locale.setDefault(Locale.of("en", "EN"));
    Arrays.stream(FlatAllIJThemes.INFOS)
            .forEach(LookAndFeelProvider::addLookAndFeel);
    ReferentialIntegrityErrorHandling.REFERENTIAL_INTEGRITY_ERROR_HANDLING
            .set(ReferentialIntegrityErrorHandling.DISPLAY_DEPENDENCIES);
    EntityApplicationPanel.builder(PetclinicAppModel.class, PetclinicAppPanel.class)
            .applicationName("Petclinic")
            .applicationVersion(PetclinicAppModel.VERSION)
            .domainType(Petclinic.DOMAIN)
            .displayStartupDialog(false)
            .defaultLookAndFeelClassName(DEFAULT_FLAT_LOOK_AND_FEEL)
            .defaultLoginUser(User.parse("scott:tiger"))
            .start();
  }
}

Domain unit test

package is.codion.demos.petclinic.domain;

import is.codion.demos.petclinic.domain.Petclinic.Owner;
import is.codion.demos.petclinic.domain.Petclinic.Pet;
import is.codion.demos.petclinic.domain.Petclinic.PetType;
import is.codion.demos.petclinic.domain.Petclinic.Specialty;
import is.codion.demos.petclinic.domain.Petclinic.Vet;
import is.codion.demos.petclinic.domain.Petclinic.VetSpecialty;
import is.codion.demos.petclinic.domain.Petclinic.Visit;
import is.codion.framework.domain.test.DomainTest;

import org.junit.jupiter.api.Test;

public final class PetclinicTest extends DomainTest {

  public PetclinicTest() {
    super(new Petclinic());
  }

  @Test
  void vet() {
    test(Vet.TYPE);
  }

  @Test
  void specialty() {
    test(Specialty.TYPE);
  }

  @Test
  void vetSpecialty() {
    test(VetSpecialty.TYPE);
  }

  @Test
  void petType() {
    test(PetType.TYPE);
  }

  @Test
  void owner() {
    test(Owner.TYPE);
  }

  @Test
  void pet() {
    test(Pet.TYPE);
  }

  @Test
  void visit() {
    test(Visit.TYPE);
  }
}

Module Info

/**
 * Petclinic demo.
 */
module is.codion.demos.petclinic {
  requires is.codion.swing.framework.ui;
  requires com.formdev.flatlaf.intellijthemes;

  exports is.codion.demos.petclinic.model
          to is.codion.swing.framework.model, is.codion.swing.framework.ui;
  exports is.codion.demos.petclinic.ui
          to is.codion.swing.framework.ui;

  provides is.codion.framework.domain.Domain
          with is.codion.demos.petclinic.domain.Petclinic;
}