En utilisant JOOQ avec MySQL 5.7 3.5.2, je suis en train d'accomplir ce qui suit ...
MySQL dispose d'un ensemble de fonctions qui permettent de JSON chemin manipulation ciblée des propriétés à l'intérieur de documents volumineux.
Je suis en train de faire une abstraction qui profite de cette utilisation de JOOQ. J'ai commencé par la création JSON modèle sérialisable de documents qui garde la trace des changements, puis mis en œuvre une coutume JOOQ Binding
pour elle.
Dans cette liaison, j'ai toutes les informations d'état nécessaires pour générer des appels à ces fonctions MySQL JSON à l'exception du nom qualifié ou un alias de la colonne mise à jour. Une référence à ce nom est nécessaire pour la mise à jour des documents existants JSON en place.
Je suis incapable de trouver un moyen d'accéder à ce nom des * Context
types disponibles dans l'interface de reliure.
J'ai réfléchi mettre en œuvre un VisitListener
pour capturer ces noms de champs et les passer à travers la Scope
carte de données personnalisées, mais cette option semble assez fragile.
Que pourrait la meilleure façon d'accéder au nom de l'être sur le terrain ou alias abordé dans mon implémentation d'une liaison?
--Éditer--
OK, pour aider à clarifier mes objectifs ici, prendre la DDL suivante:
create table widget (
widget_id bigint(20) NOT NULL,
jm_data json DEFAULT NULL,
primary key (widget_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Maintenant , supposons que jm_data tiendra la représentation JSON d'un java.util.Map<String,String>
. Pour cette JOOQ offre une très belle API d'extension en mettant en œuvre et l' enregistrement d' une liaison de type de données personnalisées (dans ce cas en utilisant Jackson):
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();
}
}
... cette mise en œuvre est fixé au moment de la construction par le code-gen comme ceci:
<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>
... donc avec cela en place, nous avons une belle carte de java fortement typé que nous pouvons manipuler dans notre code d'application. La mise en œuvre de liaison si, bien il écrit toujours le contenu de carte à la colonne JSON, même si une seule entrée de carte a été insérée, mis à jour ou supprimé. Cette implémentation traite la MySQL JSON
colonne comme une normale VARCHAR
colonne.
Cette approche pose deux problèmes d'importance variable en fonction de l'utilisation.
- Mise à jour seulement une partie d'une grande carte avec plusieurs milliers d'entrées produit du trafic inutile de fil SQL comme un effet secondaire.
- Si le contenu de la carte sont modifiables de l'utilisateur, et il y a plusieurs utilisateurs modifier le contenu en même temps, les changements d'un facteur peut être écrasé par l'autre, même si elles ne sont pas contradictoires.
MySQL 5.7 a introduit le type de données JSON, et un certain nombre de fonctions pour manipuler les documents dans SQL. Ces fonctions permettent de traiter le contenu des documents JSON, ce qui permet des mises à jour ciblées des propriétés individuelles. Poursuivant notre exemple ...:
insert into DEV.widget (widget_id, jm_data)
values (1, '{"key0":"val0","key1":"val1","key2":"val2"}');
... ce qui précède la mise en œuvre de liaison générerait SQL comme ça si je devais changer la valeur java carte « key1 » à l'égalité « updated_value1 » et invoquez une mise à jour sur le dossier:
update DEV.widget
set DEV.widget.jm_data = '{"key0":"val0","key1":"updated_value1","key2":"val2"}'
where DEV.widget.widget_id = 1;
... Avis de toute chaîne JSON est mise à jour. MySQL peut gérer cela de manière plus efficace en utilisant la json_set
fonction:
update DEV.widget
set DEV.widget.jm_data = json_set( DEV.widget.jm_data, '$."key1"', 'updated_value1' )
where DEV.widget.widget_id = 1;
Donc, si je veux générer SQL comme ça, je dois d' abord garder trace des modifications apportées à ma carte de quand il a été lu au début de la DB jusqu'à ce qu'il doit être mis à jour. Puis, en utilisant ces informations de changement, je peux générer un appel à la json_set
fonction qui me permettra de mettre à jour uniquement les propriétés modifiées en place.
Enfin arriver à ma question réelle. Vous remarquerez dans le SQL je souhaite générer, la valeur de la colonne étant mise à jour contient une référence à la colonne elle - même json_set( DEV.widget.jm_data, ...
. Ce nom de la colonne (ou pseudonyme) ne semble pas être disponible à l'API de reliure. J'y suis un moyen d'identifier le nom de la colonne d'alias étant mis à jour à partir de mon Binding
application?
Votre Binding
mise en œuvre est le bon endroit pour chercher une solution à ce problème. Vous ne savez pas vraiment envie de changer la liaison de votre colonne comme par magie de cette json_set
fonction, ce qui fait une mise à jour incrémentale des données JSON plutôt qu'un remplacement complet. Lorsque vous utilisez UpdatableRecord.store()
(que vous semblez être en utilisant), l'attente est de tout Record.field(x)
pour refléter le contenu de la ligne de base de données exactement , pas un delta. Bien sûr, vous pouvez mettre en œuvre quelque chose de similaire dans la sql()
méthode de votre fixation, mais il serait très difficile d'obtenir le droit et la liaison ne serait pas applicable à tous les cas d' utilisation.
Par conséquent, afin de faire ce que vous voulez réaliser, il suffit d' écrire une explicite UPDATE
déclaration avec jOOQ, l' amélioration de l'API jOOQ en utilisant templating SQL simple .
// 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));
}
Ensuite, utilisez votre méthode de bibliothèque:
using(configuration)
.update(WIDGET)
.set(WIDGET.JM_DATA, jsonSet(WIDGET.JM_DATA, "$.\"key1\"", "updated_value1"))
.where(WIDGET.WIDGET_ID.eq(1))
.execute();
Si cela devient trop répétitif, je suis sûr que vous pouvez tenir compte des parties communes et dans certaines API de la vôtre.