Introduction and Features

Why another utility or component library?

FlowLogix provides a few remaining missing pieces in what is covered by Jakarta EE, PrimeFaces, OmniFaces, Apache Shiro and other very popular software.

What are the features provided?

FlowLogix Jakarta EE library fills in the last few gaps that are left over in PrimeFaces, OmniFaces and Jakarta EE itself, including:

  • Provides automatic Data Access for JPA with delegation and without inheritance

  • Adds Type-safe Native SQL query with JPA via generics

  • Declares Jakarta Faces PROJECT_STAGE development mode automatically

  • Automatically uses minified versions of assets with Jakarta Faces

  • Provides easy and automatic initialization of OmniFaces' UnmappedResourceHandler

  • Easier-to-use, Injected JPA Lazy Data Model for PrimeFaces DataTable that supports clustered sessions

  • Automatically includes PrimeFaces font mime types in your web applications, preventing warnings and extra entries in your web.xml

  • Convert strings to arbitrary types on a best-effort basis

  • Transforms names between javax and jakarta namespaces

  • Checks if objects are truly serializable by testing them

  • Easy Transform Streams to Strings

  • Simplify creation and manipulation of ShrinkWrap and Arquillian test archives including assets

Where does FlowLogix fit into your application architecture?

Is it a framework?

No. FlowLogix fits within the Jakarta EE design philosophy and works with MicroProfile, Jakarta EE, OmniFaces and PrimeFaces ecosystem. FlowLogix tries to be the least intrusive, automatic and with the fewest requirements possible.

What are the design principles?

Simplicity is the #1 principal. FlowLogix doesn’t make you inherit from base classes or interfaces, unless absolutely necessary. Annotations are also used sparingly, being careful not to introduce any unnecessary cognitive load and complexity.

Project Lombok is mentioned a lot. Is it required?

No. Although the library itself uses Lombok to avoid boilerplate code, it is completely optional to the user, and is not required to be a dependency of user code. Having said that, even though Java itself currently has lots of features only available with Lombok in prior years, Lombok still has many features not available in Java today, and is very useful and highly recommended.

Installation and Compatibility

Compatibility

FlowLogix 5.x is compatible with Java 11+ and Java EE 8, Jakarta EE 8 and later. Jakarta EE 9 and later (jakarta.* namespace) is available via the jakarta maven classifier.
FlowLogix 6.x and later is compatible with Java 17+ and Jakarta EE 9 and later. No classifier is required.

Installation

Artifacts are available in the Sonatype’s Maven Central repository. All snapshots are available in the Maven Central snapshot repository

Jakarta EE Components - Maven Example
<dependencies>
    <dependency>
        <groupId>com.flowlogix</groupId>
        <artifactId>flowlogix-jee</artifactId>
        <version>9.0.7</version>
    </dependency>
</dependencies>
PrimeFaces JPA LazyDataModel - Maven Example
<dependencies>
    <dependency>
        <groupId>com.flowlogix</groupId>
        <artifactId>flowlogix-datamodel</artifactId>
        <version>9.0.7</version>
    </dependency>
</dependencies>

Flow Logix BOM

Flow Logix includes an easy way to declare all the required dependency versions, via the Maven Bill of Materials.

Example
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.flowlogix</groupId>
            <artifactId>flowlogix-bom</artifactId>
            <version>9.0.7</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

See the Maven Documentation for more information on BOMs

Developer’s Guide

FlowLogix Jakarta EE Components

JPAFinder for JPA: Delegation-based Composable Queries for Data Access Objects and business logic

Enhanced Finder and count methods

findAll(), findRange() and count() take an optional argument to enhance the queries, which can add additional JPA clauses, enhancements and hints. This parameter takes a form of QueryEnhancement interface, which extends BiConsumer interface. There are a number of accept convenience methods in QueryEnhancement interface that can be passed via a method reference. This enables composition of different QueryEnhancement instances.

Enhanced query and hints example
public record CountAndList(long count, List<UserEntity> list) { };
public CountAndList countAndList(String userName) {
    // add "where fullName = 'userName'" clause
    QueryEnhancement<UserEntity> enhancement = (partial, criteria) -> criteria
            .where(partial.builder().equal(partial.root()
                    .get(UserEntity_.fullName), userName));

    return new CountAndList(jpaFinder.count(enhancement::accept),
            jpaFinder.findAll(enhancement::accept)
            .setHint(QueryHints.BATCH_TYPE, BatchFetchType.IN)
            .getResultList());
}
Composable query with customized parameters
public CountAndList extractedCountAndList(String userName) {
    // add "where fullName = 'userName'" clause
    QueryEnhancement<UserEntity> enhancement = (partial, criteria) -> criteria
            .where(partial.builder().equal(partial.root()
                    .get(UserEntity_.fullName), userName));
    // descending order for queries
    QueryEnhancement<UserEntity> orderBy = (partial, criteria) -> criteria
            .orderBy(partial.builder().desc(partial.root().get(UserEntity_.fullName)));

    return new CountAndList(jpaFinder.count(enhancement::accept),
            jpaFinder.findAll(enhancement.andThen(orderBy)::accept)
            .getResultList());
}

There are many ways to implement the Data-Access-Object pattern in Java. Most of them require to implement an interface, have some kind of bytecode generation magic or inherit from a base class. FlowLogix takes a different approach. The amount of magic is totally up to the developers. If none of the below approaches work, your DAO can simply inherit from InheritableDaoHelper, and initialize the jpaFinder protected field in your @PostConstruct method.

You can leverage Project Lombok @Delegate annotation to transparently implement Data Access Objects without any specific requirements. Alternatively, developers can write manual forwarder methods, dynamic proxies, or other bytecode generators to delegate to JPAFinder from an interface.

Recommended approach is to use Lombok’s @Delegate annotation. This introduces the fewest amount of magic, results in the least amount of code, and absolute minimum (if any) boilerplate.

JPAFinder can also be @Inject ed and it will infer the EntityManager via CDI. If default CDI producer for the EntityManager is insufficient, @EntityManagerSelector annotation can be used to specify qualifiers for non-default EntityManager producer

JPAFinder is Serializable and thus can be used inside @ViewScoped beans, for example.

Fluent Builder pattern can be used to create DaoHelper object, which requires EntityManager Supplier and entity Class.

Why does DaoHelper take Supplier as a parameter instead of EntityManager directly?

That’s Because EntityManager is not initialized when the object is created. Supplier lets you delay the initialization of DaoHelper until EntityManager is actually needed at run-time and is already initialized. This is why InheritableDAOHelper needs to be initialized in @PostConstruct method instead of the constructor.

Where are find() and related methods?

These methods are built into the EntityManager and can be accessed that way (see the "more complete" example below). There is no need for JPAFinder to provide them. If more functionality related to find() is desired, Jakarta Data or Apache DeltaSpike project is a wonderful addition to your architecture and will satisfy those needs.

Simple DAO Example
@Stateless
public class ExampleDAO {
    @PersistenceContext(unitName = "demo-pu")
    EntityManager em;
    @Delegate
    JPAFinder<UserEntity> jpaFinder = new DaoHelper<>(() -> em, UserEntity.class);
}
Injected DAO example
@Stateless
public class InjectedDAO {
    @Inject
    @Delegate
    JPAFinder<UserEntity> jpaFinder;
}
Non-Default annotation example
@Qualifier
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface NonDefault {
}
Non-Default EntityManager injection example
@Stateless
public class InjectedEntityManager {
    @Inject
    @NonDefault
    EntityManager entityManager;
}
Injected DAO with non-default EntityManager
@Stateless
public class InjectedNonDefaultDAO {
    @Inject
    @Delegate
    @EntityManagerSelector(NonDefault.class)
    JPAFinder<UserEntity> jpaFinder;
}
More complete DAO example that forwards EntityManager methods
@Stateless
public class ExampleDelegateDAO {
    @Inject
    @Delegate(excludes = EntityManagerExclusions.class)
    EntityManager entityManager;
    @Inject
    @Delegate
    JPAFinderHelper<UserEntity> jpaFinder;
}
Non-default EntityManager Producer example
@RequestScoped
public class EntityManagerProducer {
    @Getter(onMethod = @__({@Produces, @NotDefault}))
    @PersistenceContext(unitName = "nonDefault")
    EntityManager entityManager;
}
Note
@NonDefault annotation can be removed to use it as a Default producer. If there is only one Persistence Unit, unitName can be also omitted.
EJB Stateless EntityManager Producer example (optimization)
@Stateless
@TransactionAttribute(SUPPORTS)
public class StatelessEntityManagerProducer {
    @Getter(onMethod = @__({@Produces, @NonDefault}))
    @PersistenceContext(unitName = "nonDefault")
    EntityManager entityManager;
}

The example above works well if @RequestScope is unavailable and may work better because of EJB bean pooling optimizations.

Inherited DaoHelper example (not recommeded)
@Stateless
public class InheritedDAO extends InheritableDaoHelper<UserEntity> {
    @Inject
    EntityManager entityManager;

    @PostConstruct
    void init() {
        jpaFinder = new DaoHelper<>(() -> entityManager, UserEntity.class);
    }
}

JPA: Generics-based Type-safe native query

JPANativeQuery interface has a convenience method createNativeQuery() which will return TypedNativeQuery object. This is a thin wrapper over JPA’s Query object, however it’s getResult*() methods return typed results via generics, which avoids casting and makes the results easier and safer to use.

Native Query Example
public List<UserEntity> findByNative(String sql) {
    return jpaFinder.createNativeQuery(sql, jpaFinder.getEntityClass()).getResultList();
}

Jakarta Faces: Automated PROJECT_STAGE configuration

Jakarta Faces runs in production mode by default. However, most applications set up development mode by modifiying web.xml. Traditionally, it’s been difficult to set up environment-based switching from development to production mode. FlowLogix sets this up automatically via web-fragment.xml and allows JNDI-based switch to production mode.

Note
Special entries in web.xml, glassfish-web.xml or any other container-specific configuration are no longer required and can be removed.

Below is an example of setting up production mode with Payara and GlassFish with Jakarta EE 9 or later:

$ asadmin create-custom-resource --resType=java.lang.String --factoryClass=org.glassfish.resources.custom.factory.PrimitivesAndStringFactory faces/ProjectStage
$ asadmin set resources.custom-resource.faces/ProjectStage.property.value=Production

With Jakarta EE or Java EE 8 and earlier, replace faces/ with jsf/.

Jakarta Faces: Use minified assets automatically in production mode

Most front-end applications want to use minified versions of their assets, such as JavaScript and CSS files in production (i.e. any non-development) modes. FlowLogix allows this via MinimizedHandler which will automatically insert min prefix into the appropriate assets, for example resource.js → resource.min.js and resource.css → resource.min.css. This is configurable via web.xml parameters com.flowlogix.MINIMIZED_PREFIX and com.flowlogix.MINIMIZED_FILE_TYPES

MinimizedHandler works with build tools that generate minified versions of resources automatically, such as maven minify plugin.

faces-config.xml
<application>
    <resource-handler>com.flowlogix.ui.MinimizedHandler</resource-handler>
</application>

Any configured Jakarta Faces resources are resolved to their minified versions

index.xhtml: automatically resolves to myjavascript.min.js
<h:outputScript name="myjavascript.js"/>
Importing automatically resolves to other.min.css
@import url("#{resource['css/other.css']}");

By default, only resources with css and js extensions are resolved to their minified versions. The min extension is inserted prior to their original extension.

web.xml: change the default .min to .minimized
<context-param>
    <param-name>com.flowlogix.MINIMIZED_PREFIX</param-name>
    <param-value>minimized</param-value>
</context-param>

The above example changes resolution from mycss.cssmycss.min.css to mycss.cssmycss.minimized.css

Override extensions that are resolved to their minimized versions
<!-- Optional, default is "css, js" -->
<context-param>
    <param-name>com.flowlogix.MINIMIZED_FILE_TYPES</param-name>
    <param-value>css, js, tsx, sass, scss, less</param-value>
</context-param>

OmniFaces: Automatic initialization of UnmappedResourceHandler

In order to initialize OmniFaces' UnmappedResourceHandler, both web.xml servlet-mapping and faces-config.xml entries are ordinarily required. FlowLogix automates more complicated web.xml requirements by configuring the servlet container to include all unmapped resources. This alleviates requirement to specify Faces Servlet mapping in your web.xml file at all. In order to enable this, add the below context parameter in web.xml:

<context-param>
    <param-name>com.flowlogix.add-unmapped-resources</param-name>
    <param-value>true</param-value>
</context-param>

PrimeFaces: Font mime-types automatically included

PrimeFaces automatically includes fonts as part of the application. However, the file extensions of these fonts are not usually included in most servlet containers by default. FlowLogix adds those mime types automatically and prevents the warnings such as below from appearing in log files:

 WARNING: JSF1091: No mime type could be found for file font1.woff2

This way, no extra mime-mapping entries are required in web.xml

Type Converter, Serialization Tester and Integration Test Helper

Convert Strings to arbitrary types

Most classes that can be constructed from String include valueOf(String) method by convention. TypeConverter class uses this to generically convert a String to any type specified, thus dramatically reducing the code required. If unable to convert the class, an exception is thrown.
TypeConverter specifically does not support custom converters for simplicity. If those are desired, other libraries do a great job of handling custom converters, including Jakarta Faces' Converters.

Generically and dynamically transform a String to any class, throws an exception if failed
TT convertedValue = TypeConverter.valueOf(input, cls);
Convert and check if returned value matches the input after the conversion, without throwing any exceptions
CheckedValue<TT> checkedValue = TypeConverter.checkAndConvert(input, cls);
if (checkedValue.isValid()) {
    // get and operate on a value in a type-safe way
    TT value = checkedValue.getValue();
}
Transform java → jakarta namespace at run-time (jakartify)

FlowLogix provides a convenience method for converting strings from javax to jakarta namespace:

Servlet example
// returns "jakarta.servlet.Servlet" in Jakarta artifacts
String jakartaServlet = JakartaTransformerUtils.jakartify("javax.servlet.Servlet");
Jakarta Faces example
// returns "jakarta.faces.FacesException: message X" in Jakarta artifacts
String jakartaError = JakartaTransformerUtils.jakartify("javax.faces.FacesException: message X");

FlowLogix automatically detects which environment it’s in, and converts javax-based names into jakarta-based namespace accordingly. Keep in mind that the environment check occurs at run-time, and not compile time. This method works not only for classes, but also for error messages and other strings.

The jakartify() utility is particularly useful in scenarios where maven shade plugin is used to produce both javax and jakarta-based JAR artifacts. It allows to use same source code for both and dynamically produce correct strings without additional shade plugins or complex regular expressions.

Serialization Tester

Generic serializeAndDeserialize() method can be used to check the true ability to serialize a class. It returns the result of the input going through serialization and deserialization, so the new object’s state can be checked for correctness.

Check correctness of serialized object
TT output = SerializeTester.serializeAndDeserialize(input);
assertEquals(input, output);
Read String from Stream easily

FlowLogix provides an easy way to read a String from any input stream:

var stream = new ByteArrayInputStream(input.getBytes());
String string = Streams.readString(stream);
Note
The input stream is not closed by the readString method
Configuring testing environments and making testing easier

ShrinkWrapManupulator class has a few utility methods that make testing tricky behaviors easier.

Some tests require TLS/SSL to execute properly. Arquillian uses plain http by default. To facilitate tests that require TLS/SSL, toHttpsURL(url) takes a URL and converts it to it’s https equivalent, taking the httpsPort system property into account. Default TLS port is 8181. A more complete method toHttpURL(url, sslPortPropertyName, defaultPort) is available if the default system property and port are not suitable for your needs.

With command below, toHttpsUrl("http://host/index.html") will return "https://host:8282/index.html"
$ mvn verify -DhttpsPort=8282
Creating ShrinkWrap (Arquillian) tests easily from maven POM file
@Deployment
public static WebArchive deployMaven() {
    WebArchive archive = ShrinkWrapManipulator.createDeployment(WebArchive.class);
    log.info("Archive Contents: %s", archive.toString(true));
    return archive;
}
Creating ShrinkWrap (Arquillian) tests easily from maven POM file with prod suffix
@Deployment
public static WebArchive deployMavenSuffix() {
    WebArchive archive = ShrinkWrapManipulator.createDeployment(WebArchive.class, name -> name + "-prod");
    log.info("Archive Contents: %s", archive.toString(true));
    return archive;
}
Configuring ShrinkWrap (Arquillian) test artifacts

webXmlXPath() takes a List<Action> and will manipulate archive’s web.xml to achieve the desired test configuration. For example, if Jakarta Faces production mode is desired for a particular archive, web.xml context-param can be changed below:

Configure tests for Jakarta Faces production mode
@Deployment
public static WebArchive deployProductionMode() {
    var archive = ShrinkWrapManipulator.createDeployment(WebArchive.class);
    // add classes to the archive here
    var productionList = List.of(new Action(getContextParamValue(
            jakartify("javax.faces.PROJECT_STAGE")),
            node -> node.setTextContent("Production")));
    new ShrinkWrapManipulator().webXmlXPath(archive, productionList);
    return archive;
}

getContextParamValue() is a shorthand to produce XPath for web.xml context parameter (<context-param>) element:

//web-app/context-param[param-name = 'jakarta.faces.PROJECT_STAGE']/param-value

The second parameter is DOM Node class Consumer lambda, which allows for manipulation of the DOM element directly by the user.
Above, we also combine web.xml manipulation with jakartify to be compatible with both Jakarta EE 8 or 9, if desired.

ShrinkWrapManipulator can also easily manipulate persistence.xml file. Even though maven filtering can be used to resolve a versioned entities JAR in persistence.xml, ShrinkWrap will not do the filtering because it uses its own mechanism to create the archive. Another way is needed to create the substitution inside the deploy() method in ShirkWrap:

<persistence>
    <persistence-unit>
        <jar-file>entities-${project.version}.jar</jar-file>
    </persistence-unit>
</persistence>
Example of manipulation of persistence.xml file
public static WebArchive deployPersistence() {
    var archive = ShrinkWrapManipulator.createDeployment(WebArchive.class);
    // add classes to the archive here
    String version = System.getProperty("project.version");
    new ShrinkWrapManipulator().persistenceXmlXPath(archive,
            List.of(new Action("//persistence/persistence-unit/jar-file",
                    node -> node.setTextContent(String.format("lib/entities-%s.jar", version)))));
    return archive;
}

Developers can transform any XML file from the archive by using the generic manipulateXml() method, which both webXmlXPath() and persistenceXmlXPath() methods utilize to do their job.

FlowLogix PrimeFaces DataTable Lazy Data Model backed by JPA

An easier alternative to PrimeFaces JPA Lazy Data model

PrimeFaces provides a convenient wrapper for the Lazy DataModel. However, FlowLogix JPALazyDataModel predates it and therefore has a big "head start" in ease of use, features and compactness. Biggest advantage is ability to @Inject the model via CDI. The model utilizes JPAFinder classes and methodology to make JPA lazy data model easier to use, with a lot less code and better design. The model not require inheritance and problems associated with it. In addition to its ease of use advantages, JPALazyDataModel is fully serializable, making its use trivial in clustered sessions, like the ones in Payara and Hazelcast. To make it even easier to use and configure, model’s initialization lambdas do not have to be serializable and 'just work' out-of-the-box without paying any special attention to their context.

userviewer.xhtml
<p:dataTable lazy="true" value="#{userViewer.lazyModel}" var="user">
    ... specify columns as usual ...
</p:dataTable>
UserViewer.java
@Named
@ViewScoped
public class UserViewer implements Serializable {
    @Inject
    @Getter
    // optional configuration annotation
    @LazyModelConfig(caseInsensitive = true)
    JPALazyDataModel<UserEntity> lazyModel;
}

Above we created a model with case-insensitive filtering. Model can be optionally initialized further inside @PostConstruct using initialize() method and builder pattern (see below)

UserViewer.java - Direct model creation
@Named
@ViewScoped
public class UserViewer implements Serializable {
    private final @Getter JPALazyDataModel<UserEntity> userModel =
            JPALazyDataModel.create(builder -> builder.entityClass(UserEntity.class).build());
}

Above, we create the model without injection. During direct creation, JPALazyDataModel only requires entityClass to work, everything else is optional:

  • entityManager: Provide entity manager Supplier. Default is injected via CDI

  • entityManagerQualifiers: Provides a list of qualifier annotations to select the correct EntityManager for injection

  • caseSensitiveFilter: Specifies if filtering of String values are case-sensitive (boolean). Default is true.

  • filterCaseConversion: either UPPER or LOWER. Specifies whether to convert queries to upper or lower case during case-insensitive filter queries. Default is UPPER

  • wildcardSupport: Specifies whether wildcards are supported in EXACT and other String queries (boolean). Default is false.

  • sorter: Apply additional or replacement sort criteria

  • filter: Apply additional or replacement filter criteria

  • optimizer: Apply additional customizations to queries, such as JPA hints, works together with JPAFinder

  • resultEnricher: Apply transformation to the results of the query, such as adding or modifying resulting rows displayed by the model

  • converter: Function that converts String representation of a primary key into a primary key object. Needed only if the default is insufficient.

  • keyConverter: Function that converts an entity object into it’s primary key in String form. Needed only if the default is insufficient.

Let’s use custom criteria to add address to the default sort:

Add sorting by zip code
@Named
@ViewScoped
public class SortingDataModel implements Serializable {
    @Inject
    @Getter
    JPALazyDataModel<UserEntity> userModel;

    @PostConstruct
    void initialize() {
        // add an ascending zip code-based sort order
        userModel.initialize(builder -> builder.sorter((sortData, cb, root) ->
                        sortData.applicationSort(UserEntity_.zipCode.getName(),
                        var -> cb.asc(root.get(UserEntity_.zipCode))))
                .build());
    }
}

Let’s use custom filter criteria using replaceFilter convenience method. Here we make sure that only zip codes greater than that in the filter are returned:

Show only zip codes greater than filtered field
@Named
@ViewScoped
public class FilteringDataModel implements Serializable {
    @Inject
    @Getter
    JPALazyDataModel<UserEntity> userModel;

    @PostConstruct
    void initialize() {
        // display only zip codes greater than the filter field
        userModel.initialize(builder -> builder.filter((filters, cb, root) ->
                        filters.replaceFilter(UserEntity_.zipCode.getName(),
                        (Predicate predicate, Integer value) -> cb.greaterThan(root.get(UserEntity_.zipCode), value)))
                .build());
    }
}

Optimizer hints can be used to fetch dependent entity relationships in batches. The UnaryOperator should return the same TypedQuery instance it was passed in the Fluent manner:

Batch query of dependent entities using IN query
@Named
@ViewScoped
public class OptimizedDataModel implements Serializable {
    @Inject
    @Getter
    JPALazyDataModel<UserEntity> userModel;

    @PostConstruct
    void initialize() {
        // optimize query by batching relationship fetching
        userModel.initialize(builder -> builder.optimizer(query -> query
                        .setHint(QueryHints.BATCH, getResultField(UserEntity_.userSettings.getName()))
                        .setHint(QueryHints.BATCH, getResultField(UserEntity_.alternateEmails.getName()))
                        .setHint(QueryHints.BATCH_TYPE, BatchFetchType.IN))
                .build());
    }
}

Results Enricher can be used to post-process the results of the query:

Add a new row to the results
@Named
@ViewScoped
public class EnrichedDataModel implements Serializable {
    @Inject
    @Getter
    JPALazyDataModel<UserEntity> userModel;

    @PostConstruct
    void initialize() {
        // enrich returned results from the model
        userModel.initialize(builder -> builder.resultEnricher(EnrichedDataModel::addLastRow).build());
    }

    private static List<UserEntity> addLastRow(List<UserEntity> list) {
        list.add(UserEntity.builder().userId("golden").fullName("Golden User").build());
        return list;
    }
}

Let’s select a specific entity manager for this particular data model, by using a CDI qualifier:

Using @LazyModelConfig(entityManagerSelector) to use a specific persistence unit
@Named
@ViewScoped
public class QualifiedDataModel implements Serializable {
    @Inject
    @Getter
    @LazyModelConfig(entityManagerSelector = NonDefault.class)
    JPALazyDataModel<UserEntity> userModel;
}

JPALazyDataModel needs to convert a String representation of the entity’s primary key into the actual entity primary key object. This is a requirement for PrimeFaces' LazyDataModel.getRowKey(String) method. Default converter is provided. However, if it’s insufficient, converter builder method is provided to override the default.

JPALazyDataModel needs to convert an entity instance to a String that represents its primary key. This is a requirement for PrimeFaces' LazyDataModel.getRowKey(TT) method. Default converter is provided. However, if it’s insufficient, keyConverter builder method is provided to override the default.

Use binary value as a primary key representation, using converters
@Named
@ViewScoped
public class ConverterDataModel implements Serializable {
    @Inject
    @Getter
    JPALazyDataModel<UserEntity> userModel;

    @PostConstruct
    void initialize() {
        userModel.initialize(builder -> builder
                // key is stored as binary string
                .converter(binaryString -> new BigInteger(binaryString, 2).longValue())
                .keyConverter(entity -> Long.toBinaryString(entity.getId()))
                .build());
    }
}

API Reference