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 adding a single Entity definition to the domain model. The Domain subsections below continue the Petclinic class.

public final class Petclinic extends DefaultDomain {

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

  public Petclinic() {
    super(DOMAIN);
    vet();
    specialty();
    vetSpecialty();
    petType();
    owner();
    pet();
    visit();
  }
Display full Petclinic 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 - 2023, Björn Darri Sigurðsson.
 */
package is.codion.framework.demos.petclinic.domain;

import is.codion.framework.demos.petclinic.domain.Petclinic.Owner.PhoneType;
import is.codion.framework.domain.DefaultDomain;
import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.Entity;
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.io.Serializable;
import java.sql.Statement;
import java.time.LocalDate;
import java.util.function.Predicate;

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 DefaultDomain {

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

  public Petclinic() {
    super(DOMAIN);
    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 void vet() {
    add(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));
  }

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

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

  private void specialty() {
    add(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));
  }

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

    final class Exists implements Predicate<Entity>, Serializable {

      private static final long serialVersionUID = 1;

      @Override
      public boolean test(Entity entity) {
        return entity.originalPrimaryKey().isNotNull();
      }
    }
  }

  private void vetSpecialty() {
    add(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())
            .exists(new VetSpecialty.Exists()));
  }

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

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

  private void petType() {
    add(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));
  }

  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 void owner() {
    add(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)));
  }

  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 void pet() {
    add(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)));
  }

  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 void visit() {
    add(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"));
  }
}

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 defining an Owner interface and defining constants for the table and its columns.

The EntityType identifies this entity and the Attributes identify the columns and their types. We define a PhoneType enum, which we use as a custom type for the attribute based on 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 based on the attributes, wrapping each attribute in a property, allowing us to configure presentation and persistance.

  private void owner() {
    add(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)));
  }

  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 and implement the initializeUI method to provide a user interface for editing Owner instances.

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

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

package is.codion.framework.demos.petclinic.ui;

import is.codion.framework.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);
  }
}
NOTE

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

Pet

SQL

CREATE TABLE petclinic.pet (
  id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name VARCHAR(30),
  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
  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
  private void pet() {
    add(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)));
  }

UI

package is.codion.framework.demos.petclinic.ui;

import is.codion.framework.demos.petclinic.domain.Petclinic.Pet;
import is.codion.framework.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);
    createTemporalFieldPanel(Pet.BIRTH_DATE);
    createForeignKeyComboBoxPanel(Pet.PET_TYPE_FK, this::createPetTypeEditPanel)
            .add(true);

    setLayout(gridLayout(2, 2));

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

  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
  private void visit() {
    add(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"));
  }

UI

package is.codion.framework.demos.petclinic.ui;

import is.codion.framework.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
);

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 void petType() {
    add(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));
  }

UI

package is.codion.framework.demos.petclinic.ui;

import is.codion.framework.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)
);

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 void specialty() {
    add(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));
  }

UI

package is.codion.framework.demos.petclinic.ui;

import is.codion.framework.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),
  last_name VARCHAR(30)
);

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 void vet() {
    add(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));
  }

UI

package is.codion.framework.demos.petclinic.ui;

import is.codion.framework.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);

    final class Exists implements Predicate<Entity>, Serializable {

      private static final long serialVersionUID = 1;

      @Override
      public boolean test(Entity entity) {
        return entity.originalPrimaryKey().isNotNull();
      }
    }
  }
Implementation
  private void vetSpecialty() {
    add(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())
            .exists(new VetSpecialty.Exists()));
  }

Model

package is.codion.framework.demos.petclinic.model;

import is.codion.common.db.exception.DatabaseException;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.petclinic.domain.Petclinic.VetSpecialty;
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);
    persist(VetSpecialty.VET_FK).set(false);
    persist(VetSpecialty.SPECIALTY_FK).set(false);
  }

  @Override
  public void validate(Entity entity) throws ValidationException {
    super.validate(entity);
    try {
      int rowCount = connectionProvider().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");
      }
    }
    catch (DatabaseException e) {
      throw new RuntimeException(e);
    }
  }
}

UI

package is.codion.framework.demos.petclinic.ui;

import is.codion.framework.demos.petclinic.domain.Petclinic.Specialty;
import is.codion.framework.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)
            .add(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

package is.codion.framework.demos.petclinic.model;

import is.codion.common.version.Version;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.demos.petclinic.domain.Petclinic.Owner;
import is.codion.framework.demos.petclinic.domain.Petclinic.Pet;
import is.codion.framework.demos.petclinic.domain.Petclinic.Visit;
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.parsePropertiesFile(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

package is.codion.framework.demos.petclinic.ui;

import is.codion.common.model.CancelException;
import is.codion.common.user.User;
import is.codion.framework.demos.petclinic.domain.Petclinic;
import is.codion.framework.demos.petclinic.domain.Petclinic.Owner;
import is.codion.framework.demos.petclinic.domain.Petclinic.Pet;
import is.codion.framework.demos.petclinic.domain.Petclinic.PetType;
import is.codion.framework.demos.petclinic.domain.Petclinic.Specialty;
import is.codion.framework.demos.petclinic.domain.Petclinic.Vet;
import is.codion.framework.demos.petclinic.domain.Petclinic.VetSpecialty;
import is.codion.framework.demos.petclinic.domain.Petclinic.Visit;
import is.codion.framework.demos.petclinic.model.PetclinicAppModel;
import is.codion.framework.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.materialthemeuilite.FlatMaterialDarkerIJTheme";

  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(new Locale.Builder()
            .setLanguage("EN")
            .setRegion("en")
            .build());
    Arrays.stream(FlatAllIJThemes.INFOS).forEach(LookAndFeelProvider::addLookAndFeelProvider);
    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.framework.demos.petclinic.domain;

import is.codion.common.db.exception.DatabaseException;
import is.codion.framework.demos.petclinic.domain.Petclinic.Owner;
import is.codion.framework.demos.petclinic.domain.Petclinic.Pet;
import is.codion.framework.demos.petclinic.domain.Petclinic.PetType;
import is.codion.framework.demos.petclinic.domain.Petclinic.Specialty;
import is.codion.framework.demos.petclinic.domain.Petclinic.Vet;
import is.codion.framework.demos.petclinic.domain.Petclinic.VetSpecialty;
import is.codion.framework.demos.petclinic.domain.Petclinic.Visit;
import is.codion.framework.domain.entity.test.EntityTestUnit;

import org.junit.jupiter.api.Test;

public final class PetclinicTest extends EntityTestUnit {

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

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

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

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

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

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

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

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

Module Info

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

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

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