Updating the domain entity
In this tutorial, we will discuss how to update domain entities. By updating here we mean (a) add new fields/relationships and (b) remove or change existing fields/relationships. This kind of updating is a quite common situation during the developing of applications and we will present here an alternative to handle it.
The steps to update a domain entity are:
- Commit all your existing code
- Edit the domain metadata file
- Edit all the related entities metadata files
- Execute the generator
- (a) Clean the database OR (b) Refactor the generated liquibase changeset
- Revert eventual unwanted overrides
- Restart the application
We will demonstrate using the Travel Plan Process how to proceed updated in a domain entity.
Original Code for the Travel Plan Process
In the original model, the TravelPlan entity has the fields name, startDate, endDate, airflightCompanyName, airflightFlightNumber, hotelName, hotelBookingNumber, rentalCarCompanyName, rentalCarBookingNumber, and price.
The TravelPlan metadata is shown below:
{
"travelPlanApp": {
"travelPlan": {
"home": {
"title": "Travel Plans",
"refreshListLabel": "Refresh list",
"notFound": "No Travel Plans found"
},
"detail": {
"title": "Travel Plan"
},
"id": "ID",
"name": "Name",
"startDate": "Start Date",
"endDate": "End Date",
"airlineCompanyName": "Airline Company Name",
"airlineTicketNumber": "Airline Ticket Number",
"hotelName": "Hotel Name",
"hotelBookingNumber": "Hotel Booking Number",
"carCompanyName": "Car Company Name",
"carBookingNumber": "Car Booking Number"
}
}
}
This metadata file among with the other entities metadata files are used by the generator to scaffold the application and after generating the code for all entities (domain, process-binding, start-form, and user-task), the Travel Plan Process is ready to be started (see the Getting Started for more details on how to generate all the code for a given process). Some of the corresponding pages for this process are shown below:
Travel Plan list page
Travel Plan details page
Travel Plan Start Form page
Travel Plan Process details page
User Task - Flight page
Note: You can checkout the tag part6-updating-domain-entities-original-model from the TravelPlan repository on Github to get the code generated on this step. git checkout part6-updating-domain-entities-original-model
.
Customizations
All the pages above were completely generated by the AgileKip Generator so there is no customization applied yet.
In the last three pages (Start Form, Process Instance Details, User Task), for example, the startDate
and endDate
fields are placed one above the other.
Let’s make a simple customization changing these pages and placing the startDate
and endDate
fields side-by-side:
Travel Plan Start Form page (customized)
Travel Plan Process details page (customized)
User Task - Flight page (customized)
Note: You can checkout the tag part6-updating-domain-entities-original-model-custom-layout from the TravelPlan repository on Github to get the code generated on this step. git checkout part6-updating-domain-entities-original-model-custom-layout
.
Remember we don’t want to lose these customizations after updating the domain entity.
Changes in the domain entity
To keep things simpler, let’s make only a few changes into the original model:
- the name field was renamed to travelPlanName;
- the userName and userEmail fields were added.
To implement these changings, we will follow the 6 steps described above:
Step 1. Commit all your existing code
It’s really important that you commit all changes in your workspace before continue. Commiting your workspace will guarantee that you can revert eventual unwanted changes you have made.
Step 2. Edit the domain metadata file
Change the Travel Plan entity metadata:
~/.jhipster/TravelPlan.json
{
"fields": [
{
"fieldName": "travelPlanName",
"fieldType": "String"
},
{
"fieldName": "userName",
"fieldType": "String"
},
{
"fieldName": "userEmail",
"fieldType": "String"
},
{
"fieldName": "startDate",
"fieldType": "LocalDate"
},
{
"fieldName": "endDate",
"fieldType": "LocalDate"
},
{
"fieldName": "airlineCompanyName",
"fieldType": "String"
},
{
"fieldName": "airlineTicketNumber",
"fieldType": "String"
},
{
"fieldName": "hotelName",
"fieldType": "String"
},
{
"fieldName": "hotelBookingNumber",
"fieldType": "String"
},
{
"fieldName": "carCompanyName",
"fieldType": "String"
},
{
"fieldName": "carBookingNumber",
"fieldType": "String"
}
],
"relationships": [],
"entityType": "domain",
"service": "serviceClass",
"dto": "mapstruct",
"jpaMetamodelFiltering": false,
"readOnly": true,
"pagination": "no",
"name": "TravelPlan",
"changelogDate": "20210401000001",
"skipFakeData": true
}
Step 3. Edit all the related entities metadata files
~/.jhipster/TravelPlanProcess.json
{
"fields": [
{
"fieldName": "travelPlanName",
"fieldType": "String"
},
{
"fieldName": "userName",
"fieldType": "String"
},
{
"fieldName": "userEmail",
"fieldType": "String"
},
{
"fieldName": "startDate",
"fieldType": "LocalDate"
},
{
"fieldName": "endDate",
"fieldType": "LocalDate"
},
{
"fieldName": "airlineCompanyName",
"fieldType": "String"
},
{
"fieldName": "airlineTicketNumber",
"fieldType": "String"
},
{
"fieldName": "hotelName",
"fieldType": "String"
},
{
"fieldName": "hotelBookingNumber",
"fieldType": "String"
},
{
"fieldName": "carCompanyName",
"fieldType": "String"
},
{
"fieldName": "carBookingNumber",
"fieldType": "String"
}
],
"relationships": [],
"entityType": "process-binding",
"processBpmnId": "TravelPlanProcess",
"domainEntityName": "TravelPlan",
"service": "serviceClass",
"dto": "mapstruct",
"jpaMetamodelFiltering": false,
"readOnly": false,
"pagination": "no",
"name": "TravelPlanProcess",
"changelogDate": "20210401000002",
"skipFakeData": true
}
~/.jhipster/TravelPlanProcessStartForm.json
{
"fields": [
{
"fieldName": "travelPlanName",
"fieldType": "String"
},
{
"fieldName": "userName",
"fieldType": "String"
},
{
"fieldName": "userEmail",
"fieldType": "String"
},
{
"fieldName": "startDate",
"fieldType": "LocalDate"
},
{
"fieldName": "endDate",
"fieldType": "LocalDate"
}
],
"relationships": [],
"entityType": "start-form",
"processBpmnId": "TravelPlanProcess",
"processEntityName": "TravelPlanProcess",
"domainEntityName": "TravelPlan",
"service": "serviceClass",
"dto": "mapstruct",
"jpaMetamodelFiltering": false,
"readOnly": false,
"pagination": "no",
"name": "TravelPlanStartForm",
"changelogDate": "20210401000002",
"skipFakeData": true
}
~/.jhipster/TaskFlight.json
{
"fields": [
{
"fieldName": "travelPlanName",
"fieldType": "String",
"fieldReadOnly": true
},
{
"fieldName": "userName",
"fieldType": "String",
"fieldReadOnly": true
},
{
"fieldName": "userEmail",
"fieldType": "String",
"fieldReadOnly": true
},
{
"fieldName": "startDate",
"fieldType": "LocalDate",
"fieldReadOnly": true
},
{
"fieldName": "endDate",
"fieldType": "LocalDate",
"fieldReadOnly": true
},
{
"fieldName": "airlineCompanyName",
"fieldType": "String"
},
{
"fieldName": "airlineTicketNumber",
"fieldType": "String"
}
],
"relationships": [],
"entityType": "user-task-form",
"processBpmnId": "TravelPlanProcess",
"processEntityName": "TravelPlanProcess",
"taskBpmnId": "TaskFlight",
"domainEntityName": "TravelPlan",
"service": "serviceClass",
"dto": "mapstruct",
"jpaMetamodelFiltering": false,
"readOnly": false,
"pagination": "no",
"skipFakeData": true,
"name": "TaskFlight",
"changelogDate": "20210401000004"
}
~/.jhipster/TaskHotel.json
{
"fields": [
{
"fieldName": "travelPlanName",
"fieldType": "String",
"fieldReadOnly": true
},
{
"fieldName": "userName",
"fieldType": "String",
"fieldReadOnly": true
},
{
"fieldName": "userEmail",
"fieldType": "String",
"fieldReadOnly": true
},
{
"fieldName": "startDate",
"fieldType": "LocalDate",
"fieldReadOnly": true
},
{
"fieldName": "endDate",
"fieldType": "LocalDate",
"fieldReadOnly": true
},
{
"fieldName": "hotelName",
"fieldType": "String"
},
{
"fieldName": "hotelBookingNumber",
"fieldType": "String"
}
],
"relationships": [],
"entityType": "user-task-form",
"processBpmnId": "TravelPlanProcess",
"processEntityName": "TravelPlanProcess",
"taskBpmnId": "TaskHotel",
"domainEntityName": "TravelPlan",
"service": "serviceClass",
"dto": "mapstruct",
"jpaMetamodelFiltering": false,
"readOnly": false,
"pagination": "no",
"skipFakeData": true,
"name": "TaskHotel",
"changelogDate": "20210401000003"
}
~/.jhipster/TaskCar.json
{
"fields": [
{
"fieldName": "travelPlanName",
"fieldType": "String",
"fieldReadOnly": true
},
{
"fieldName": "userName",
"fieldType": "String",
"fieldReadOnly": true
},
{
"fieldName": "userEmail",
"fieldType": "String",
"fieldReadOnly": true
},
{
"fieldName": "startDate",
"fieldType": "LocalDate",
"fieldReadOnly": true
},
{
"fieldName": "endDate",
"fieldType": "LocalDate",
"fieldReadOnly": true
},
{
"fieldName": "carCompanyName",
"fieldType": "String"
},
{
"fieldName": "carBookingNumber",
"fieldType": "String"
}
],
"relationships": [],
"entityType": "user-task-form",
"processBpmnId": "TravelPlanProcess",
"processEntityName": "TravelPlanProcess",
"taskBpmnId": "TaskCar",
"domainEntityName": "TravelPlan",
"service": "serviceClass",
"dto": "mapstruct",
"jpaMetamodelFiltering": false,
"readOnly": false,
"pagination": "no",
"skipFakeData": true,
"name": "TaskCar",
"changelogDate": "20210401000005"
}
Step 4. Execute the generator
$>jhipster entity TravelPlan --regenerate
The generator will modify many existing files and for each one, it will ask you for confirmation.
You should say y
(yes) to all confirmation questions.
In the end of the generation process, all the files for this new version of the model were generated but before verifying the changes, you need to follow the next step.
Step 5. (a) Clean the database OR (b) Refactor the generated liquibase changeset
If you try to start the application you will get this error:
2021-11-19 02:36:57.243 DEBUG 38373 --- [ restartedMain] c.m.myapp.config.LiquibaseConfiguration : Configuring Liquibase
2021-11-19 02:36:57.246 WARN 38373 --- [vel-plan-task-1] t.j.c.liquibase.AsyncSpringLiquibase : Starting Liquibase asynchronously, your database might not be ready at startup!
2021-11-19 02:36:57.431 ERROR 38373 --- [vel-plan-task-1] t.j.c.liquibase.AsyncSpringLiquibase : Liquibase could not start correctly, your database is NOT ready: Validation Failed:
1 change sets check sum
config/liquibase/changelog/20210401000001_added_entity_TravelPlan.xml::20210401000001-1::jhipster was: 8:00cc6c359219c0fe4407af8f85132c0b but is now: 8:0968eee3556708cb40f73d1ab02c9602
liquibase.exception.ValidationFailedException: Validation Failed:
1 change sets check sum
config/liquibase/changelog/20210401000001_added_entity_TravelPlan.xml::20210401000001-1::jhipster was: 8:00cc6c359219c0fe4407af8f85132c0b but is now: 8:0968eee3556708cb40f73d1ab02c9602
at liquibase.changelog.DatabaseChangeLog.validate(DatabaseChangeLog.java:299)
at liquibase.Liquibase.lambda$update$1(Liquibase.java:237)
This is a quite trick issue and in order to understand it you have to know a little bit about Liquibase. Liquibase is a tool for managing the creation of database objects such as tables, sequences, index, and data. It is mainly used to create tables, populate tables, modify tables, drop tables, etc. Each database object is created through a script called changeset. Once Liquibase executes a changeset, the script cannot be modified. The problem here comes from the fact that the regeneration process touched changesets that have already been executed. So, Liquibase raises an error indicating that it is not possible to change them. There are two alternatives to solve this issue: (a) a very simple solution and (b) a more complex solution.
Solution A - Clean the database:
The simplest solution to this problem is to clean the entire database.
If you are in dev mode, you just need to delete the file under the directory target/h2db/db/
.
The downside to this strategy is that by deleting these files, you will discard any data that eventually have been input into the app. So, this strategy can be only used in dev mode where data can be discarded. For production mode (or any situation you cannot discard data) you should use the second strategy.
Solution B - Refactor the generated liquibase changeset
The second strategy requires investigating the changes made by the generator in the original changeset.
The image below shown the changes in the file
src/main/resources/config/liquibase/changelog/YYYYMMDD000001_added_entity_TravelPlan.xml
:
In summary:
- The
name
column was renamed totravel_name
- The
user_name
anduser_smail
columns were added
To refactor the regenerated liquibase changeset, we need to revert the changes made in the original changeset and create manually a new changeset with the required changes. After refactoring the changesets, the resulting file would be something like the code below:
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<!--
Added the entity TravelPlan.
-->
<changeSet id="20210401000001-1" author="jhipster">
<createTable tableName="travel_plan">
<column name="id" type="bigint">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="start_date" type="date">
<constraints nullable="true" />
</column>
<column name="end_date" type="date">
<constraints nullable="true" />
</column>
<column name="airline_company_name" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="airline_ticket_number" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="hotel_name" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="hotel_booking_number" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="car_company_name" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="car_booking_number" type="varchar(255)">
<constraints nullable="true" />
</column>
<!-- jhipster-needle-liquibase-add-column - JHipster will add columns here -->
</createTable>
</changeSet>
<changeSet id="20210401000001-2" author="jhipster">
<renameColumn newColumnName="travel_plan_name"
oldColumnName="name"
tableName="travel_plan"/>
<addColumn tableName="travel_plan">
<column name="user_name" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="user_email" type="varchar(255)">
<constraints nullable="true" />
</column>
</addColumn>
</changeSet>
<!-- jhipster-needle-liquibase-add-changeset - JHipster will add changesets here -->
</databaseChangeLog>
The second changeset (id="20210401000001-2
) has a renameColumn
element to rename the name
column and an addColumn
element to add the user_name
and user_email
columns.
After executing one of the two solutions (A ou B), you will be able to restart the application.
5. Revert eventual unwanted overrides
One of the side effects of the code regenaration is that the generator overrides all the customizations you have made in the code. Let’s see, for example, the layout of the start form page for this process.
Travel Plan Start Form page
Note the customizations were overridden by the generator: the startDate
and endDate
fields were placed one above the other and not side-by-side. The same happened to the Travel Plan Process Instance Details and User Task Flight pages.
In this step, you have to manually reapply the desired customizations that were overridden.
For example, after reapplying the customizations, the image below shows the start form after placing the startDate
and endDate
fields side-by-side.