Applications produced using DOC
allow processing data. This data takes the form of "scenarios" that are formatted upon a model.
The application data model is described using the JHipster Domain Language, either:
In a single file, for a simple data model. For more details, refer to Section Defining a Data Model; or
In several files, for a composite data model. For more details, refer to Section Defining a Composite Data Model (CDM).
The data model defines the structure for the scenarios to import into the application. For more details, refer to Section Managing the Application Data.
In a .jdl
file, names should be declared without any whitespace. This includes the:
entity
names;
attribute
names;
relationship
names; and
collectorClass
name.
JDL declarations can be annotated using DOM
tags to provide information to DOC
generator. For more details, refer to Section Generating the Application Structure.
Note that, while the data model is described using a JDL file, |
The following examples show how DOM tags are inserted in JDL:
entity ResourceUsagePerDay { // DOM [primary.keys] : [day] day Integer, numberOfHours Integer, } relationship ManyToOne { // DOM [affects.primary.key] : [true] Requirement{activity} to Activity{requirements}, ... }
The following JDL text provides you with an example of a simple data model:
application { // DOM [java.collectorClass] : [<%=collectorClass%>] } entity Country { // DOM [primary.keys] : [id] id String required, name String } entity Plant { // DOM [primary.keys] : [plantId] plantId String required, } entity Resource { // DOM [primary.keys] : [id] id String required, name String } entity ResourceCapacity { quantity Integer } entity Activity { // DOM [primary.keys] : [id] id String required, name String, durationInHours Double } entity Precedence { } entity Requirement { quantity Integer } entity Schedule { start Instant, end Instant } entity SolutionSummary { // DOM [single.row] : [true] start Instant, end Instant, timespan Integer } entity ResourceUsagePerDay { // DOM [primary.keys] : [day] day Integer, numberOfHours Integer, } relationship ManyToOne { // DOM [affects.primary.key] : [true] Requirement{activity} to Activity{requirements}, // DOM [affects.primary.key] : [true] Requirement{resource} to Resource{requirements}, // DOM [affects.primary.key] : [true] Precedence{first} to Activity{successors}, // DOM [affects.primary.key] : [true] Precedence{second} to Activity{predecessors}, // DOM [affects.primary.key] : [true] Schedule{resource} to Resource{schedules} // DOM [affects.primary.key] : [true] Schedule{activity} to Activity{schedules} // DOM [affects.primary.key] : [true] ResourceUsagePerDay{resource} to Resource{usagePerDay} // DOM [affects.primary.key] : [true] Plant{country} to Country{plants} // DOM [affects.primary.key] : [true] Activity{plant} to Plant{activities} } relationship OneToOne { // DOM [affects.primary.key] : [true] ResourceCapacity{resource} to Resource{capacity} }
A JDL file should contain exactly one application definition block. In a composite data model, the application block must be placed in the main JDL file.
In DOC
context, the collector class name will be injected during the generation of the application. For this injection to succeed, the <%=collectorClass%>
placeholder must be provided.
application { // DOM [java.collectorClass] : [<%=collectorClass%>] }
Any other information is ignored.
Note that the following application
|
Entities are related to tables in the SQL world and to classes in the java world.
Entities are declared one after the other. The order of the declaration has no impact on the application generation. Entities are described by a unique name and by a set of attributes of elementary types. Attributes that are part of a relationship to another entity are declared separately. Additional information can be provided using DOM tags or annotations.
Valid elementary types for attributes are:
String
, to represent any string value.
Instant
, to represent an instant in time.
LocalDate
, to represent a date without a time zone.
LocalTime
, to represent a local time.
LocalDateTime
, to represent a date with time without a time zone.
Duration
, to represent a duration (time between two dates).
Integer
, to represent an integer value.
Long
, to represent a long value.
Double
, to represent a double value.
Boolean
, to represent a boolean value.
Binary
, to represent binary data.
More information on the data types can be found in Section Understanding Data Types
The following sample code declares an entity named Resource
which has two attributes:
an id
of type String
a name
of type String
The DOM
tag states that the id
attribute is part of the Resource
primary key.
entity Resource { // DOM [primary.keys] : [id] id String required, name String }
Note that, if |
You will find in this section the different data formats supported.
The JDL column represents the type that can be used in the JDL files.
The PostgreSQL column shows how the data is stored in the database.
The Java column shows the used type in the collectors.
The DBM column represents the data type imported from DOC3.9.
JDL | PostgreSQL | Java | DBM |
---|---|---|---|
String | text | String | VARCHAR, CHAR |
Integer | integer | Integer | INTEGER INT SMALLINT TINYINT |
Long | bigint | Long | BIGINT |
Double | double precision | Double | DOUBLE PRECISION, DECIMAL, FLOAT, REAL, NUMERIC |
Boolean | boolean | Boolean | BOOLEAN, BIT |
Instant | bigint | Instant | TIMESTAMP, DATE |
LocalDate | bigint | LocalDate | - |
LocalTime | bigint | LocalTime | TIME |
LocalDateTime | bigint | LocalDateTime | - |
Duration | bigint | Duration | - |
Binary | bytea | byte[] | BINARY |
To send data between services or to the web client, the following format is used:
JDL | Typescript | Java | Representation |
---|---|---|---|
String | string | String | - |
Integer | number | Integer | - |
Long | number | Long | - |
Double | number | Double | - |
Boolean | boolean | Boolean | - |
Instant | number | Long | The number of milliseconds since the epoch of 1970-01-01T00:00:00Z |
LocalDate | number | Long | The number of days since the epoch of 1970-01-01T00:00:00Z |
LocalTime | number | Long | The number of milliseconds from beginning of the day |
LocalDateTime | number | Long | The number of milliseconds since the epoch of 1970-01-01T00:00:00Z using UTC offset |
Duration | number | Long | The total length of the duration in milliseconds |
Binary | string | String | A Base64-encoded string |
Each entity and field can have a description associated with it.
To add a description the annotation @Description
should be used.
@Description("My entity description") entity MyEntity { @Description("My attribute description \n in multiple lines.") myAttribute String }
All annotations support text blocks, for example:
@Description(""" This is a "multiline" description. """)
Attributes may be assigned default values using @DefaultValue
annotation. Such an annotation must be added within the entity declaration just before the field definition.
The syntax is the following:
entity XXXXX { ... @DefaultValue("value1") attribute1 <elementaryType> ... @DefaultValue("value2") attribute2 <elementaryType> ... }
The following example defines 10
as the default value for the quantity
attribute of the ResourceCapacity
entity:
entity ResourceCapacity { @DefaultValue("10") quantity Integer }
The following table gives some examples by type.
Question | Description | Example |
---|---|---|
String | String value, special character supported: \n (new line), \r (carriage return), \t (tabulation) | @DefaultValue("Lorem ipsum dolor amet") |
Instant | a date in the ISO-8601 format | @DefaultValue("2007-12-03T10:15:30.00Z") |
LocalDate | a local date in the ISO-8601 format | @DefaultValue("2007-12-03") |
LocalTime | a local time in the ISO-8601 format | @DefaultValue("10:15:30") |
LocalDateTime | a local date time in the ISO-8601 format | @DefaultValue("2007-12-03T10:15:30") |
Duration | a duration in the ISO-8601 format or a double representing seconds | @DefaultValue("PT20.345S") @DefaultValue("20.345") |
Integer | an integer value (e.g. 42) | @DefaultValue("42") |
ILong | a long value | @DefaultValue("42") |
Double | a double value | @DefaultValue("42.5") |
Boolean | a boolean value 'true' or 'false' case-insensitive | @DefaultValue("true") |
Binary | No default value support | N/A |
Computed fields are not editable fields, as they have dynamic values computed by the database during fetching. They are annotated in the JDL file using the @Formula
annotation.
The underlying implementation of JDL computed fields uses the Hibernate Formula. The expressions must comply with the Hibernate Formula syntax and be supported by Postgres; refer to https://docs.jboss.org/hibernate/orm/current/javadocs/org/hibernate/annotations/Formula.html.
The @Formula
annotation takes as a parameter an expression to compute the field value. The expression can be a simple expression or a complex SQL query.
The following example shows a simple expression:
entity Employee { // DOM [primary.keys] : [id] id String required, firstName String, lastName String, @Description("Example of computed field with firstname and lastname") @Formula("concat(first_name, ' ', last_name)") fullName String }
When the expression is a complex SQL query, one must ensure that the query filters out data using the current Scenario ID, which is always present in column db_gene_internal_scenario_id
. Relations are implemented using a single column foreign key to the db_gene_internal_id
column of the target table.
In the following example, the computed field averageAge
of entity Company
represents the average value of the field age of entity Employee
.
Note that the Hibernate Formula SQL expressions use snake-case table and column names (https://en.wikipedia.org/wiki/Snake_case). If you are uncertain of the column and table names, check the generated JPA entities under the gene-model/gene-model-jpa/build/generated
module of your project.
entity Company { // DOM [primary.keys] : [id] id String required, @Description("Employees average age") @Formula(""" (SELECT AVG(e.age) FROM employee e WHERE e.company = db_gene_internal_id AND e.db_gene_internal_scenario_id = db_gene_internal_scenario_id) """) averageAge Double } entity Employee { // DOM [primary.keys] : [id] id String required, firstName String, lastName String, age Integer } relationship ManyToOne { // DOM [affects.primary.key] : [true] Employee{company} to Company{employees} }
The e.company = db_gene_internal_id
clause implements the join between tables company
and employee
, in which db_gene_internal_id
represents the primary key identifier of the current table company and e.company
represents the foreign key column in the table employee
referencing the company
table primary key.
The e.db_gene_internal_scenario_id = db_gene_internal_scenario_id
clause restricts data from the joined table employee
to the current scenario.
Required
Defines whether the value of the field cannot be null.
Example of use:
entity Activity { name String required }
Unique
Defines whether the field (or a list of fields) is a unique key.
There are two ways of defining a unique constraint:
In-lined with the field definition (single unique field)
Defined with '@UniqueConstraint' annotation (single or multiple unique fields)
Example of use:
entity DayOfWeek { name String unique } @UniqueConstraint("id, lastName") entity Persons { id String lastName String firstName String }
Min/Max bounds
Defines the min and/or the max values of a field.
Example of use:
entity { dayOfWeek Integer min(0) max(6) }
Length
Defines the maximum length of a String field. Note that, by default, String fields do not have a maximum length and can contain large text data.
Example of use:
entity { zipCode String length(10) }
Field type and constraint association
The following table lists the supported constraints by type.
Example of use:
Question | Allowed Constraint |
---|---|
String | required, unique, length |
Instant | required, unique |
LocalDate | required, unique |
LocalTime | required, unique |
LocalDateTime | required, unique |
Duration | required, unique |
Integer | required, unique, min, max |
Long | required, unique, min, max |
Double | required, unique, min, max |
Boolean | required, unique |
Binary | required, unique |
In some cases, an entity is meant to represent a unique instance or row in a table. This is typically the case for unique Key Performance Indicators. To show that only one instance/row should be considered, the single.row
DOM
tag should be used.
The following SolutionSummary
entity will have only one instance/row:
entity SolutionSummary { // DOM [single.row] : [true] start Instant, end Instant, timespan Integer }
The following entity DOM
tags are recognized:
[java.className] : [MyClassName]
used to give an explicit java class name to the entity. If not present, a default name is computed from the entity name.
[java.collectionName] : [myEntities]
a name used to represent the entity collection in the collector. If not present, a default name is computed from the entity name.
[java.externalInterfaces] : [list of comma-separated interface names]
a list of interfaces that will be added as implemented by the generated class.
[primary.keys] : [id, name]
a list of comma-separated elementary attributes that are part of the entity primary key.
[single.row] : [true]
indicates that the entity will have only one instance/row. By default, this is false
.
There are two kinds of relationships:
OneToOne
- is used to represent 1:1 relationships between entities.
ManyToOne
- is used to represent N:1 relationships between entities.
The following relationship DOM
tags are recognized and optional:
[affects.primary.key] : [true or false]
indicates that the primary key of the first end of the relationship is affected by this relationship (default false
).
[java.isReferenceInverted] : [true or false]
indicates whether the relationship should be inverted or not in the generated java code. By default, relationships are inverted.
When a relation is affecting the primary key, the relation is implicitly required
(not nullable).
A relation that is not affecting the primary key is by default optional (nullable). To declare that a relation is not nullable, you need to use the required
keyword as follows:
Precedence { second required } to Activity { predecessors }
For example, the following sample code declares a OneToOne
relationship between two entities:
entity Resource { // DOM [primary.keys] : [id] id String required, name String } entity ResourceCapacity { quantity Integer } relationship OneToOne { // DOM [affects.primary.key] : [true] ResourceCapacity{resource} to Resource{capacity} }
This declaration affects the entities as follows:
the Resource
entity has three attributes (an id
, a name
of type String
and a capacity
of type ResourceCapacity
).
the primary key of a Resource
entity is built from id
. The ResourceCapacity
entity has two attributes (quantity
of type Integer
and resource
of type Resource
).
the primary key of a ResourceCapacity
entity is built from the primary key of the attached Resource
.
The following example declares a ManyToOne
relationship between two entities:
entity Activity { // DOM [primary.keys] : [id] id String required, name String, durationInHours Integer } entity Precedence { } relationship ManyToOne { // DOM [affects.primary.key] : [true] Precedence{first required} to Activity{successors}, // DOM [affects.primary.key] : [true] Precedence{second required} to Activity{predecessors} }
This declaration affects the entities as follows:
the Activity
entity has five attributes (id
and name
of type String
, durationInHours
of type Integer
, while successors
and predecessors
are arrays of type Precedence
)
the Precedence
entity has two attributes (first
and second
of type Activity
) that cannot be null
the primary key of a Precedence
entity is built from the primary keys of the attached first
and second
activities.
When using the generated java code, setting an activity in the first
attribute of a precedence will automatically add the precedence in the successors
list of that activity. Similarly, removing a precedence from the successors
list of an activity will automatically set the first
attribute of that precedence to null
.
DOC
allows chunking the data model into different components that can depend on one another in a composite data model.
By default, a simple data model application declares only one scenario type while a composite data model declares several scenario types.
In a CDM, a scenario type only describes some entities of the data model. However, it can reference or be referenced by other scenario types to provide the user with all the relevant data when necessary.
This improves performances as it prevents duplicating data and allows synchronizing parts of the model for the purpose at hand. For example, instead of managing a large amount of scenarios with the same reccuring data everytime an operation is performed on an entity attribute, users only need the type of scenario that describes the entity in question.
This makes sense in the example below where the Plant
and Country
entities from the MasterData
scenarios or the Truck Route
in the DeliveryData
scenarios are less likely to change overtime than the daily Schedule
described in the PlanData
scenarios.
A composite data model is described across several JDL files, located in the gene-model/spec
directory of the project, either directly or organized in subfolders.
Among the JDL files that define the data model description, exactly one must contain an application
block. It is called the “main JDL file”, capacity_planning.jdl
here.
In a CDM, as opposed to a simple data model, the application
block should contain include
statements that indicate the additional JDL files to consider in the definition of the data model. Here is an example:
application { // DOM [java.collectorClass] : [CapacityPlanning] include "master_data.jdl" include "delivery_data.jdl" include "transactional_data.jdl" include "plan_data.jdl" }
The paths of the included JDL files are considered to be relative to the directory where the main JDL file is stored.
A composite data model is structured in scenario types. Each entity of a composite data model belongs to a scenario type. The top-level blocks that can be found in a JDL file are application
, scenarioType
, entity
and relationship
.
Any JDL file that contains entities must also contain exactly one scenarioType
block. In other words, the JDL file defines the scenario type.
By definition, the main JDL file contains an application
block; it may also contain scenarioType
, entity
, and relationship
blocks. The two supported use cases are:
The main JDL file only contains an application
block.
The main JDL file contains an application
block, a scenarioType
block, and zero or more entity
and relationship
blocks.
Included JDL files must contain a scenarioType
block and zero or more entity
and relationship
blocks.
The scenarioType
block provides the name of the scenario type that is defined by the JDL file. There is no constraint between the name of the JDL file and the name of the scenario type. The name of the scenario type must be a valid identifier. The convention is to use Pascal case for scenario type names (that is, attached words with initial letters in uppercase, including the very first letter), as we do for examples for entities. The scenarioType
block supports the @Description
annotation.
@Description("Master data of the application") scenarioType MasterData { }
In a composite data model, all the entities in a JDL file are attached to the scenario type that is defined in the JDL file. There is nothing to do in the entity
block to achieve this, except for putting this block in the same JDL file as the scenarioType
block that defines the scenario type.
Entity names must be unique across the application data model.
Relations between two entities of the same scenario type are declared by simply including them in the same JDL file as the two entities.
Relations from an entity in scenario type ST1 to an entity in scenario type ST2 are declared by including them in the JDL file that defines ST1. In addition, visibility on scenario type ST2 must be declared by adding an import
statement in the scenarioType
block for ST1.
@Description("Data that yields a new plan") scenarioType TransactionalData { import MasterData }
The above allows the JDL file that contains this block to declare relations from entities of TransactionalData
to entities of MasterData
.
No cycles are allowed in the graph of scenario type imports and an import
statement can only refer to a scenario type that is defined in one of the JDL files mentioned in the include
statements of the application
block.
Usually, an application model evolves with new versions. Therefore, the database schema has to follow the model definition. This implies data structures and data need to be migrated. For this use case, we rely on the standard tool Flyway.
Flyway keeps trace of the current version of your schema. This allows Flyway to incrementally execute all the migration scripts from your current database schema version to the target schema version.
![]() |
In this example, your database is already upgraded with a schema in version 1, if you run Flyway, it will migrate your database schema to version 2.
To achieve migrations, Flyway uses so-called migration scripts. Migration scripts are "sequentially ordered SQL scripts" that incrementally migrate your schema.
In order to recognize migration scripts, Flyway relies on the following naming convention:
![]() |
An example of migration script folder could be:
gene-model/gene-model-jpa ~> tree -L 1 flyway/db/migration flyway/db/migration - v1__Add_item_table.sql # First migration script that adds an 'Activity' table in the SQL schema. - v2__Add_item_category_table.sql # Migration script that creates an activity table.
By this naming convention Flyway is able to determine that it has to run v2__Add_item_category_table.sql
after v1__Add_item_table.sql
and only if it has not already been run.
Let's take an example,
Imagine you already have a model with an Item table, and you want to introduce a Category table like in the following diagram.
![]() |
Note that, in your migration scripts, you will have to migrate the schema AND the existing data. |
See below the migration script that will allow us to upgrade V1 schema and data to V2.
It adds a category
table and updates the item
table accordingly. It also creates a default category and updates the existing Items with it.
-- v2__Add_category_table.sql -- This migration script introduces item categories. -- -- ################# -- # Category # -- ################# -- Introduce category table create table category ( category_id text not null constraint category_pkey primary key, name text not null ); alter table category owner to data_server; create unique index uk_p8a7ixxjnxsm18wq9f1eobrmn on category (category_id); create unique index idx_tdsi66b2e619y4cuympjbrfw7 on category (name); -- Create a default category insert into category (category_id, name) values ('default_category', 'Default'); -- ################# -- # Item # -- ################# -- Update item table alter table item add column category_id text constraint fk_1tgju5f47333ash49oqmv8vm2 references category deferrable; -- Very important we migrate the data to (not only the structure). - -- ------------------------------------------------------------------ -- Update the items accordingly update item set category_id = 'default_category' where 1 = 1;
The following diagram shows you how to integrate a model in a DOC
application.
![]() |
* The migration script generation gradle command is: ./gradlew :gene-model:gene-model-jpa:generateJPAForce
You can see that depending on if you already have a model definition (jdl file) or not;
You will have to launch a gradle task to create a new version in migration scripts and ensure to make the script incremental. The script generated when you edit your model is placed in the following location: gene-model/gene-model-jpa/flyway/db/migration/
and is called: V1.0.000__CREATE_TABLES_AND_CONSTRAINTS.sql
.
For example, if I have created an application V1 and now I want to update the JDL with some modification of my data model V2 (e.g. add the category table) without losing the existing data.
I need to update the JDL file to implement my new model (V2).
I create a flyway migration script "V2.0.0.xxxx.sql" under the folder gene-model/gene-model-jpa/flyway/db/migration/
that will contain the SQL code required to update the schema accordingly to the new model and migrate the existing data (V1).
I can rebuild and restart the application with the new model.
Note that the V1xxxx SQL script is generated only if the migration is empty; To force the regeneration of the V1xxx SQL Script you can use |
There are multiple cases where the migration of scenarios is not required:
during development phases,
when the targeted application relies on scenarios as volatile/short term data that does not need to be kept on the long term.
In those cases, developers may launch ./gradlew generateJPAForce
after each model change and keep the "all in one" migration script.
In that case, to properly boot the application, the Data Service database must be emptied. A way to achieve this can be to delete the corresponding Docker volumes.
Warning: Since Keycloak and the Data Service share the same database engine, deleting the volumes will delete the Keycloak configurations. For more details on how to save and restore this configuration, refer to Section Importing and Exporting User Data. |