Hi Lukas,
A contrived table and UDT setup to demonstrate the issue would be as follows:
CREATE TYPE message AS (
definition TEXT,
parameters HSTORE
);
CREATE TABLE person (
id BIGINT NOT NULL,
messages message[],
PRIMARY KEY (id)
);
Here is the binding implementation I am using to bind HSTORE to Map<String, String>. It is pretty vanilla, copied straight from the Jooq examples (very helpful!).
public class JooqHStoreToMapBinding implements Binding<Object, Map<String, String>> {
private static final long serialVersionUID = SerialVersion.SERIAL_VERSION_UID;
private Converter converter = new Converter();
@Override
public org.jooq.Converter<Object, Map<String, String>> converter() {
return converter;
}
@Override
public void sql(final BindingSQLContext<Map<String, String>> ctx) throws SQLException {
ctx.render().visit(DSL.val(ctx.convert(converter).value())).sql("::hstore");
}
@Override
public void register(final BindingRegisterContext<Map<String, String>> ctx) throws SQLException {
ctx.statement().registerOutParameter(ctx.index(), Types.CLOB);
}
/**
* Gets a Map and convert it to a string on a {@link PreparedStatement}.
*/
@Override
public void set(final BindingSetStatementContext<Map<String, String>> ctx) throws SQLException {
ctx.statement().setString(ctx.index(), Objects.toString(ctx.convert(converter).value(), null));
}
/**
* Gets a string value from a {@link ResultSet} and converts it to a Map
*/
@Override
public void get(final BindingGetResultSetContext<Map<String, String>> ctx) throws SQLException {
ctx.convert(converter).value(ctx.resultSet().getString(ctx.index()));
}
/**
* Gets a string value from a {@link CallableStatement} and converts it to a Map
*/
@Override
public void get(final BindingGetStatementContext<Map<String, String>> ctx) throws SQLException {
ctx.convert(converter).value(ctx.statement().getString(ctx.index()));
}
// The following methods are not required for Postgres
@Override
public void get(final BindingGetSQLInputContext<Map<String, String>> ctx) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public void set(final BindingSetSQLOutputContext<Map<String, String>> ctx) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
private static class Converter implements org.jooq.Converter<Object, Map<String, String>> {
private static final long serialVersionUID = SerialVersion.SERIAL_VERSION_UID;
private static final Map<String, String> INSTANCE = new HashMap<>();
private static final char QUOTE = '"';
private static final String ARROW = "=>";
private static final char COMMA = ',';
@Override
public Object to(final Map<String, String> userObject) {
// Convert a map into SQL values for a hstore:
// "key1"=>"value1","key2"=>"value2" etc.
final StringBuilder sb = new StringBuilder();
for (final Map.Entry<String, String> entry : userObject.entrySet()) {
sb.append(QUOTE).append(entry.getKey()).append(QUOTE).append(ARROW)
.append(QUOTE).append(entry.getValue()).append(QUOTE).append(COMMA);
}
return sb.toString();
}
@Override
public Map<String, String> from(final Object databaseObject) {
// Convert SQL values for a hstore into a map
final Map<String, String> map = new HashMap<>();
if (StringUtils.isEmpty(databaseObject.toString())) {
return map;
}
for(final String kvPair : databaseObject.toString().replace("\"", "").split(",")) {
final String[] keyAndValue = kvPair.split("=>");
map.put(keyAndValue[0], keyAndValue[1]);
}
return map;
}
@Override
public Class<Object> fromType() {
return Object.class;
}
@SuppressWarnings("unchecked")
@Override
public Class<Map<String, String>> toType() {
return (Class<Map<String, String>>) INSTANCE.getClass();
}
}
}
Finally, my code generator is setup like this to register the binding (gradle notation):
generator() {
name('org.jooq.util.DefaultGenerator')
strategy {
name('org.jooq.util.DefaultGeneratorStrategy')
}
database() {
name('org.jooq.util.postgres.PostgresDatabase')
inputSchema(jooqSchema)
forcedTypes() {
forcedType() {
userType('java.util.Map<String,String>')
binding('JooqHStoreToMapBinding')
expression('.*Message\\.parameters')
types('.*')
}
}
}
generate() {
relations(true)
deprecated(false)
records(true)
immutablePojos(true)
fluentSetters(true)
}
target() {
packageName("ng.${project.name}.${jooqSchema}")
directory(outputDirectory)
}
}
The code generator produces MessageRecord with a parameters field of the correct type (Map<String, String>) and PersonRecord with a field of type MessageRecord[] as expected.
When the MessageRecord[] is serialised the custom binding for HSTORE appears not be called at all and the objects are not serialised correctly.
If the table field is just a message (rather than an array) then the binding is applied correctly. Am I missing something with my code generation to apply the custom binding correctly to array elements?
Thanks for taking a look at this!
Regards,
Leigh