Blog

Migration Oracle Forms : pourquoi ma tentative avec APEX sur plus de 200 formulaires a échoué, et pourquoi j’ai créé ORMIT™ Open Java

J’ai déjà entrepris de migrer une application Oracle Forms vers APEX. L’application comptait plus de 200 formulaires et quelques rapports. J’ai migré quelques formulaires, et il est vite devenu clair qu’APEX n’était pas le bon choix pour une migration de cette taille : chaque formulaire prenait trop de temps à migrer, et chacun avait l’allure et le comportement d’une application distincte. C’est ce projet qui m’a amené à créer ORMIT™ Open Java : réaliser une migration d’Oracle Forms vers Java rapidement et sans douleur, tout en offrant tout ce qu’offre une application web moderne. Voici ce qui s’est passé, ce que j’ai construit à la place, et le code qui montre la différence.

Une application Oracle Forms de plus de 200 formulaires

C’était une application de taille moyenne : plus de 200 formulaires Oracle Forms et quelques rapports. L’objectif était de quitter Oracle Forms pour une application web moderne.

Sur papier, Oracle APEX était le choix naturel. Il s’exécute dans Oracle Database, et il parle SQL et PL/SQL, les langages qu’une équipe Forms connaît déjà. C’est donc par là que j’ai commencé.

Reconstruire les formulaires dans Oracle APEX

Dans APEX, il n’y avait aucun module Forms à convertir. J’ai reconstruit chaque écran dans Page Designer. Un bloc Forms devenait une région de formulaire, une grille interactive ou un rapport, selon ce qui semblait convenir. Chaque déclencheur devenait une validation, un calcul, un processus ou une action dynamique, réécrit à la main.

Page Designer d’Oracle APEX affichant la région de formulaire Item - Details avec les éléments de page P7_PRODUCT_ID, P7_PRICE, P7_QUANTITY et P7_QUANTITY_SHIPPED, et l’éditeur de propriétés réglé sur le type Form pour la table S_ITEM
Un bloc Forms reconstruit dans Page Designer : le bloc S_ITEM de Summit devient une région de formulaire sur la table S_ITEM, et chaque élément Forms, un élément de page. Capture d’écran : Oracle LiveLabs, atelier Forms to APEX (UPL 1.0).

J’ai migré quelques formulaires de cette façon. Pour un écran, ça fonctionnait. Avec 200 formulaires, chacun de ces choix devait être refait, formulaire après formulaire.

Page Designer d’Oracle APEX avec la page 6 Orders Form : les éléments de la région Ord placés dans la mise en page, et l’éditeur de propriétés montrant P6_PAYMENT_TYPE en groupe de boutons radio avec une liste de valeurs partagée
Le formulaire de commande de Summit reconstruit dans Page Designer. Le type de chaque élément, sa liste de valeurs et sa place dans la page, comme le groupe de boutons radio du type de paiement, se règlent à la main dans l’éditeur de propriétés, une page à la fois. Capture d’écran : Oracle LiveLabs, atelier Forms to APEX (UPL 1.0).

Aujourd’hui, ce processus est plus rapide. Avec Oracle AI Database 26ai, des agents de codage IA comme Claude Code peuvent utiliser les skills APEX d’Oracle pour générer les pages. Mais au bout du compte, vous héritez toujours d’une application sans apparence centralisée, sans véritable thème qui décide de l’allure de chaque écran, comme en ont aujourd’hui les applications web modernes.

200 formulaires, 200 petites applications

Il est vite devenu clair que j’avais un problème. Deux, en fait.

Le premier était le temps. Chaque formulaire prenait trop de temps à migrer, et il y en avait 200.

Le second était pire. Les formulaires se comportaient davantage comme des applications distinctes que comme une seule application. L’interface et l’expérience utilisateur changeaient d’un formulaire à l’autre, et chaque différence était une chose de plus à maintenir, formulaire par formulaire. Ça devenait un cauchemar de maintenance.

APEX donnait bien un thème à l’application, Universal Theme. Mais le thème ne décidait pas de la façon dont un écran était construit. Les types de régions, les options de gabarit, le CSS et le JavaScript de page se réglaient page par page.

J’ai tenté de créer mes propres composants APEX

J’ai essayé de régler le problème de l’intérieur. J’ai créé mes propres composants pour APEX, pour que les formulaires aient belle allure et fonctionnent comme les Oracle Forms d’origine, mais de façon uniforme.

Dans APEX, un composant personnalisé est un plug-in ou un gabarit modifié : son propre PL/SQL, JavaScript et CSS, stocké dans l’application et maintenu à la main à chaque mise à niveau d’APEX. Chaque page devait encore choisir de l’utiliser, et de l’utiliser de la même façon.

C’était tout simplement trop de travail à maintenir dans le temps. Au lieu de gérer une application, je gérais chaque formulaire. Pour une migration de plus de 200 formulaires, APEX n’était pas le bon choix.

Ce que j’attendais d’une migration Oracle Forms

Ce projet m’a appris ce qu’une migration d’envergure devait livrer. Pas 200 écrans reconstruits, mais une seule application web moderne, avec tout ce qu’a une application web :

  • un seul thème pour toute l’application
  • des composants et des mises en page gérés une seule fois, pour tous les formulaires
  • des écrans adaptatifs
  • une API REST que d’autres systèmes peuvent appeler
  • la logique d’affaires des formulaires reprise, pas réécrite à la main

Les convertisseurs Forms vers Java sur le marché n’offraient pas cela non plus. Aucun ne s’approchait d’une véritable application web moderne. J’ai donc entrepris d’en construire un.

Migration automatisée d’Oracle Forms avec ORMIT™ Open Java

J’ai conçu ORMIT™ Open Java (OOJ) pour convertir les modules Oracle Forms en un frontend TypeScript, en React ou en Angular, et un backend Java Spring Boot, sur la même base de données Oracle. Chaque choix ci-dessous répond à l’un des problèmes que j’ai eus avec APEX.

Les exemples viennent de Summit, l’application de démonstration Forms publique d’Oracle, migrée avec OOJ : 4 formulaires, 16 blocs, 130 éléments et 89 déclencheurs. Chaque projet OOJ part du même gabarit, passe par le même pipeline et s’exécute sur le même code partagé.

Migration automatisée : un seul pipeline pour tous les formulaires

Dans APEX, j’avais reconstruit chaque écran à la main. Dans OOJ, chaque module Forms passe par les mêmes étapes :

# Après l’export de chaque .fmb en XML avec Forms2XML
workspace init SUMMIT -d dbhost:1521/ORCL -u summit   # lit les tables, fonctions et paquetages de la base de données
normalize                                             # donne aux fichiers des modules les noms attendus par les outils
extractall                                            # un fichier .sql par déclencheur et unité de programme
convertall                                            # PL/SQL vers TypeScript, triés dans pass/ et fail/
generate ORDERS                                       # frontend, backend et configuration REST

Un seul thème pour toute l’application

Dans APEX, un seul thème n’avait pas empêché chaque page d’avoir une allure différente. Dans OOJ, j’ai mis toute l’apparence de l’application dans un seul fichier, theme.ts. En voici le début dans Summit :

const theme: ThemeOptions = {
  direction: "ltr",
  // Emit theme values as --mui-* CSS variables so plain CSS (app.css) reads the same source of truth.
  cssVariables: true,
  palette: {
    primary: {
      // Renaps brand red
      main: "#ed1c24",
      light: "#f04c52",
      dark: "#b5141a",
      contrastText: "#fff",
    },
    appBar: {
      bg: "#ed1c24",
      fg: "#ffffff",
    },
    menu: {
      bg: "#222222",
      headerBg: "#000000",
    },
    // Fill for fields the user can't type into (Oracle Forms' read-only affordance).
    field: {
      readOnly: "#e4e7ea",
    },
    mode: "light",
  },
  spacing: (factor: number) => `${0.25 * factor}rem`, // (Bootstrap strategy)
  shape: {
    borderRadius: 4,
  },

Avec cssVariables activé, chaque valeur devient aussi une variable CSS. Pour changer l’image de marque d’une application, on copie un fichier d’exemple vers theme.override.ts :

export default {
  palette: {
    primary: { main: "#0057b8", light: "#3d82d6", dark: "#003f87", contrastText: "#fff" },
    appBar: { bg: "#0057b8", fg: "#ffffff" },
    menu: { bg: "#1b2a3a", headerBg: "#0e1824" },
  },

Il est fusionné en profondeur par-dessus le thème de base. Un changement de primary.main à cet endroit atteint les boutons, la barre d’application et les onglets de tous les formulaires.

Le comportement s’y trouve aussi. Une seule règle colore en jaune le champ actif dans tous les formulaires :

// Yellow focus fill applied app-wide to the focused input.
"&:focus-within": {
  backgroundColor: "yellow",
},

Chaque formulaire conserve les couleurs que définissait sa source Forms, ses attributs visuels, dans une petite feuille de style générée et limitée à ce formulaire. Tout le reste vient du thème.

Une seule bibliothèque de composants React pour tous les formulaires

Mes composants APEX ne fonctionnaient que lorsque chaque page choisissait de les utiliser. Dans OOJ, j’ai retiré ce choix : un formulaire généré ne contient aucun style et presque aucun balisage. Ses champs sont des données. Voici le champ Name du canevas client de Summit :

{
  grid: {
    columnStart: 7,
    columnEnd: 93,
    rowStart: 4,
    rowEnd: 5,
  },
  components: [
    {
      type: LabelTypes.BODY1,
      label: "Name",
      colSize: 2,
      ...
    },
    {
      name: "name",
      colSize: 10,
      type: InputTypes.TEXTFIELD,
      required: true,
      inputProps: {
        item: "name",
        block: "sCustomer",
      },
    },
  ],
},

Les valeurs de grille placent le champ sur le canevas. Le type dit quoi dessiner. Un seul hook partagé transforme chaque type en composant partagé :

if (isMemberOf(inputTypesValues, type)) {
  return toolTipWrapper(
    <InputWrapper input={input} itemProps={itemProps} value={inputValue} focusRef={focusRef} />
  );
}
if (isMemberOf(buttonTypesValues, type)) {
  return toolTipWrapper(<ButtonWrapper input={input} itemProps={itemProps} focusRef={focusRef} />);
}
if (isMemberOf(dataGridTypesValues, type)) {
  // The generator gives every datagrid its block and columns.
  return <DataGridWrapper input={input as DataGridInput} itemProps={itemProps} />;
}

Chaque élément texte de chaque formulaire aboutit au même Text.tsx, une enveloppe autour du TextField de MUI. Un correctif ou un nouveau style à cet endroit atteint tous les formulaires dans la même version.

Les 4 formulaires de Summit ont donné 114 fichiers et 8 592 lignes de configuration et de logique. Les 59 composants qu’ils utilisent se trouvent en dehors de tous les formulaires, dans common/components, des champs texte aux grilles de données, en passant par les listes de valeurs et les menus. C’est ce que j’avais tenté de construire à la main dans APEX. Cette fois, un formulaire ne pouvait pas dessiner un champ autrement.

Mises en page et écrans adaptatifs

Forms place chaque élément à des coordonnées fixes sur un canevas. Je ne voulais pas copier ces positions : le convertisseur transforme donc chaque canevas en une grille CSS de 96 colonnes, et chaque élément en cellules de cette grille :

function getCanvasGridSize()
{
    return 96;
}
...
$row = floor($item["YPosition"] / $gridSizeY);
$col = floor($item["XPosition"] / $gridSizeX);
$rowEnd = floor(($item["YPosition"] + $item["Height"]) / $gridSizeY);
$colEnd = floor(($item["XPosition"] + $item["Width"]) / $gridSizeX);

À 1 200 px et plus, des colonnes égales conservent les proportions du formulaire d’origine. En dessous, le conteneur de grille partagé abandonne la mise en page Forms et empile les champs sur une seule colonne :

sx={
  isDesktop
    ? {
        gridTemplateColumns: template?.columns,
        gridTemplateRows: template?.rows,
        ...sx,
      }
    : { gridTemplateColumns: "minmax(0, 1fr)", ...sx }
}

Sous 1 200 px, le menu devient aussi un tiroir, et sous 600 px chaque libellé et chaque champ prend toute la largeur. Rien de cela ne se trouve dans un formulaire. Tout est dans les composants partagés de grille, de menu et de fenêtre, si bien que chaque formulaire est adaptatif dès le jour où il est généré.

Le cadre de l’application fonctionne de la même façon. Un formulaire généré ne fait que déclarer ses fenêtres, ses canevas et ses menus :

export const routes = {
  name: FORM_NAME,
  menuNames: ["mSummit"],
  windows: [
    {
      name: "WIN_ORDER",
      title: "Orders and Items",
      toolbar: <Toolbar />,
      canvas: [
        {
          name: "cvOrder",
          component: <CvOrder />,
        },

Le composant partagé Window transforme chaque fenêtre en barre d’application, menu Forms converti, barre d’outils et canevas, ou en boîte de dialogue lorsque la fenêtre a une taille.

Des déclencheurs conservés presque ligne pour ligne

Dans APEX, j’avais réécrit chaque déclencheur à la main. Dans OOJ, le convertisseur les reprend. Voici un déclencheur WHEN-VALIDATE-ITEM du formulaire ORDERS de Summit, sur le numéro de produit d’une ligne de commande :

SELECT name, suggested_whlsl_price
INTO :S_ITEM.description, :S_ITEM.price
FROM S_PRODUCT
WHERE :S_ITEM.product_id = id;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    MESSAGE('Invalid Product Id!');
    RAISE FORM_TRIGGER_FAILURE;

Et le TypeScript qu’OOJ en a généré :

const productIdWhenValidateItem = async () => {
  try {
    //@OOJNOTE: sqlQuery#gQeJ8SocSvirc0YDpu9-MQ has been automaticaly commented out by Ormit Open Java:
    // SELECT name, suggested_whlsl_price INTO :S_ITEM.description, :S_ITEM.price FROM S_PRODUCT WHERE :S_ITEM.product_id = id;
    const resultgQeJ8SocSvirc0YDpu9MQ = (await sqlQuery("gQeJ8SocSvirc0YDpu9-MQ", [
      getValue("S_ITEM.PRODUCT_ID"),
    ])) as { description: string | null; price: number | null } | null | undefined;
    setValue("S_ITEM.DESCRIPTION", resultgQeJ8SocSvirc0YDpu9MQ?.description as OracleScalar);
    setValue("S_ITEM.PRICE", resultgQeJ8SocSvirc0YDpu9MQ?.price as OracleScalar);
    //@OOJNOTE: endSqlQuery#gQeJ8SocSvirc0YDpu9-MQ
  } catch (error) {
    recordException(error);
    if (error instanceof NoDataFound) {
      message("Invalid Product Id!");
      throw new FormTriggerFailure();
    } else {
      throw error;
    }
  }
};

MESSAGE est devenu message(), RAISE FORM_TRIGGER_FAILURE est devenu throw new FormTriggerFailure(), et NO_DATA_FOUND est devenu une exception NoDataFound. Les fonctions intégrées (built-ins) de Forms gardent leur nom, en camelCase, et sont implémentées une seule fois dans le paquet OOJ Core : environ 300 fonctions intégrées et utilitaires partagés par tous les formulaires.

Le SELECT ne s’exécute pas dans le navigateur : il devient un identifiant de requête enregistré, et le SQL reste sur le serveur.

Dans Summit, les 89 déclencheurs sont devenus 89 fonctions TypeScript. Sur l’ensemble des déclencheurs, des unités de programme et du code des menus, 1 980 lignes de PL/SQL sont devenues 1 772 lignes de TypeScript.

Une API REST Java que d’autres systèmes peuvent appeler

Dans APEX, la logique d’une page vivait dans ses processus et ses actions dynamiques, qu’aucun autre système ne pouvait appeler, et une API REST était un travail à part dans ORDS. Dans OOJ, le backend est en Spring Boot sur Java 21, connecté à Oracle avec le pilote JDBC thin, et l’API REST qu’utilisent les écrans React est celle que n’importe quel autre système peut appeler.

Un seul contrôleur sert tous les blocs. Le générateur n’écrit que la liste des blocs qu’il expose :

{ "endpoint": "sCustomers", "entity": "sCustomer" },
...
{ "endpoint": "sOrds", "entity": "sOrd" },

Chaque entrée donne à son bloc les mêmes routes :

OpérationRequête HTTP
InterrogationGET /api/sOrds?search=…&orderBy=…&page=…&limit=…
InsertionPOST /api/sOrds
Mise à jourPUT /api/sOrds/{id}
SuppressionDELETE /api/sOrds/{id}

Un autre système s’authentifie, puis envoie son jeton avec chaque requête. La recherche s’écrit en JSON, avec des opérateurs comme $eq, $like et $in :

curl -X POST https://your-server:8084/api/authenticate \
     -H 'Content-Type: application/json' \
     -d '{"username":"…","password":"…"}'
# {"token":"…"}

curl -G https://your-server:8084/api/sOrds \
     -H "Authorization: Bearer $TOKEN" \
     --data-urlencode 'search={"customer_id":{"$eq":201}}' \
     --data-urlencode 'page=1' --data-urlencode 'limit=20'
# {"meta":{"totalItems":…,"totalPages":…,"currentPage":1,"itemsPerPage":20},"data":[…]}

Trois détails comptaient pour moi :

  • Les valeurs de recherche sont toujours des paramètres liés, jamais collées dans le SQL.
  • Chaque insertion et chaque mise à jour, qu’elle vienne des écrans ou d’un autre système, passe d’abord par un serveur de validation qui exécute à nouveau les règles du formulaire. Un autre système ne peut pas contourner le formulaire.
  • Les modifications se font dans une session proxy Oracle au nom de l’utilisateur connecté, si bien que l’audit de la base de données porte son nom, et non celui d’un compte partagé.

Le PL/SQL stocké reste dans la base de données et s’appelle lui aussi par REST. Dans Summit, la fonction GET_PRODUCT_IMAGE est une entrée de procedures.json, et le déclencheur qui l’utilisait appelle maintenant GET /api/action/getProductImage.

L’unité, c’est l’application, pas le formulaire

Dans ma migration APEX, l’unité de travail était la page : chaque formulaire était construit, stylé et maintenu séparément.

Dans OOJ, j’ai fait de l’application l’unité. Un formulaire, ce sont des données et de la logique générées : ses champs, leur place sur la grille, ses déclencheurs, ses requêtes. Ce qui décide de son apparence et de son comportement existe une seule fois, dans le code partagé : le thème, les composants, le cadre de l’application, les fonctions intégrées de Core et le contrôleur REST. Le 200e formulaire ajoute des données, pas un nouveau design. Un changement au thème ou à un composant atteint tous les formulaires.

La base de données ne bouge pas non plus. Les tables, les contraintes, les paquetages PL/SQL et les privilèges sont les mêmes avant et après.

Quand cela ne s’applique pas : avec une poignée de formulaires, ou pour repenser une application plutôt que la migrer, APEX peut encore être le bon outil. Si je reprenais cette voie, je construirais d’abord les composants et les modèles de page, avant la première page, et j’y astreindrais chaque page.

Oracle APEX et ORMIT™ Open Java côte à côte

Ma migration APEXORMIT™ Open Java
Un formulaireReconstruit à la main dans Page DesignerGénéré par le même pipeline, puis révisé
ThèmeUniversal Theme, mais régions, options de gabarit et CSS de page réglés page par pageUn seul theme.ts et un fichier de surcharge, publiés en variables CSS
ComposantsMes propres plug-ins et gabarits, maintenus en vie à la main59 composants React partagés ; un formulaire ne contient que des données
Mise en pageDisposée page par pageCoordonnées Forms converties en une grille de 96 colonnes
Écrans adaptatifsLa grille d’Universal Theme, page par pageUne seule colonne sous 1 200 px, menu en tiroir, grâce aux composants partagés
DéclencheursRéécrits en validations, processus et actions dynamiquesConvertis en TypeScript, avec les mêmes noms de fonctions intégrées
API RESTTravail à part dans ORDS, en dehors des pagesGénérée pour chaque bloc, en JSON sur HTTPS avec un jeton
Base de donnéesInchangéeInchangée, PL/SQL appelé par REST

Ce que j’ai appris

La qualité d’une migration Oracle Forms ne se décide pas sur le premier formulaire. Elle se décide sur le 200e. Dans APEX, chaque nouveau formulaire était une nouvelle occasion de dériver, et la cohérence dépendait de la discipline de la personne qui construisait la page suivante.

Ce que j’éviterais : bâtir une bibliothèque de composants en marge d’un outil page par page, en espérant que le résultat devienne une seule application. La cohérence doit venir de l’architecture. C’est la leçon sur laquelle j’ai bâti OOJ.

Si vous hésitez entre APEX et un backend Java avec un frontend React ou Angular, j’ai comparé leurs fonctionnalités en détail dans Migration Oracle Forms vers APEX vs React/Angular : un comparatif des fonctionnalités.