1. Rich client
Codion is primarily a Swing based framework, the Swing client is mature and stable while the JavaFX client is quite rudimentary and still in the 'proof-of-concept' stage. The below artifacts pull in all required framework dependencies.
Client | Artifact |
---|---|
Swing |
is.codion.jdk8:codion-swing-framework.ui:0.17.16 |
JavaFX |
is.codion.jdk8:codion-javafx-framework:0.17.16 |
2. Database connectivity
A Codion client has three ways of connecting to a database, directly via a local JDBC connection or remotely via RMI or HTTP using the Codion remote server.
DB Connection | Artifact |
---|---|
Local |
is.codion.jdk8:codion-framework-db-local:0.17.16 |
RMI |
is.codion.jdk8:codion-framework-db-rmi:0.17.16 |
HTTP |
is.codion.jdk8:codion-framework-db-http:0.17.16 |
3. DBMS
When connecting to the database with a local JDBC connection the DBMS module for the underlying database must be on the classpath. Note that these artifacts do not depend on the JDBC drivers, so those must be added separately.
The most used and tested DBMS modules are:
-
Oracle
-
H2 Database
-
PostgreSQL
DBMS | Artifact |
---|---|
Db2 |
is.codion.jdk8:codion-dbms-db2database:0.17.16 |
Derby |
is.codion.jdk8:codion-dbms-derby:0.17.16 |
H2 Database |
is.codion.jdk8:codion-dbms-h2database:0.17.16 |
HSQL |
is.codion.jdk8:codion-dbms-hsql:0.17.16 |
MariaDB |
is.codion.jdk8:codion-dbms-mariadb:0.17.16 |
MySQL |
is.codion.jdk8:codion-dbms-mysql:0.17.16 |
Oracle |
is.codion.jdk8:codion-dbms-oracle:0.17.16 |
PostgreSQL |
is.codion.jdk8:codion-dbms-postgresql:0.17.16 |
SQLite |
is.codion.jdk8:codion-dbms-sqlite:0.17.16 |
SQL Server |
is.codion.jdk8:codion-dbms-sqlserver:0.17.16 |
4. Logging
Codion uses SLF4J throughout so all you need to do is add a SLF4J bridge for your logging framework of choice to the classpath. If you use Logback, Log4J or Java Util Logging you can use one of the logging-proxy plugins below which will pull in the required dependencies and provide a main-menu action in the client for setting the logging level.
Logging | Artifact |
---|---|
Logback |
is.codion.jdk8:codion-plugin-logback-proxy:0.17.16 |
Log4j |
is.codion.jdk8:codion-plugin-log4j-proxy:0.17.16 |
Java Util Logging |
is.codion.jdk8:codion-plugin-jul-proxy:0.17.16 |
5. Gradle
dependencies {
//Swing client UI module
implementation 'is.codion.jdk8:codion-swing-framework-ui:0.17.16'
//Local JDBC connection module
runtimeOnly 'is.codion.jdk8:codion-framework-db-local:0.17.16'
//H2 DBMS module
runtimeOnly 'is.codion.jdk8:codion-dbms-h2database:0.17.16'
//H2 JDBC driver
runtimeOnly 'com.h2database:h2:2.1.212'
//Logging with Logback
runtimeOnly 'is.codion.jdk8:codion-plugin-logback-proxy:0.17.16'
//Domain model unit testing module
testImplementation 'is.codion.jdk8:codion-framework-domain-test:0.17.16'
//JUnit 5
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
}
6. Code examples
There are at minimum two steps involved in creating a Codion application:
-
Defining a domain model based on the underlying tables.
-
Creating edit panels for tables requiring CRUD functionality.
6.1. Domain
The Codion framework is based around the Entity class, a map-like structure, representing a row in a table. The domain is modelled much like an E/R diagram, with a EntityType and EntityDefinition per table, an Attribute and a Property for each column and a ForeignKey and a ForeignKeyProperty for each foreign key relationship.
Module |
Artifact |
is.codion.framework.domain |
is.codion.jdk8:codion-framework-domain:0.17.16 |
6.1.1. Defining entities
The below examples are somewhat simplified, but functionally correct.
View example schema SQL
Note
|
Not all columns are used in the example code below. |
create user if not exists scott password 'tiger';
alter user scott admin true;
create schema store;
create table store.customer (
id varchar(36) not null,
first_name varchar(40) not null,
last_name varchar(40) not null,
email varchar(100),
is_active boolean default true not null,
constraint customer_pk primary key (id),
constraint customer_email_uk unique (email)
);
create table store.address (
id identity not null,
street varchar(120) not null,
city varchar(50) not null,
valid boolean default true not null,
constraint address_pk primary key (id),
constraint address_uk unique (street, city)
);
create table store.customer_address (
id identity not null,
customer_id varchar(36) not null,
address_id integer not null,
constraint customer_address_pk primary key (id),
constraint customer_address_uk unique (customer_id, address_id)
);
insert into store.customer(id, first_name, last_name, email)
values ('ff60ebc3-bae0-4c0f-b094-4129edd3665a', 'John', 'Doe', 'doe@doe.net');
insert into store.address(street, city)
values ('Elm Street 123', 'Syracuse');
insert into store.customer_address(customer_id, address_id)
values ('ff60ebc3-bae0-4c0f-b094-4129edd3665a', 1);
commit;
First we define a EntityType constant for the table and a typed Attribute constant for each column. We wrap these in an interface for a convenient namespace. We create a Property.Builder instance for each Attribute, which allow further configuration, such as specifying a caption and maximum length. These Property.Builder instances we supply as parameters to the EntityDefinition.definition() method, which returns a EntityDefinition.Builder instance, allowing further configuration of the entity. Finally, we add the definition builder to the domain model.
Note
|
In this example the constants contain the actual table and column names, but these can be specified via the builder if that is preferred (see manual). |
public interface Customer {
EntityType TYPE = DOMAIN.entityType("store.customer");
Attribute<String> ID = TYPE.stringAttribute("id");
Attribute<String> FIRST_NAME = TYPE.stringAttribute("first_name");
Attribute<String> LAST_NAME = TYPE.stringAttribute("last_name");
}
void customer() {
add(definition(
primaryKeyProperty(Customer.ID),
columnProperty(Customer.FIRST_NAME, "First name")
.nullable(false)
.maximumLength(40),
columnProperty(Customer.LAST_NAME, "Last name")
.nullable(false)
.maximumLength(40))
.keyGenerator(new CustomerKeyGenerator())
.stringFactory(StringFactory.builder()
.value(Customer.LAST_NAME)
.text(", ")
.value(Customer.FIRST_NAME)
.build()));
}
private static final class CustomerKeyGenerator implements KeyGenerator {
@Override
public void beforeInsert(Entity entity,
List<ColumnProperty<?>> primaryKeyProperties,
DatabaseConnection connection) throws SQLException {
entity.put(Customer.ID, randomUUID().toString());
}
}
Next we define an entity based on the table store.address.
public interface Address {
EntityType TYPE = DOMAIN.entityType("store.address");
Attribute<Integer> ID = TYPE.integerAttribute("id");
Attribute<String> STREET = TYPE.stringAttribute("street");
Attribute<String> CITY = TYPE.stringAttribute("city");
}
void address() {
add(definition(
primaryKeyProperty(Address.ID),
columnProperty(Address.STREET, "Street")
.nullable(false)
.maximumLength(120),
columnProperty(Address.CITY, "City")
.nullable(false)
.maximumLength(50))
.keyGenerator(automatic("store.address"))
.stringFactory(StringFactory.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build()));
}
And finally we define an entity based on the many-to-many relationship table store.customer_address.
public interface CustomerAddress {
EntityType TYPE = DOMAIN.entityType("store.customer_address");
Attribute<Integer> ID = TYPE.integerAttribute("id");
Attribute<String> CUSTOMER_ID = TYPE.stringAttribute("customer_id");
Attribute<Integer> ADDRESS_ID = TYPE.integerAttribute("address_id");
ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
ForeignKey ADDRESS_FK = TYPE.foreignKey("address_fk", ADDRESS_ID, Address.ID);
}
void customerAddress() {
add(definition(
primaryKeyProperty(CustomerAddress.ID),
columnProperty(CustomerAddress.CUSTOMER_ID)
.nullable(false),
foreignKeyProperty(CustomerAddress.CUSTOMER_FK, "Customer"),
columnProperty(CustomerAddress.ADDRESS_ID)
.nullable(false),
foreignKeyProperty(CustomerAddress.ADDRESS_FK, "Address"))
.keyGenerator(automatic("store.customer_address"))
.caption("Customer address"));
}
6.2. UI
The EntityPanel class provides a Swing UI for viewing and editing entities. It is composed of a EntityEditPanel and a EntityTablePanel. For each of these panel classes there is a corresponding model class; SwingEntityModel, which is composed of a SwingEntityEditModel and a SwingEntityTableModel. The only class you are required to extend is the EntityEditPanel, which provides the input controls for editing an entity. Below we demonstrate how to set up a simple master/detail panel.
Module |
Artifact |
is.codion.swing.framework.ui |
is.codion.jdk8:codion-swing-framework-ui:0.17.16 |
6.2.1. Master
Here we extend a EntityEditPanel to provide the UI for editing a customer and use that edit panel class when we assemble the EntityPanel. We use a default SwingEntityModel implementation, which internally, creates a default SwingEntityEditModel and SwingEntityTableModel.
class CustomerEditPanel extends EntityEditPanel {
public CustomerEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
setInitialFocusAttribute(Customer.FIRST_NAME);
createTextField(Customer.FIRST_NAME);
createTextField(Customer.LAST_NAME);
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
}
}
EntityConnectionProvider connectionProvider =
LocalEntityConnectionProvider.builder()
.domainClassName(Store.class.getName())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModel customerModel = new SwingEntityModel(Customer.TYPE, connectionProvider);
EntityPanel customerPanel = new EntityPanel(customerModel,
new CustomerEditPanel(customerModel.editModel()));
6.2.2. Detail
Here we create a panel for viewing and editing customer addresses, much like the one above. We start by creating a default SwingEntityModel instance, which we add as a detail model on the customer model. Finally, we create a EntityPanel for the customer address and add that as a detail panel on the customer panel.
class CustomerAddressEditPanel extends EntityEditPanel {
public CustomerAddressEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
setInitialFocusAttribute(CustomerAddress.CUSTOMER_FK);
createForeignKeyComboBox(CustomerAddress.CUSTOMER_FK);
createForeignKeyComboBox(CustomerAddress.ADDRESS_FK);
addInputPanel(CustomerAddress.CUSTOMER_FK);
addInputPanel(CustomerAddress.ADDRESS_FK);
}
}
SwingEntityModel customerAddressModel = new SwingEntityModel(CustomerAddress.TYPE, connectionProvider);
customerModel.addDetailModel(customerAddressModel);
EntityPanel customerAddressPanel = new EntityPanel(customerAddressModel,
new CustomerAddressEditPanel(customerAddressModel.editModel()));
customerPanel.addDetailPanel(customerAddressPanel);
//lazy initialization of UI components
customerPanel.initializePanel();
//populate the model with data from the database
customerModel.tableModel().refresh();
Dialogs.componentDialog(customerPanel)
.title("Customers")
.show();
6.3. Domain unit test
The EntityTestUnit class provides a way to unit test the domain model. The test method performs insert, update, select and delete on a randomly generated entity instance and verifies the results.
Module |
Artifact |
is.codion.framework.domain.test |
is.codion.jdk8:codion-framework-domain-test:0.17.16 |
class StoreTest extends EntityTestUnit {
public StoreTest() {
super(Store.class.getName(), User.parse("scott:tiger"));
}
@Test
void customer() throws DatabaseException {
test(Customer.TYPE);
}
@Test
void address() throws DatabaseException {
test(Address.TYPE);
}
@Test
void customerAddress() throws DatabaseException {
test(CustomerAddress.TYPE);
}
}
6.4. Persistance
The EntityConnection interface provides select, insert, update, and delete methods. It has three available implementations, one of which is based on a local JDBC connection, used below.
Module |
Artifact |
Description |
is.codion.framework.db.core |
is.codion.jdk8:codion-framework-db-core:0.17.16 |
Database API |
is.codion.framework.db.local |
is.codion.jdk8:codion-framework-db-local:0.17.16 |
Local JDBC |
6.4.1. Selecting
Store domain = new Store();
EntityConnection connection =
LocalEntityConnection.localEntityConnection(
Database.instance(), domain, User.parse("scott:tiger"));
//select customer where last name = Doe
Entity johnDoe = connection.selectSingle(Customer.LAST_NAME, "Doe");
//select all customer addresses
List<Entity> customerAddresses = //where customer = john doe
connection.select(CustomerAddress.CUSTOMER_FK, johnDoe);
Entity customerAddress = customerAddresses.get(0);
Entity address = customerAddress.referencedEntity(CustomerAddress.ADDRESS_FK);
String lastName = johnDoe.get(Customer.LAST_NAME);
String street = address.get(Address.STREET);
String city = address.get(Address.CITY);
6.4.2. Persisting
Store domain = new Store();
EntityConnection connection =
LocalEntityConnection.localEntityConnection(
Database.instance(), domain, User.parse("scott:tiger"));
Entities entities = domain.entities();
Entity customer = entities.builder(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.build();
connection.insert(customer);
Entity address = entities.builder(Address.TYPE)
.with(Address.STREET, "Elm Street 321")
.with(Address.CITY, "Syracuse")
.build();
connection.insert(address);
Entity customerAddress = entities.builder(CustomerAddress.TYPE)
.with(CustomerAddress.CUSTOMER_FK, customer)
.with(CustomerAddress.ADDRESS_FK, address)
.build();
connection.insert(customerAddress);
customer.put(Customer.FIRST_NAME, "Jonathan");
connection.update(customer);
connection.delete(customerAddress.primaryKey());
Full code
/*
* Copyright (c) 2004 - 2023, Björn Darri Sigurðsson. All Rights Reserved.
*/
package is.codion.framework.demos.manual.quickstart;
import is.codion.common.db.connection.DatabaseConnection;
import is.codion.common.db.database.Database;
import is.codion.common.db.exception.DatabaseException;
import is.codion.common.user.User;
import is.codion.framework.db.EntityConnection;
import is.codion.framework.db.EntityConnectionProvider;
import is.codion.framework.db.local.LocalEntityConnection;
import is.codion.framework.db.local.LocalEntityConnectionProvider;
import is.codion.framework.domain.DefaultDomain;
import is.codion.framework.domain.DomainType;
import is.codion.framework.domain.entity.Attribute;
import is.codion.framework.domain.entity.Entities;
import is.codion.framework.domain.entity.Entity;
import is.codion.framework.domain.entity.EntityType;
import is.codion.framework.domain.entity.ForeignKey;
import is.codion.framework.domain.entity.KeyGenerator;
import is.codion.framework.domain.entity.StringFactory;
import is.codion.framework.domain.entity.test.EntityTestUnit;
import is.codion.framework.domain.property.ColumnProperty;
import is.codion.swing.common.ui.dialog.Dialogs;
import is.codion.swing.framework.model.SwingEntityEditModel;
import is.codion.swing.framework.model.SwingEntityModel;
import is.codion.swing.framework.ui.EntityEditPanel;
import is.codion.swing.framework.ui.EntityPanel;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.util.List;
import static is.codion.framework.demos.manual.quickstart.Example.Store.*;
import static is.codion.framework.domain.DomainType.domainType;
import static is.codion.framework.domain.entity.EntityDefinition.definition;
import static is.codion.framework.domain.entity.KeyGenerator.automatic;
import static is.codion.framework.domain.property.Property.*;
import static java.util.UUID.randomUUID;
public final class Example {
public static class Store extends DefaultDomain {
static final DomainType DOMAIN = domainType(Store.class);
public Store() {
super(DOMAIN);
customer();
address();
customerAddress();
}
public interface Customer {
EntityType TYPE = DOMAIN.entityType("store.customer");
Attribute<String> ID = TYPE.stringAttribute("id");
Attribute<String> FIRST_NAME = TYPE.stringAttribute("first_name");
Attribute<String> LAST_NAME = TYPE.stringAttribute("last_name");
}
void customer() {
add(definition(
primaryKeyProperty(Customer.ID),
columnProperty(Customer.FIRST_NAME, "First name")
.nullable(false)
.maximumLength(40),
columnProperty(Customer.LAST_NAME, "Last name")
.nullable(false)
.maximumLength(40))
.keyGenerator(new CustomerKeyGenerator())
.stringFactory(StringFactory.builder()
.value(Customer.LAST_NAME)
.text(", ")
.value(Customer.FIRST_NAME)
.build()));
}
private static final class CustomerKeyGenerator implements KeyGenerator {
@Override
public void beforeInsert(Entity entity,
List<ColumnProperty<?>> primaryKeyProperties,
DatabaseConnection connection) throws SQLException {
entity.put(Customer.ID, randomUUID().toString());
}
}
public interface Address {
EntityType TYPE = DOMAIN.entityType("store.address");
Attribute<Integer> ID = TYPE.integerAttribute("id");
Attribute<String> STREET = TYPE.stringAttribute("street");
Attribute<String> CITY = TYPE.stringAttribute("city");
}
void address() {
add(definition(
primaryKeyProperty(Address.ID),
columnProperty(Address.STREET, "Street")
.nullable(false)
.maximumLength(120),
columnProperty(Address.CITY, "City")
.nullable(false)
.maximumLength(50))
.keyGenerator(automatic("store.address"))
.stringFactory(StringFactory.builder()
.value(Address.STREET)
.text(", ")
.value(Address.CITY)
.build()));
}
public interface CustomerAddress {
EntityType TYPE = DOMAIN.entityType("store.customer_address");
Attribute<Integer> ID = TYPE.integerAttribute("id");
Attribute<String> CUSTOMER_ID = TYPE.stringAttribute("customer_id");
Attribute<Integer> ADDRESS_ID = TYPE.integerAttribute("address_id");
ForeignKey CUSTOMER_FK = TYPE.foreignKey("customer_fk", CUSTOMER_ID, Customer.ID);
ForeignKey ADDRESS_FK = TYPE.foreignKey("address_fk", ADDRESS_ID, Address.ID);
}
void customerAddress() {
add(definition(
primaryKeyProperty(CustomerAddress.ID),
columnProperty(CustomerAddress.CUSTOMER_ID)
.nullable(false),
foreignKeyProperty(CustomerAddress.CUSTOMER_FK, "Customer"),
columnProperty(CustomerAddress.ADDRESS_ID)
.nullable(false),
foreignKeyProperty(CustomerAddress.ADDRESS_FK, "Address"))
.keyGenerator(automatic("store.customer_address"))
.caption("Customer address"));
}
}
static void customerPanel() {
class CustomerEditPanel extends EntityEditPanel {
public CustomerEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
setInitialFocusAttribute(Customer.FIRST_NAME);
createTextField(Customer.FIRST_NAME);
createTextField(Customer.LAST_NAME);
addInputPanel(Customer.FIRST_NAME);
addInputPanel(Customer.LAST_NAME);
}
}
EntityConnectionProvider connectionProvider =
LocalEntityConnectionProvider.builder()
.domainClassName(Store.class.getName())
.user(User.parse("scott:tiger"))
.build();
SwingEntityModel customerModel = new SwingEntityModel(Customer.TYPE, connectionProvider);
EntityPanel customerPanel = new EntityPanel(customerModel,
new CustomerEditPanel(customerModel.editModel()));
class CustomerAddressEditPanel extends EntityEditPanel {
public CustomerAddressEditPanel(SwingEntityEditModel editModel) {
super(editModel);
}
@Override
protected void initializeUI() {
setInitialFocusAttribute(CustomerAddress.CUSTOMER_FK);
createForeignKeyComboBox(CustomerAddress.CUSTOMER_FK);
createForeignKeyComboBox(CustomerAddress.ADDRESS_FK);
addInputPanel(CustomerAddress.CUSTOMER_FK);
addInputPanel(CustomerAddress.ADDRESS_FK);
}
}
SwingEntityModel customerAddressModel = new SwingEntityModel(CustomerAddress.TYPE, connectionProvider);
customerModel.addDetailModel(customerAddressModel);
EntityPanel customerAddressPanel = new EntityPanel(customerAddressModel,
new CustomerAddressEditPanel(customerAddressModel.editModel()));
customerPanel.addDetailPanel(customerAddressPanel);
//lazy initialization of UI components
customerPanel.initializePanel();
//populate the model with data from the database
customerModel.tableModel().refresh();
Dialogs.componentDialog(customerPanel)
.title("Customers")
.show();
}
static void domainModelTest() {
class StoreTest extends EntityTestUnit {
public StoreTest() {
super(Store.class.getName(), User.parse("scott:tiger"));
}
@Test
void customer() throws DatabaseException {
test(Customer.TYPE);
}
@Test
void address() throws DatabaseException {
test(Address.TYPE);
}
@Test
void customerAddress() throws DatabaseException {
test(CustomerAddress.TYPE);
}
}
}
static void selectEntities() throws DatabaseException {
Store domain = new Store();
EntityConnection connection =
LocalEntityConnection.localEntityConnection(
Database.instance(), domain, User.parse("scott:tiger"));
//select customer where last name = Doe
Entity johnDoe = connection.selectSingle(Customer.LAST_NAME, "Doe");
//select all customer addresses
List<Entity> customerAddresses = //where customer = john doe
connection.select(CustomerAddress.CUSTOMER_FK, johnDoe);
Entity customerAddress = customerAddresses.get(0);
Entity address = customerAddress.referencedEntity(CustomerAddress.ADDRESS_FK);
String lastName = johnDoe.get(Customer.LAST_NAME);
String street = address.get(Address.STREET);
String city = address.get(Address.CITY);
}
static void persistEntities() throws DatabaseException {
Store domain = new Store();
EntityConnection connection =
LocalEntityConnection.localEntityConnection(
Database.instance(), domain, User.parse("scott:tiger"));
Entities entities = domain.entities();
Entity customer = entities.builder(Customer.TYPE)
.with(Customer.FIRST_NAME, "John")
.with(Customer.LAST_NAME, "Doe")
.build();
connection.insert(customer);
Entity address = entities.builder(Address.TYPE)
.with(Address.STREET, "Elm Street 321")
.with(Address.CITY, "Syracuse")
.build();
connection.insert(address);
Entity customerAddress = entities.builder(CustomerAddress.TYPE)
.with(CustomerAddress.CUSTOMER_FK, customer)
.with(CustomerAddress.ADDRESS_FK, address)
.build();
connection.insert(customerAddress);
customer.put(Customer.FIRST_NAME, "Jonathan");
connection.update(customer);
connection.delete(customerAddress.primaryKey());
}
}