Cached parameter tables

It's very useful to have parameter tables, keeping configuration and business rules outside of the code provides meaningful separation of concerns, whilst also affording adaptability. In EBX, it's common practice to create a parameters dataset where tables are created to configure the environment. There is a clear downside to this approach: EBX tables are slow to access and annoying to work with. This is where cached parameter tables come in.

Cached parameter tables are nothing more than a parameter table with a cached access layer on top. The cache is initialized on repository startup using the module's register servlet and kept up to date during execution using a table trigger.

Example

As an example, let's build a cached parameter table for a keyd flags. This table must store key-value pairs, where the keys are strings that indicate which flag each row references and the values are booleans that indicate whether the flag is set or not. A row of this table that controls whether the username should be displayed in the navigation bar could look like this: ("DISPLAY_USERNAME", true).

This is the implementation of the cached parameter table for the keyd flags:

(Parameters.xsd)
<xs:complexType name="SystemFlags_Type">
    <xs:annotation>
        <xs:appinfo>
            <osd:table>
                <primaryKeys>/flag</primaryKeys>
            </osd:table>
            <osd:trigger class="com.example.trigger.SystemFlagTableTrigger" />
        </xs:appinfo>
    </xs:annotation>
    <xs:sequence>
        <xs:element name="flag" type="xs:string" />
        <xs:element name="value" type="xs:boolean" minOccurs="0" default="false" />
    </xs:sequence>
</xs:complexType>

(SystemFlag.java)
public enum SystemFlag {
    /**
     * Whether to display the username in the navigation bar.
     */
    DISPLAY_USERNAME("DISPLAY_USERNAME"),

    /**
     * Whether to display the user's initials in the navigation bar.
     */
    DISPLAY_INITIALS("DISPLAY_INITIALS"),
; private final String id; SystemFlag(String id) { this.id = id; } public static SystemFlag fromId(String systemFlag) { for (SystemFlag flag : values()) { if (StringUtils.equals(systemFlag, flag.id)) { return flag; } } return null; } public String getId() { return id; } }

(SystemFlagTableTrigger.java)
public class SystemFlagTableTrigger extends TableTrigger {
    private static final EnumSet<SystemFlag> FLAGS = EnumSet.noneOf(SystemFlag.class);

    @Override
    public void setup(TriggerSetupContext triggerSetupContext)
    {
        // Not used
    }

    @Override
    public void handleAfterCreate(AfterCreateOccurrenceContext context) throws OperationException
    {
        ValueContext valueContext = context.getOccurrenceContext();
        String flagId = FRecordUtils.getNonNull(valueContext, ParametersPath._SystemFlags._Flag);
SystemFlag flag = SystemFlag.fromId(flagId); if (flag == null) { return; } boolean flagValue = BooleanUtils.isTrue(FRecordUtils.get(valueContext, ParametersPath._SystemFlags._Value));
if (flagValue) { FLAGS.add(flag); } else { FLAGS.remove(flag); } } @Override public void handleAfterModify(AfterModifyOccurrenceContext context) throws OperationException { ValueContext valueContext = context.getOccurrenceContext(); String flagId = FRecordUtils.getNonNull(valueContext, ParametersPath._SystemFlags._Flag); SystemFlag flag = SystemFlag.fromId(flagId); if (flag == null) { return; } boolean flagValue = BooleanUtils.isTrue(FRecordUtils.get(valueContext, ParametersPath._SystemFlags._Value)); if (flagValue) { FLAGS.add(flag); } else { FLAGS.remove(flag); } } @Override public void handleAfterDelete(AfterDeleteOccurrenceContext context) throws OperationException { ValueContext valueContext = context.getOccurrenceContext(); String flagId = FRecordUtils.getNonNull(valueContext, ParametersPath._SystemFlags._Flag); SystemFlag flag = SystemFlag.fromId(flagId); if (flag == null) { return; } FLAGS.remove(flag); } public static boolean isFlagSet(SystemFlag systemFlag) { return FLAGS.contains(systemFlag); } public static void init(@NotNull Repository repository) throws OperationException { Session session = SessionImpl.createSessionForSystem(repository); AdaptationHome parametersDataspace = repository.lookupHome(HomeKeys.PARAMETERS); if (parametersDataspace == null) { return; } ProgrammaticService programmaticService = ProgrammaticService.createForSession(session, parametersDataspace); ProcedureResult result = programmaticService.execute(new InitializationProcedure()); if (result.hasFailed()) { throw result.getException(); } } private record InitializationProcedure() implements Procedure { @Override public void execute(ProcedureContext procedureContext) throws Exception { AdaptationHome dataspace = procedureContext.getAdaptationHome(); Adaptation dataset = dataspace.findAdaptationOrNull(AdaptationNames.PARAMETERS); AdaptationTable systemFlagsTable = dataset.getTable(ParametersPath._SystemFlags.getPathInSchema()); for (SystemFlag systemFlag : SystemFlag.values()) { PrimaryKey primaryKey = PrimaryKey.parseString(systemFlag.getId()); Adaptation recordAdaptation = systemFlagsTable.lookupAdaptationByPrimaryKey(primaryKey); if (recordAdaptation != null) { boolean flagValue = BooleanUtils.isTrue(FRecordUtils.get(recordAdaptation, ParametersPath._SystemFlags._Value)); if (flagValue) { FLAGS.add(systemFlag); } else { FLAGS.remove(systemFlag); } } else { ValueContextForUpdate valueContext = procedureContext.getContextForNewOccurrence(systemFlagsTable); valueContext.setValueEnablingPrivilegeForNode(systemFlag.getId(), ParametersPath._SystemFlags._Flag); valueContext.setValueEnablingPrivilegeForNode(false, ParametersPath._SystemFlags._Value); procedureContext.doCreateOccurrence(valueContext, systemFlagsTable); } } } } }

(RegisterServlet.java)
public class RegisterServlet extends ModuleRegistrationServlet {
    @Override
    public void handleRepositoryStartup(ModuleContextOnRepositoryStartup context) throws OperationException {
        try {
            SystemFlagTableTrigger.init(repository);
        }
        catch (OperationException e) {
            LoggingCategory.getKernel().error("Could not initialize system flags", e);
        }
    }
}

Pros and cons

This approach improves access speed and removes the requirement of acessing the repository during access of the parameters. On the other hand, it copies the database data to the memory, for large and complex parameter tables, it's important to consider the memory footprint of the implementation. Also, the usage of triggers is not without issues, there are a few ways to bypass table trigger on EBX, invalidating the cached values.

Comments