Migration de colonnes texte avec AbstractTextColumnsProcessor
Vue d’ensemble fonctionnelle
AbstractTextColumnsProcessor est une base de migration Flyway (Java) dédiée aux colonnes texte des tables applicatives. Elle permet de:
- Parcourir automatiquement toutes les tables (sauf exclusions configurées) et détecter les colonnes “texte”.
- Pour chaque ligne, transformer le contenu des colonnes texte:
- Si la valeur est un JSON “wrappeur” (string commençant par
{et finissant par}), extraire les valeurs texte et leur appliquer une transformation HTML, puis ré-encoder le JSON. - Sinon, considérer la valeur comme du HTML et l’envoyer à une transformation HTML spécialisée.
- Si la valeur est un JSON “wrappeur” (string commençant par
- Vérifier l’intégrité “fonctionnelle” du contenu après transformation (pas de perte de texte visible) et, en cas d’échec, conserver la valeur d’origine.
- Mettre à jour en base uniquement les lignes modifiées, avec logs détaillés.
Les classes concrètes définissent:
- La logique de transformation HTML (
processHtmlContent). - Le prédicat SQL ciblant uniquement les lignes susceptibles d’être impactées (
buildColumnPredicat).
Des exemples existants:
V7_3_1_6__migration_grilles: migration des grilles CKEditor -> nouveau layout.V7_3_1_10__migration_accordeons: migration d’anciens accordéons<dl/dt/dd>vers<details>.V7_3_1_7__migration_styles,V7_3_1_8__migration_styles_v2,V7_3_1_9__migration_onglets: mêmes patterns pour d’autres structures.
Architecture et flux d’exécution (technique)
-
Initialisation Flyway
- La classe étend
BaseJavaMigrationet implémentemigrate(Context). Flyway l’exécute au moment des migrations (au démarrage).
- La classe étend
-
Sélection des tables à traiter
listAllTables(Connection): récupère l’ensemble des tables via les métadonnées JDBC.- Exclusions: la propriété
text_column_processor.excluded_tables(chargée viaPropertyHelper) peut contenir une liste de tables à ignorer, séparées par des virgules.- Exemple de configuration (fichier
env.propertiesde l’environnement):text_column_processor.excluded_tables=HISTORIQUE_LOGS,TMP_IMPORT
- Exemple de configuration (fichier
-
Détection des colonnes texte
listTextColumns(Connection, String table):- Tente un
SELECT * FROM {table} LIMIT 1et lit leResultSetMetaData. - Est considérée comme “texte” une colonne dont le type JDBC est
LONGVARCHAR, ou de typeVARCHARd’une taille supérieure à un seuil minimal, ou dont le type contientTEXT. - Seuil configurable au niveau de la classe via le champ protégé
minimumColumnSize(valeur par défaut:1024). Une classe fille peut l’ajuster si nécessaire. - Exclut systématiquement la colonne identifiant
ID_{TABLE}.
- Tente un
-
Construction des requêtes SQL
buildQuerySelect(table, columns): construit leSELECTavecID_{table}+ colonnes texte, et y ajoute une clause WHERE construite parbuildColumnPredicat(table, columns)(spécifique à la classe fille).buildQueryUpdate(table, columns): construit unUPDATEavec paramètres, du styleUPDATE {table} SET col1=?, col2=?, ... WHERE ID_{table}=?.buildColumnPredicat(Set<String> columns, String needle): utilitaire générique pour produire une clauseWHERE (col LIKE '%needle%' OR ...), avec échappement des'dansneedle.
-
Traitement ligne par ligne
- Pour chaque ligne du
SELECT:- On lit l’ID (
getIdValue) et chaque colonne texte. processColumn(col, oldVal)décide de la voie:- JSON wrappeur ? (
isJsonWrapped(value): valeur non vide, trim, commence par{et finit par}):processJsonWrappedContent(col, value)- Parse via
CodecJSon.decodeStringJSonToClassenMap<String, Object>. - Pour chaque valeur de type
String, appliqueprocessHtmlContent(html). - Re-encode le JSON via
CodecJSon.encodeObjectToJSonInString.
- Parse via
- Sinon:
processHtmlContent(value)directement.
- JSON wrappeur ? (
- Si la valeur a changé et que l’intégrité est validée, on
UPDATEla ligne.
- On lit l’ID (
- Pour chaque ligne du
-
Vérification d’intégrité
verifyContentIntegrity(originalValue, processedValue):- Convertit HTML -> texte via
Jsoup.parse(...).text()de part et d’autre. - Normalise (suppression
\n,\t, espaces; tri des caractères) puis compare. - Si l’intégrité échoue et que
applyIntegrityVerificationest àtrue(par défaut), la migration pour cette ligne est annulée (on écrit la valeur d’origine), avec unWARN. - Une classe fille peut désactiver ce garde-fou en réglant
applyIntegrityVerification=falsesi elle doit effectuer des transformations modifiant intentionnellement le texte visible.
- Convertit HTML -> texte via
Points d’attention et bonnes pratiques
-
Prédicats SQL efficaces
- Évitez un scan complet des tables: construisez un
WHEREqui restreint aux contenus suspects (viabuildColumnPredicat(columns, "motif")). - Double gestion « non échappé / échappé » si vos motifs peuvent se trouver dans du HTML direct ou du HTML sérialisé en JSON string (voir
SEARCH_ESCAPED).
- Évitez un scan complet des tables: construisez un
-
Intégrité du texte visible
- Conservez
applyIntegrityVerification=truetant que possible. Ne désactivez que si vos transformations changent volontairement le texte (ex. suppression de décorations textuelles devenues obsolètes).
- Conservez
Créer un nouveau script de migration: guide pas-à-pas
-
Étendre
AbstractTextColumnsProcessor- Implémenter:
protected String processHtmlContent(String htmlValue).protected String buildColumnPredicat(String tableName, Set<String> columns).
- Implémenter:
-
Construire des constantes de recherche
- Exemple:
private static final String SEARCH = "<div class=\"old-widget\">"; private static final String SEARCH_ESCAPED = "<div class=\\\\\"old-widget\\\\\">"; // pour JSON string
- Exemple:
-
Implémenter
buildColumnPredicat -
Implémenter
processHtmlContent- Pattern recommandé:
@Override protected String processHtmlContent(final String value) { if (!value.contains("old-widget")) { return value; // court-circuit } final Document doc = Jsoup.parseBodyFragment(value); setXmlSettings(doc); boolean changed = false; // Sélectionner précisément ce que vous transformez final Elements widgets = doc.select("div.old-widget"); for (final Element w : widgets) { final Element replacement = transformWidget(w); if (replacement != null) { w.replaceWith(replacement); changed = true; } } return changed ? doc.body().html() : value; } private Element transformWidget(final Element old) { // Construire le nouveau DOM (balises, classes, attributs) en réutilisant le inner HTML si besoin final Element container = new Element("div").addClass("new-widget"); final Element inner = new Element("div"); inner.html(old.html()); container.appendChild(inner); return container; }
- Pattern recommandé:
-
Ajuster les garde-fous si nécessaire
- Si la transformation modifie le texte visible (suppression de libellés), envisagez
this.applyIntegrityVerification = false;(dans un bloc d’initialisation ou un constructeur) avec prudence et justification. - Vous pouvez aussi abaisser/augmenter
minimumColumnSizesi votre migration doit couvrir desVARCHARplus petits/grands.
- Si la transformation modifie le texte visible (suppression de libellés), envisagez
Modèle minimal de classe de migration
package com.kosmos.core.migration;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.kosmos.migration.core.executor.AbstractTextColumnsProcessor;
public class V7_3_1_X__migration_mon_widget extends AbstractTextColumnsProcessor {
private static final Logger LOG = LoggerFactory.getLogger(V7_3_1_X__migration_mon_widget.class);
// Motifs pour ciblage SQL (HTML direct vs HTML encodé dans JSON)
private static final String SEARCH = "<div class=\"old-widget\">";
private static final String SEARCH_ESCAPED = "<div class=\\\\\"old-widget\\\\\">";
@Override
public Integer getChecksum() {
return 1234567890; // fige le checksum Flyway de cette migration Java
}
@Override
protected String processHtmlContent(final String value) {
if (!value.contains("old-widget")) {
return value;
}
final Document doc = Jsoup.parseBodyFragment(value);
setXmlSettings(doc);
boolean changed = false;
final Elements olds = doc.select("div.old-widget");
for (final Element old : olds) {
final Element neo = transformOldToNew(old);
if (neo != null) {
old.replaceWith(neo);
changed = true;
}
}
return changed ? doc.body().html() : value;
}
private Element transformOldToNew(final Element old) {
final Element container = new Element("div").addClass("new-widget");
final Element inner = new Element("div");
inner.html(old.html());
container.appendChild(inner);
return container;
}
@Override
protected String buildColumnPredicat(final String tableName, final Set<String> columns) {
final String p1 = buildColumnPredicat(columns, SEARCH);
final String p2 = buildColumnPredicat(columns, SEARCH_ESCAPED);
if (StringUtils.isEmpty(p1) && StringUtils.isEmpty(p2)) return StringUtils.EMPTY;
if (StringUtils.isEmpty(p1)) return p2;
if (StringUtils.isEmpty(p2)) return p1;
return p1 + p2.replace(" WHERE ", " OR ");
}
}
FAQ / points spécifiques
-
Et si mon contenu JSON n’est pas un objet au niveau racine ?
isJsonWrappedne reconnaît que les strings démarrant/finissant par{/}. Si vous avez du JSON tableau ou un wrapper différent, ajustezisJsonWrappeddans une sous-classe (ou surchargezprocessColumn).
-
Puis-je désactiver l’intégrité uniquement pour une colonne ?
- Le garde-fou est global à l’instance. Pour un contrôle plus fin, surchargez
processColumnpour décider au cas par cas si l’intégrité doit bloquer la migration et retournez la valeur d’origine si besoin.
- Le garde-fou est global à l’instance. Pour un contrôle plus fin, surchargez
-
Et si
ID_{TABLE}n’existe pas ?getIdValuetenteID_{TABLE}puis, en fallback, la première colonne duResultSet. Si aucune n’est accessible, la ligne est ignorée (pas d’UPDATE possible).