草庐IT

java - 在 JOOQ 自定义绑定(bind)中生成 SQL 以填充 MySQL JSON 函数时访问字段名称或别名

coder 2023-10-13 原文

将 JOOQ 3.5.2 与 MySQL 5.7 结合使用,我正在尝试完成以下任务...

MySQL 有一组 JSON 函数,允许对较大文档中的属性进行路径定向操作。

我正在尝试使用 JOOQ 进行抽象,利用它。我首先创建 JSON 可序列化文档模型来跟踪更改,然后实现 JOOQ 自定义 Binding

在此绑定(bind)中,我拥有生成对这些 MySQL JSON 函数的调用所需的所有状态信息,但要更新的列的限定名称或别名除外。就地更新现有 JSON 文档需要引用此名称。

我一直无法找到从 * Context 访问此名称的方法绑定(bind)接口(interface)中可用的类型。

我一直在考虑实现 VisitListener捕获这些字段名称并通过 Scope 传递它们自定义数据映射,但该选项似乎很脆弱。

在我的绑定(bind)实现中访问字段名称或别名的最佳方法是什么?

--编辑--

好的,为了帮助阐明我的目标,请采用以下 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 通过实现和注册自定义数据类型绑定(bind)(在本例中使用 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&lt;String,String&gt;</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,我们可以在我们的应用程序代码中操作它。但是,绑定(bind)实现总是将整个 map 内容写入 JSON 列,即使只插入、更新或删除了一个 map 条目。此实现处理 MySQL JSON像正常的列VARCHAR柱子。

根据使用情况,这种方法会带来两个意义不同的问题。

  1. 仅更新包含数千个条目的大型 map 的一部分会产生不必要的 SQL 线路流量作为副作用。
  2. 如果 map 的内容是用户可编辑的,并且有多个用户同时编辑内容,一个人的更改可能会被另一个人覆盖,即使他们不冲突也是如此。

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,我需要首先跟踪从最初从 DB 读取 map 到更新 map 对 map 所做的更改。然后,使用此更改信息,我可以生成对 json_set 的调用函数,它允许我只更新已修改的属性。

终于到了我的实际问题。您会注意到在我希望生成的 SQL 中,正在更新的列的值包含对列本身的引用 json_set( DEV.widget.jm_data, ... .该列(或别名)名称似乎对 Binding API 不可用。我有一种方法可以识别正在从我的 Binding 中更新的别名列的名称。实现?

最佳答案

您的 Binding 实现是寻找此问题解决方案的错误位置。您真的不想更改列的绑定(bind)以某种方式神奇地知道此 json_set 函数,该函数对 json 数据进行增量更新而不是完全替换。当您使用 UpdatableRecord.store()(您似乎正在使用)时,期望任何 Record.field(x) 反射(reflect)数据库行的内容完全,不是增量。当然,您可以在绑定(bind)的 sql() 方法中实现类似的东西,但是很难做到正确并且绑定(bind)不会适用于所有用例。

因此,为了实现您想要实现的目标,只需使用 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 中分解出通用部分。

关于java - 在 JOOQ 自定义绑定(bind)中生成 SQL 以填充 MySQL JSON 函数时访问字段名称或别名,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51649605/

有关java - 在 JOOQ 自定义绑定(bind)中生成 SQL 以填充 MySQL JSON 函数时访问字段名称或别名的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  3. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  4. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  5. ruby - 续集在添加关联时访问many_to_many连接表 - 2

    我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

  6. ruby - ruby 中的 TOPLEVEL_BINDING 是什么? - 2

    它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput

  7. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

    我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

  8. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  9. ruby-on-rails - 在 ruby​​ 中使用 gsub 函数替换单词 - 2

    我正在尝试用ruby​​中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了

  10. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

随机推荐