Categories
Java SQL

Java Optional in Hibernate 6 entity support

Support Java Optional in Hibernate 6 entity, thanks to the Lib Custom library, a wrapper that simplifies calls to Byte Buddy library

In this tuto we use the functional option class: OptionF, but with little changes you can use any other option class, such as Optional

1) Add lib-custom to your pom.xml dependencies

Lib-custom is a library that allows easy customization of external third-party libs like Hibernate

<dependency>
    <groupId>io.github.jleblanc64</groupId>
    <artifactId>lib-custom</artifactId>
    <version>1.0.0</version>
</dependency>

2) Customize Hibernate 6 with Java Optional

We need to override 3 Hibernate functions:

UnknownBasicJavaType.getRecommendedJdbcType()
UnknownBasicJavaType.unwrap()
UnknownBasicJavaType.wrap()

basically we are explaining to Hibernate how to do the conversion String.class <-> OptionF.class:

[string=null] <-> [option=OptionF.emptyO()]
[string=x] <-> [option=OptionF.o(x)]

Java code HibernateOption:

public class HibernateOption {
    public static void override() {
        // replace with your own values
        var rootPackage = "com.demo";
        var optionClass = OptionF.class;

        var tableToEntity = f(new Reflections(rootPackage).getTypesAnnotatedWith(Entity.class))
                .toMap(x -> x.getAnnotation(Table.class).name(), x -> x);

        LibCustom.override(UnknownBasicJavaType.class, "getRecommendedJdbcType", args -> {
            var ind = args[0];
            if (!(ind instanceof BasicValue))
                return null;

            var b = (BasicValue) ind;

            var s = b.getColumn();
            if (!(s instanceof Column))
                return null;

            var c = (Column) s;

            var entity = tableToEntity.get(b.getTable().getName());
            var fields = f(entity.getDeclaredFields());
            var field = fields.findSafe(x -> x.getName().equals(c.getName()));
            if (field.getType() == optionClass)
                return new VarcharJdbcType();

            return null;
        });

        LibCustom.overrideWithSelf(UnknownBasicJavaType.class, "unwrap", argsSelf -> {
            var args = argsSelf.args;
            var v = args[0];
            var type = (Class<?>) args[1];

            OptionF<?> o;
            if (type == String.class && instanceOf(v, optionClass)) {

                o = (OptionF<?>) v;
                if (o.isPresent())
                    return o.get();

                return new ValueWrapper(null);
            }

            if (type == optionClass && !instanceOf(v, optionClass))
                // return Option from nullable value v
                return o(v);

            return v;
        });

        LibCustom.overrideWithSelf(UnknownBasicJavaType.class, "wrap", argsSelf -> {
            var args = argsSelf.args;
            var u = (UnknownBasicJavaType) argsSelf.self;
            var v = args[0];
            var type = u.getJavaTypeClass();

            if (type == String.class && instanceOf(v, optionClass)) {
                var o = (OptionF<?>) v;
                if (o.isPresent())
                    return o.get();

                return new ValueWrapper(null);
            }

            if (type == optionClass && !instanceOf(v, optionClass))
                // return Option from nullable value v
                return o(v);

            return v;
        });
    }

    static boolean instanceOf(Object o, Class<?> c) {
        return o != null && o.getClass() == c;
    }
}

3) Load custom Hibernate 6 byte code at the start of your app

Here we are using Spring boot, and a good place to load the custom byte code is in the DataSourceConfig configuration class. To load the custom byte code (customized by LibCustom in step 2)), just call:

HibernateOption.override();
LibCustom.load();

Java code DataSourceConfig:

@Configuration
public class DataSourceConfig {
    @Value("${spring.datasource.url}")
    String url;

    @Value("${spring.datasource.username}")
    String username;

    @Value("${spring.datasource.password}")
    String password;

    @Bean
    public DataSource getDataSource() {
        HibernateList.override();
        Jackson.override();

        HibernateOption.override();
        LibCustom.load();

        var ds = new HikariDataSource();
        ds.setJdbcUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);

        // default is 10
        ds.setMaximumPoolSize(8);

        var config = Flyway.configure().dataSource(url, username, password);
        config.load().migrate();

        return ds;
    }
}

4) You can now use Optional class in your Hibernate 6 entity code:

For instance, for column name:

private OptionF<String> name;

Java code Customer:

@Getter
@Setter
@Entity
@Table(name = "customers")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private OptionF<String> name;

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
    private ListF<Order> orders;
}

5) Full working example source code

https://github.com/jleblanc64/hibernate6/blob/main/src/main/java/com/demo/custom/HibernateOption.java

Test class:

https://github.com/jleblanc64/hibernate6/blob/main/src/test/java/com/demo/ApplicationTests.java

Leave a Reply

Your email address will not be published. Required fields are marked *