-
Notifications
You must be signed in to change notification settings - Fork 45
devon4j components
When working with devon4j the recommended approach for designing an applications is Component Oriented Design. Each component will represent a significant part (or feature) of our application related to CRUD operations. Internally, the components will be divided into three layers (service
, logic
, and dataaccess
) and will communicate in two directions: service with database or — in the logic layer — a component with another component.
The benefits of dividing our application into components are:
-
to avoid redundant code
-
self contained, descriptive and stable component APIs
-
data consistency (a component is responsible for its data and changes to this data shall only happen via the component)
A component consists of three packages, which correspond to the three layers defined by the devon4j architecture: service
, logic
and dataaccess
.
-
Service layer: exposes the REST API to exchange information with client applications
-
Logic layer: in charge of hosting the business logic of the application
-
Data Access layer: communicates with the database
Apart from that, most components will have a fourth package — common — to store shared elements, which will be used by all layers of the component. It will contain common interfaces, constants, exceptions or enumerations.
As we mentioned earlier, each component will be related to a functionality. This functionality will be represented in code by an Entity that defines all the properties needed to wrap the logic of that feature.
This Entity represents the "core" of the component and will be located in the dataaccess.api
package.
The naming convention for these entities in devon4j is:
[Target]Entity
"Target" should match the name of the related table in the database — although this is not mandatory.
Basically, each Entity is a POJO (plain old Java object) that will be mapped to a table in the database and represent each column via a suitable property.
We are now going to create our first app components. Our example application needs to provide two basic functionalities:
-
register a user (returning an access code)
-
show registered queue members
To accomplish this we are going to work with three entities; Visitor, Queue and AccessCode
:
The components will be defined as follows:
Visitor | Access Code | Daily Queue |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
— |
|
|
— |
|
|
— |
|
— |
— |
|
In addition, we will have to represent two relationships:
-
The one to one relation between Visitor and Access Code.
-
The one to many relation between Daily Queue and Access Code.
Now is the moment to decide about the components of our app. The low complexity of the functionality would allow us to create only one component for managing all entities. In order to clarify the example we are going to create three managing components however; one for Visitor, one for Access Code and one for Daily Queue.
ℹ️
|
If you feel more comfortable managing all the entities in a single component, you could also do it this way. The result will be the same, the only difference will be the structure of some elements and the distribution of code inside the packages. |
Projects created with the devon4j archetype already contain a pre-defined database schema, which we can use as a basis to create our own. We are going to utilize the H2 Database Engine, because our generated devon4j application uses it by default.
There are four pre-defined database schemas:
jtqj-core/src/main/resources/db/type/h2/V0001__Create_Sequence.sql
jtqj-core/src/main/resources/db/type/h2/V0002__Create_RevInfo.sql
jtqj-core/src/main/resources/db/type/h2/V0003__Create_BinaryObject.sql
jtqj-core/src/main/resources/db/migration/1.0/V0004__Add_blob_data.sql
ℹ️
|
May be you need to install some SQL editor from eclipse marketplace, or use an external one. |
We are going to create our own table for Visitor(s) by right-clicking the folder /jtqj-core/src/main/resources/db/migration/1.0
and selecting New > File
. Following the naming scheme we are going to call it:
V0005__Create_Visitor.sql
A visitor will provide: username
, name
, password
, phoneNumber
, acceptedCommercial
and acceptedTerms
in order to obtain an Access Code. We need to represent this data in our table:
create table Visitor(
id BIGINT NOT NULL AUTO_INCREMENT,
modificationCounter INTEGER NOT NULL,
username VARCHAR(255),
name VARCHAR(255),
password VARCHAR(255),
phoneNumber VARCHAR(255),
acceptedCommercial BOOL DEFAULT '0',
acceptedTerms BOOL NOT NULL DEFAULT '0',
userType BOOL DEFAULT '0',
CONSTRAINT PK_Visitor PRIMARY KEY(id)
);
-
id
: TheID
of each visitor. -
modificationCounter
: Used internally by JPA to take care of optimistic locking for us. -
username
: The visitors email address. -
name
: The visitors name. -
password
: The visitors password. -
phoneNumber
: The visitors phone number. -
acceptedCommercial
: Aboolean
to denote if the visitor has the accepted commercial agreements. -
acceptedTerms
: Aboolean
to denote if the visitor has accepted the terms & conditions. -
userType
: Denotes the type of user.
In a second table we will represent the Daily Queue, which will contain name
, logo
, currentNumber
, attentionTime
, minAttentionTime
, active
and customers
. This table will be created in /jtqj-core/src/main/resources/db/type/h2
, and is called:
V0006__Create_Queue.sql
It will contain the following declarations:
create table DailyQueue(
id BIGINT NOT NULL AUTO_INCREMENT,
modificationCounter INTEGER NOT NULL,
name VARCHAR(255),
logo VARCHAR(255),
currentNumber VARCHAR(255),
attentionTime TIMESTAMP,
minAttentionTime TIMESTAMP NOT NULL DEFAULT '60000',
active BOOL NOT NULL DEFAULT '1',
customers INTEGER NOT NULL DEFAULT '0',
CONSTRAINT PK_DailyQueue PRIMARY KEY(id)
);
-
id
: TheID
of each queue. -
modificationCounter
: Used internally by JPA to take care of optimistic locking for us. -
name
: The queues name. -
logo
: The queues logo. -
currentNumber
: the queue’s number being attended. -
attentionTime
: Average time required to attend a visitor. -
minAttentionTime
: Minimum time required to attend a visitor, set by default. -
active
: Aboolean
to denote if the queue is active. -
customer
: The queues total number of customers.
The third table will represent the Access Code and contain the ticketNumber
, creationTime
, startTime
and endTime
. This table will be created in /jtqj-core/src/main/resources/db/type/h2
, and is called:
V0007__Create_Access_Code.sql
It will contain the following declarations:
CREATE TABLE AccessCode(
id BIGINT NOT NULL AUTO_INCREMENT,
modificationCounter INTEGER NOT NULL,
ticketNumber VARCHAR(5),
creationTime TIMESTAMP,
startTime TIMESTAMP,
endTime TIMESTAMP,
idVisitor BIGINT NOT NULL,
idQueue BIGINT NOT NULL,
CONSTRAINT PK_AccessCode PRIMARY KEY(id),
CONSTRAINT FK_AccessCode_idVisitor FOREIGN KEY(idVisitor) REFERENCES Visitor(id),
CONSTRAINT FK_AccessCode_idQueue FOREIGN KEY(idQueue) REFERENCES DailyQueue(id)
);
-
id
: TheID
of each code. -
modificationCounter
: Used internally by JPA to take care of optimistic locking for us. -
ticketNumber
: The number of the ticket for a queue. -
creationTime
: The date and time of creation. -
startTime
: The date and time, from which the code is valid. -
endTime
: The date and time, when the code expires. -
idVisitor
: The relation with the Visitor table. -
idQueue
: The relation with theDailyQueue
table.
Finally we are going to provide a certain amount of mock data, which will be available right from the start in our application. Create a new SQL script in /jtqj-core/src/main/resources/db/migration/1.0/
, called:
V0008__Master_data.sql
Copy and paste the following data into it:
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (0, 1, 'mike@mail.com', 'test', '1', '123456789', '0', '1', '1');
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (1, 1, 'peter@mail.com', 'test', '1', '123456789', '1', '1', '0');
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (2, 1, 'pablo@mail.com', 'test', '1', '123456789', '0', '1', '0');
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (3, 1, 'test1@mail.com', 'test', '1', '123456789', '0', '1', '0');
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (4, 1, 'test2@mail.com', 'test', '1', '123456789', '1', '1', '0');
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (5, 1, 'test3@mail.com', 'test', '1', '123456789', '0', '1', '0');
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (6, 1, 'test4@mail.com', 'test', '1', '123456789', '0', '1', '0');
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (7, 1, 'test5@mail.com', 'test', '1', '123456789', '1', '1', '0');
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (8, 1, 'test6@mail.com', 'test', '1', '123456789', '0', '1', '0');
INSERT INTO Visitor (id, modificationCounter, username, name, password, phoneNumber, acceptedCommercial, acceptedTerms, userType) VALUES (9, 1, 'test7@mail.com', 'test', '1', '123456789', '0', '1', '0');
INSERT INTO DailyQueue (id, modificationCounter, name, logo, currentNumber, attentionTime, minAttentionTime, active, customers) VALUES (1, 1, 'Day2', 'C:/logos/Day1Logo.png', 'Q001', NULL, '1970-01-01 00:01:00', TRUE, 9);
INSERT INTO AccessCode (id, modificationCounter, ticketNumber, creationTime, startTime, endTime, idVisitor, idQueue) VALUES (1, 1, 'Q001', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, 1, 1);
INSERT INTO AccessCode (id, modificationCounter, ticketNumber, creationTime, startTime, endTime, idVisitor, idQueue) VALUES (2, 1, 'Q002', CURRENT_TIMESTAMP, '2008-01-01 00:00:01', NULL, 2, 1);
INSERT INTO AccessCode (id, modificationCounter, ticketNumber, creationTime, startTime, endTime, idVisitor, idQueue) VALUES (3, 1, 'Q003', CURRENT_TIMESTAMP, '2008-01-01 00:00:01', NULL, 3, 1);
INSERT INTO AccessCode (id, modificationCounter, ticketNumber, creationTime, startTime, endTime, idVisitor, idQueue) VALUES (4, 1, 'Q004', CURRENT_TIMESTAMP, '2008-01-01 00:00:01', NULL, 4, 1);
INSERT INTO AccessCode (id, modificationCounter, ticketNumber, creationTime, startTime, endTime, idVisitor, idQueue) VALUES (5, 1, 'Q005', CURRENT_TIMESTAMP, '2008-01-01 00:00:01', NULL, 5, 1);
INSERT INTO AccessCode (id, modificationCounter, ticketNumber, creationTime, startTime, endTime, idVisitor, idQueue) VALUES (6, 1, 'Q006', CURRENT_TIMESTAMP, '2008-01-01 00:00:01', NULL, 6, 1);
INSERT INTO AccessCode (id, modificationCounter, ticketNumber, creationTime, startTime, endTime, idVisitor, idQueue) VALUES (7, 1, 'Q007', CURRENT_TIMESTAMP, '2008-01-01 00:00:01', NULL, 7, 1);
INSERT INTO AccessCode (id, modificationCounter, ticketNumber, creationTime, startTime, endTime, idVisitor, idQueue) VALUES (8, 1, 'Q008', CURRENT_TIMESTAMP, '2008-01-01 00:00:01', NULL, 8, 1);
INSERT INTO AccessCode (id, modificationCounter, ticketNumber, creationTime, startTime, endTime, idVisitor, idQueue) VALUES (9, 1, 'Q009', CURRENT_TIMESTAMP, '2008-01-01 00:00:01', NULL, 9, 1);
Now that we have defined the database for our entities, we should start creating the code of the related components.
We are going to use CobiGen to generate the component structure. That means that — as already commented — we can generate all the structure and layers starting from a core element: a simple Plain Old Java Object that represents our Entity. So, in order to use CobiGen, we have to create our entities in the expected locations (as you will see in the following section): <entitymanagement>.dataaccess.api
.
To implement the component we will need to define a VisitorEntity
to connect and manage the data of the Visitor table in the database. The name of this component will be visitormanagement
, the entity will be called VisitorEntity
.
Right-click on the root folder of the project /jtqj-core/src/main/java
, select New > Package
and create the following package:
com.devonfw.application.jtqj.visitormanagement.dataaccess.api
Now create a new Java class in this package and call it VisitorEntity
:
We are going to need fields, which represent the data model, so our entity should contain the following code:
...
private String username;
private String name;
private String phoneNumber;
private String password;
private Boolean acceptedCommercial;
private Boolean acceptedTerms;
private Boolean userType;
...
ℹ️
|
We are not adding |
Now we need to declare our entity as a JPA
entity with the @Entity
annotation (javax.persistence.Entity
) at class level. To map the entity to the database table, we will use the @Table
annotation (javax.persistence.Table
) defining the name
of our already created Visitor table (also at class level):
...
@Entity
@Table(name = "Visitor")
public class VisitorEntity {
...
Now we have to declare the getter and setter methods for the fields of our entity. We can do this manually or automatically generate them using Eclipse:
The resulting implementation of our VisitorEntity
class should now look like this:
package com.devonfw.application.jtqj.visitormanagement.dataaccess.api;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "Visitor")
public class VisitorEntity {
private String username;
private String name;
private String phoneNumber;
private String password;
private Boolean acceptedCommercial;
private Boolean acceptedTerms;
private Boolean userType;
/**
* @return the username
*/
public String getUsername() {
return username;
}
/**
* @param username the username to set
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the phoneNumber
*/
public String getPhoneNumber() {
return phoneNumber;
}
/**
* @param phoneNumber the phoneNumber to set
*/
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
/**
* @return the password
*/
public String getPassword() {
return password;
}
/**
* @param password the password to set
*/
public void setPassword(String password) {
this.password = password;
}
/**
* @return the acceptedCommercial
*/
public Boolean getAcceptedCommercial() {
return acceptedCommercial;
}
/**
* @param acceptedCommercial the acceptedCommercial to set
*/
public void setAcceptedCommercial(Boolean acceptedCommercial) {
this.acceptedCommercial = acceptedCommercial;
}
/**
* @return the acceptedTerms
*/
public Boolean getAcceptedTerms() {
return acceptedTerms;
}
/**
* @param acceptedTerms the acceptedTerms to set
*/
public void setAcceptedTerms(Boolean acceptedTerms) {
this.acceptedTerms = acceptedTerms;
}
/**
* @return the userType
*/
public Boolean getUserType() {
return userType;
}
/**
* @param userType the userType to set
*/
public void setUserType(Boolean userType) {
this.userType = userType;
}
}
We are going to repeat the same process for the AccessCode
component. Create these packages in /jtqj-core/src/main/java
:
com.devonfw.application.jtqj.accesscodemanagement.dataaccess.api
... and create a class called AccessCodeEntity
inside of them.
We will end up with the following structure:
The contents of AccessCodeEntity
before using CobiGen will be:
package com.devonfw.application.jtqj.accesscodemanagement.dataaccess.api;
import java.sql.Timestamp;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.constraints.Size;
import com.devonfw.application.jtqj.visitormanagement.dataaccess.api.VisitorEntity;
@Entity
@Table(name = "AccessCode")
public class AccessCodeEntity {
@Size(min = 2, max = 5)
private String ticketNumber;
@Temporal(TemporalType.TIMESTAMP)
private Timestamp creationTime;
@Temporal(TemporalType.TIMESTAMP)
private Timestamp startTime;
@Temporal(TemporalType.TIMESTAMP)
private Timestamp endTime;
private VisitorEntity visitor;
private QueueEntity queue;
/**
* @return the ticketNumber
*/
public String getTicketNumber() {
return ticketNumber;
}
/**
* @param ticketNumber the ticketNumber to set
*/
public void setTicketNumber(String ticketNumber) {
this.ticketNumber = ticketNumber;
}
/**
* @return the creationTime
*/
public Timestamp getCreationTime() {
return creationTime;
}
/**
* @param creationTime the creationTime to set
*/
public void setCreationTime(Timestamp creationTime) {
this.creationTime = creationTime;
}
/**
* @return the startTime
*/
public Timestamp getStartTime() {
return startTime;
}
/**
* @param startTime the startTime to set
*/
public void setStartTime(Timestamp startTime) {
this.startTime = startTime;
}
/**
* @return the endTime
*/
public Timestamp getEndTime() {
return endTime;
}
/**
* @param endTime the endTime to set
*/
public void setEndTime(Timestamp endTime) {
this.endTime = endTime;
}
/**
* @return the visitor
*/
@OneToOne(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
@JoinColumn(name = "idVisitor")
public VisitorEntity getVisitor() {
return visitor;
}
/**
* @param visitor the visitor to set
*/
public void setVisitor(VisitorEntity visitor) {
this.visitor = visitor;
}
/**
* @return the queue
*/
@ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
@JoinColumn(name = "idQueue")
public QueueEntity getQueue() {
return queue;
}
/**
* @param queue the queue to set
*/
public void setQueue(QueueEntity queue) {
this.queue = queue;
}
}
|
Eclipse will report some errors related to |
Finally, we are going to repeat the same process for our last entity QueueEntity
component. Create these packages in /jtqj-core/src/main/java
:
com.devonfw.application.jtqj.queuemanagement.dataaccess.api
... and create a class called QueueEntity
inside of them.
We will end up with the following structure:
The contents of QueueEntity
before using CobiGen will be:
package com.devonfw.application.jtqj.queuemanagement.dataaccess.api;
import java.sql.Timestamp;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "DailyQueue")
public class QueueEntity {
private String name;
private String logo;
private String currentNumber;
@Temporal(TemporalType.TIMESTAMP)
private Timestamp attentionTime;
@Temporal(TemporalType.TIMESTAMP)
private Timestamp minAttentionTime;
private Boolean active;
private int customers;
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the logo
*/
public String getLogo() {
return logo;
}
/**
* @param logo the logo to set
*/
public void setLogo(String logo) {
this.logo = logo;
}
/**
* @return the currentNumber
*/
public String getCurrentNumber() {
return currentNumber;
}
/**
* @param currentNumber the currentNumber to set
*/
public void setCurrentNumber(String currentNumber) {
this.currentNumber = currentNumber;
}
/**
* @return the attentionTime
*/
public Timestamp getAttentionTime() {
return attentionTime;
}
/**
* @param attentionTime the attentionTime to set
*/
public void setAttentionTime(Timestamp attentionTime) {
this.attentionTime = attentionTime;
}
/**
* @return the minAttentionTime
*/
public Timestamp getMinAttentionTime() {
return minAttentionTime;
}
/**
* @param minAttentionTime the minAttentionTime to set
*/
public void setMinAttentionTime(Timestamp minAttentionTime) {
this.minAttentionTime = minAttentionTime;
}
/**
* @return the active
*/
public Boolean getActive() {
return active;
}
/**
* @param active the active to set
*/
public void setActive(Boolean active) {
this.active = active;
}
/**
* @return the customers
*/
public int getCustomers() {
return customers;
}
/**
* @param customers the customers to set
*/
public void setCustomers(int customers) {
this.customers = customers;
}
}
Now we have finished preparing the core of our components and can start using CobiGen to generate the remaining structure (services, layers, DAOs, …).
❗
|
Now we can resolve the compilation errors related to Or just manually add this line to your import statements:
|
Once we are finished creating the core of our components we could continue to create the structure and all elements manually, but we are going to use CobiGen for these tasks, since we can save a significant amount of time and effort this way.
First however, we need to make sure that the CobiGen plugin is installed in our Eclipse instance:
If you don’t see this option in the dropdown menu, close Eclipse (remember to save all your progress) and in the jump-the-queue
folder right-click to Open a Devon `CMD
shell here`.
Now enter and execute:
devon eclipse add-plugin cobigen
... and re-open Eclipse via the eclipse-main.bat
script.
When using CobiGen for the first time it’s recommended to check the health of the tool.
To do so, right-click one of our entities and select CobiGen > Health Check…
.
The next dialogs will show us if there are outdated templates. In that case just click the "Update" button. You can also run an Advanced Health Check to see exactly which CobiGen templates are available for this project.
In case you receive an error like this: image::images/devon4j/4.Components/templates_not_found.png[CobiGen Health Check 3, 400]
You need to force download of templates as in the following image: image::images/devon4j/4.Components/adapt-templates.png[CobiGen Health Check 3, 550]
Now the templates should be downloaded, and you will see a new folder in the workspace: image::images/devon4j/4.Components/cobigen-folder.png[CobiGen Health Check 3, 400]
In order to create the whole structure of a component with CobiGen we only need to right-click our component core entity (QueueEntity
) and select CobiGen > Generate
.
Now we’ll get to choose which packages we want to generate with the tool.
To get the needed functionalities for our component we are going to select all of the following packages at the same time:
By default, all files will be selected for generation (which is what we want in this case), but you could also change which files will be generated by clicking Customize
.
For now just click Finish
and let CobiGen do its work.
ℹ️
|
In detail the selected options do the following:
|
During the process CobiGen will show a message asking us to review some ambiguous references, which we will get to right away. For now just click Continue
.
Once CobiGen has finished generating the new classes, we will check for and fix those ambiguous references if we need to introduce manual adjustments.
First, we need to adjust manually some imports related to Timestamp in:
jtqj-core
:
-
queuemanagement.dataaccess.api.repo.QueueRepository
jtqj-api
:
-
queuemanagement.common.api.Queue
-
queuemanagement.logic.api.to.QueueEto
-
queuemanagement.logic.api.to.QueueSearchCriteriaTo
We can fix these errors manually by adding import java.sql.Timestamp
to the affected Java files:
We repeat this process on our AccessCodeEntity
, but in this case — since its an entity with relations — we are going to have to select different CobiGen options:
After CobiGen has finished generating, fix the issues regarding import java.sql.Timestamp
(as you did in the last step) in the following files:
jtqj-core
:
-
accesscodemanagement.dataaccess.api.repo.AccessCodeRepository
jtqj-api
:
-
accesscodemanagement.common.api.AccessCode
-
accesscodemanagement.logic.api.to.AccessCodeEto
-
accesscodemanagement.logic.api.to.AccessCodeSearchCriteriaTo
There will be some compilation errors left. This is because we have some dependencies on Queue and Visitor component elements, that are not created yet. These compilation errors will be fixed in the next steps.
Finally we are going to generate the same classes that we generated for the QueueEntity
component for our VisitorEntity
component:
Once CobiGen has finished we can fix the rest of the compilation errors related to VisitorEto
by manually importing the class into:
jtqj-core
:
-
accesscodemanagement.logic.impl.usecase.UcFindAccessCodeImpl
jtqj-api
:
-
accesscodemanagement.logic.api.to.AccessCodeCto
If all compilation errors are solved run the app (right-click SpringBootApp.java > Run As > Java Application
). The back-end should launch without errors.
Congratulations!
You have created your first devon4j components. You should be able to access the login screen via localhost:8081/jumpthequeue. You can login with the username and password "waiter". In the next chapter we will show and explain each of the created elements in detail.
Next Chapter: devon4j Structure
This documentation is licensed under the Creative Commons License (Attribution-NoDerivatives 4.0 International).