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:

  1. Commit all your existing code
  2. Edit the domain metadata file
  3. Edit all the related entities metadata files
  4. Execute the generator
  5. (a) Clean the database OR (b) Refactor the generated liquibase changeset
  6. Revert eventual unwanted overrides
  7. 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.

part6-original-model

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 part6-original-travel-plan-list-page

Travel Plan details page
part6-travel-plan-details-page

Travel Plan Start Form page part6-travel-plan-start-form-page-1-original

Travel Plan Process details page part6-process-instance-details-page-1-original

User Task - Flight page part6-user-task-page-1-original

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) part6-travel-plan-start-form-page-2-custom

Travel Plan Process details page (customized) part6-process-instance-details-page-2-custom

User Task - Flight page (customized) part6-user-task-page-2-custom

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.

part6-modified-model

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
}

~/.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:

part6-changesets-diff

In summary:

  • The name column was renamed to travel_name
  • The user_name and user_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 part6-travel-plan-start-form-page-3-regenerated

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.

part6-travel-plan-start-form-page-4-regenerated-custom