在MySQL 5.7中使用JOOQ 3.5.2,我正在尝试完成以下任务...
MySQL具有一组JSON函数,可以对较大文档中的属性进行路径定位操作。
我正在尝试使用JOOQ来利用这一点进行抽象。我首先创建了可跟踪更改的JSON可序列化文档模型,然后为其实现了一个JOOQ定制Binding
。
在此绑定中,除了要更新的列的限定名称或别名之外,我具有生成对这些MySQL JSON函数的调用所需的所有状态信息。要就地更新现有JSON文档,必须使用该名称。
我一直无法通过Binding界面中的* Context
类型来访问此名称。
我一直在考虑实现VisitListener
来捕获这些字段名称,并将它们传递给Scope
自定义数据映射,但是该选项似乎很脆弱。
获得对我的Binding实现中要寻址的字段或别名的最佳访问方式是什么?
-编辑-
好的,为了在这里阐明我的目标,请使用以下DDL:
create table widget (
widget_id bigint(20) NOT NULL,
jm_data json DEFAULT NULL,
primary key (widget_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
现在,让我们假设jm_data将保存java.util.Map<String,String>
的JSON表示形式。为此,JOOQ通过实现和注册自定义数据类型绑定(在本例中使用Jackson)提供了一个非常不错的扩展API:
public class MySQLJSONJacksonMapBinding implements Binding<Object, Map<String, String>> {
private static final ObjectMapper mapper = new ObjectMapper();
// The converter does all the work
@Override
public Converter<Object, Map<String, String>> converter() {
return new Converter<Object, Map<String, String>>() {
@Override
public Map<String, String> from(final Object t) {
try {
return t == null ? null
: mapper.readValue(t.toString(),
new TypeReference<Map<String, String>>() {
});
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Object to(final Map<String, String> u) {
try {
return u == null ? null
: mapper.writer().writeValueAsString(u);
} catch (final JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public Class<Object> fromType() {
return Object.class;
}
@Override
public Class toType() {
return Map.class;
}
};
}
// Rending a bind variable for the binding context's value and casting it to the json type
@Override
public void sql(final BindingSQLContext<Map<String, String>> ctx) throws SQLException {
// Depending on how you generate your SQL, you may need to explicitly distinguish
// between jOOQ generating bind variables or inlined literals. If so, use this check:
// ctx.render().paramType() == INLINED
ctx.render().visit(DSL.val(ctx.convert(converter()).value()));
}
// Registering VARCHAR types for JDBC CallableStatement OUT parameters
@Override
public void register(final BindingRegisterContext<Map<String, String>> ctx)
throws SQLException {
ctx.statement().registerOutParameter(ctx.index(), Types.VARCHAR);
}
// Converting the JsonElement to a String value and setting that on a JDBC 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));
}
// Getting a String value from a JDBC ResultSet and converting that to a Map
@Override
public void get(final BindingGetResultSetContext<Map<String, String>> ctx) throws SQLException {
ctx.convert(converter()).value(ctx.resultSet().getString(ctx.index()));
}
// Getting a String value from a JDBC CallableStatement and converting that to a Map
@Override
public void get(final BindingGetStatementContext<Map<String, String>> ctx) throws SQLException {
ctx.convert(converter()).value(ctx.statement().getString(ctx.index()));
}
// Setting a value on a JDBC SQLOutput (useful for Oracle OBJECT types)
@Override
public void set(final BindingSetSQLOutputContext<Map<String, String>> ctx) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
// Getting a value from a JDBC SQLInput (useful for Oracle OBJECT types)
@Override
public void get(final BindingGetSQLInputContext<Map<String, String>> ctx) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
}
...此实现在生成时由代码源附加,如下所示:
<customTypes>
<customType>
<name>JsonMap</name>
<type>java.util.Map<String,String></type>
<binding>com.orbiz.jooq.bindings.MySQLJSONJacksonMapBinding</binding>
</customType>
</customTypes>
<forcedTypes>
<forcedType>
<name>JsonMap</name>
<expression>jm_.*</expression>
<types>json</types>
</forcedType>
</forcedTypes>
...因此,有了它,我们有了一个不错的强类型Java Map,可以在应用程序代码中对其进行操作。但是,绑定实现很好地始终将整个地图内容写入JSON列,即使仅插入,更新或删除了单个地图条目也是如此。此实现将MySQL JSON
列视为普通的VARCHAR
列。
这种方法提出了两个问题,这些问题的重要性取决于用法。
MySQL 5.7引入了JSON数据类型,以及许多用于处理SQL文档的函数。这些功能使处理JSON文档的内容成为可能,从而允许对单个属性进行有针对性的更新。继续我们的示例...:
insert into DEV.widget (widget_id, jm_data)
values (1, '{"key0":"val0","key1":"val1","key2":"val2"}');
...如果我将java Map的“ key1”值更改为等于“ updated_value1”并在记录上调用更新,则上述Binding实现将生成类似的SQL:
update DEV.widget
set DEV.widget.jm_data = '{"key0":"val0","key1":"updated_value1","key2":"val2"}'
where DEV.widget.widget_id = 1;
...请注意,整个JSON字符串正在更新。 MySQL可以使用json_set
函数来更有效地处理此问题:
update DEV.widget
set DEV.widget.jm_data = json_set( DEV.widget.jm_data, '$."key1"', 'updated_value1' )
where DEV.widget.widget_id = 1;
因此,如果要生成这样的SQL,则需要首先跟踪从最初从数据库读取Map到对其进行更新之前对Map所做的更改。然后,使用此更改信息,我可以生成对json_set
函数的调用,这将允许我仅更新就地修改的属性。
最后是我的实际问题。您会在我希望生成的SQL中注意到,要更新的列的值包含对列本身json_set( DEV.widget.jm_data, ...
的引用。该列(或别名)名称似乎对绑定API不可用。我可以通过Binding
实现中的方法来标识要更新的别名列的名称吗?
答案 0 :(得分:0)
您的Binding
实现是寻找此问题的解决方案的错误位置。您真的不想更改列的绑定,以某种方式神奇地知道此json_set
函数,该函数对json数据进行增量更新,而不是完全替换。使用UpdatableRecord.store()
(您似乎正在使用)时,期望任何Record.field(x)
都能准确地反映数据库行的内容,而不是增量。当然,您可以可以在绑定的sql()
方法中实现类似的功能,但是很难做到正确,并且绑定不适用于所有用例。< / p>
因此,为了执行您要实现的目标,只需使用jOOQ编写一个显式的UPDATE
语句,并使用plain SQL templating增强jOOQ API。
// Assuming this static import
import static org.jooq.impl.DSL.*;
public static Field<Map<String, String>> jsonSet(
Field<Map<String, String>> field,
String key,
String value
) {
return field("json_set({0}, {1}, {2})", field.getDataType(), field, inline(key), val(value));
}
然后,使用您的库方法:
using(configuration)
.update(WIDGET)
.set(WIDGET.JM_DATA, jsonSet(WIDGET.JM_DATA, "$.\"key1\"", "updated_value1"))
.where(WIDGET.WIDGET_ID.eq(1))
.execute();
如果这太重复了,我相信您也可以在您自己的某些API中排除常见部分。