omnifaces / omnipersistence

Utilities for JPA, JDBC and DataSources
Other
31 stars 12 forks source link

Added functionality for enhanced enum-to-database mapping via @EnumMappingTable, minor fixes. #14

Closed skuntsel closed 6 years ago

skuntsel commented 6 years ago

This update further enhances functionality of @EnumMapping by providing more strict correspondence between database table data and java enum data. To enable this functionality a nested annotation in @EnumMapping must be specified, @EnumMappingTable.

@EnumMapping changes the default mechanism of persisting enums, making persistence of enums steadier and more controllable. However, it doesn't offer any functionality to have a mapping to a database table that holds all of the possible values of an enum. With @EnumMappingTable it is possible to have a database table that will serve either as a source of enum values, that will be incorporated by the application, or as a collection of all currently possible enum values used in the application, with a possibility to track historical values of previously used enums with either option. This is achieved by enforcing the established correspondence mapping between java enum and database table representations.

The first example illustrates how to keep the enum id-code values in-sync from the java enum. It doesn't need to create any database tables in advance, as the source of information will come from the java enum and its modifications upon restart. So, for the following enum:

@EnumMapping(enumMappingTable = @EnumMappingTable(mappingType = EnumMappingTable.MappingType.ENUM, 
oneFieldMapping = false, deleteType = EnumMappingTable.DeleteAction.SOFT_DELETE))
public enum UserRole {

    USER(1, "USR"), EMPLOYEE(2, "EMP"), MANAGER(3, "MGR");

    private String code;
    private int id;

    private UserRole(int id, String code) {
        this.id = id;
        this.code = code;
    }

    public String getCode() {
        return code;
    }

    public int getId() {
        return id;
    }

}

a table will be generated with the following content (an existing table can also be used):

CREATE TABLE user_role_info (id INT NOT NULL, code VARCHAR(32) NOT NULL,
PRIMARY KEY (id), CONSTRAINT user_role_info_code_UNIQUE UNIQUE (code));
INSERT INTO user_role_info (id, code) values (1, 'USR'), (2, 'EMP'), (3, 'MGR');

If at one moment the enum will be modified and the EMPLOYEE enum constant will be removed with two new constants added to the following:

@EnumMapping(enumMappingTable = @EnumMappingTable(mappingType = EnumMappingTable.MappingType.ENUM,
oneFieldMapping = false, deleteType = EnumMappingTable.DeleteAction.SOFT_DELETE))
public enum UserRole {

    USER(1, "USR"), MANAGER(3, "MGR"), PART_TIME_EMPLOYEE(4, "PTE"), FULL_TIME_EMPLOYEE(5, "FTE");
    // Old values: EMPLOYEE(2, "EMP")
    ...

}

then the history table, user_role_info_history, will be updated with the 2, 'EMP' values and those values will be removed from the user_role_info table with two new values added. Unique key updates will also be performed if unique key changes, i.e. if EMPLOYEE(2, "EMP") is changed at some point to EMPLOYEE(2, "EMPL") then these changes will be reflected in the table as well. Note that it is a developer responsibility not to introduce colliding primary/unique keys so that the mapping can be performed successfully.

Another example is a database-driven enum. All possible values are declared in a database table, possibly keeping unused values with a soft deleted flag. The application will keep the java enums in line with the database table and update the enum class accordingly. It will also add currently declared in enum class constants, but absent in the data store, as soft deleted rows. In this case a database table must exist beforehand and could have the following structure:

CREATE TABLE user_role_info (id INT NOT NULL, code VARCHAR(32) NOT NULL,
deleted INT DEFAULT 0 NOT NULL, PRIMARY KEY (code), CONSTRAINT user_role_info_code_UNIQUE UNIQUE  (id, deleted));
INSERT INTO user_role_info (id, code, deleted) values (1, 'USR', 0), (2, 'EMP', 0), (3, 'MGR', 0);

The mapping will not be performed if the database table doesn't exist at application startup. With the following table and the configured annotation shown below the enum, no matter what values it had at compile time, will have the values corresponding to the table data. So, for the enum:

@EnumMapping(type = EnumType.STRING, enumMappingTable = @EnumMappingTable(mappingType = EnumMappingTable.MappingType.TABLE,
oneFieldMapping = false, deleteType = EnumMappingTable.DeleteAction.SOFT_DELETE))
public enum UserRole {

    USER(6, "USR"), EMPLOYEE(7, "EMP"), MANAGER(8, "MGR");

    private String code;
    private int id;

    private UserRole() {} // Note that this no-argument constructor is obligatory with this setup.

    private UserRole(int id, String code) {
        this.id = id;
        this.code = code;
    }

    public String getCode() {
        return code;
    }

    public int getId() {
        return id;
    }

}

the identifiers of enums will be replaced with the data in the table, i.e. 1, 2, 3 accordingly. If at one moment the table will be modified and two new rows are added, i.e. (4, 'PTE', 0), (5, 'FTE', 0), and one row with id 'EMP' is hard-deleted, then at application startup the enum constants will be replaced with the ones essentially equal to USER(1, "USR"), MANAGER(3, "MGR") (the existing ones) and NEW_CONSTANT_1(4, "PTE"), NEW_CONSTANT_2(5, "FTE") (the new ones). The value EMPLOYEE(7, "EMP") will be removed and a soft-deleted row (7, 'EMP', 1) will be added to the database. Note that it would be still possible to refer to the existing enum constants via the traditional methods, i.e. UserRole.USER, but the deleted constant will be unavailable, i.e. UserRole.EMPLOYEE will hold null value, and the freshly added ones will be unavailable via static fields, but only via UserRole.values() call, or UserRole.valueOf("PTE") call, and the values will hold correct set of enum constants. Also note that it is a developer responsibility not to introduce tables with colliding primary/unique keys so that the mapping can be performed successfully.

A monor update is added as well. Now the defult value for EnumMapping#fieldName is introduced. The fieldName defaults to "id" when EnumMapping#type evaluates to EnumType#ORDINAL and to "code" when EnumMapping#type evaluates to EnumType#STRING.

BalusC commented 6 years ago

Sorry for the late response, I was having holidays in Bahamas and then migrated back from The Netherlands to Curaçao. I'm now finally settled and I will review the changes this week. These look at least good at first glance.