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.framework.demos.petclinic.domain;
import is.codion.framework.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
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.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);
}
}
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.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);
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.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
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.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
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.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
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.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);
}
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.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.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) throws ValidationException {
super.validate(entity);
try {
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");
}
}
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)
.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.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.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.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::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.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.test.DomainTest;
import org.junit.jupiter.api.Test;
public final class PetclinicTest extends DomainTest {
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;
}