Utiliser des schémas multiples dans Spring Boot : un guide pratique pour un code productif et testé

Alors que nous nous efforcions de supprimer l'un de nos systèmes de base de données, nous avons été confrontés à la tâche de migrer des données vers notre base de données MySQL existante. Cependant, les données ne correspondaient pas à nos données/modèles commerciaux actuels stockés dans la base de données. Cela nous a amenés à prendre une décision : créer une toute nouvelle instance MySQL ou établir un nouveau schéma au sein de la base de données existante. Le choix de ne pas utiliser une nouvelle base de données a été motivé par la complexité de la mise en place d'une nouvelle connexion et de la configuration.

Dans cet article, nous allons partager notre approche de la gestion des connexions à différents schémas au sein de la même base de données MySQL. Sans entrer dans les raisons du choix de MySQL par rapport à d'autres options de stockage de données potentiellement plus appropriées, nous nous concentrerons sur les aspects pratiques de la gestion efficace de plusieurs schémas.

Configuration pour la connexion

L'établissement d'une connexion à une instance MySQL implique généralement l'utilisation d'une URL de connexion. La structure de l'URL apparaît généralement comme suit :

 url: jdbc:mysql://<url>:<port>/<schemaName>?<additionalSettings> 

Heureusement, nous n'avons pas eu besoin de modifier quoi que ce soit dans l'URL de connexion. Le site <schemaName> dans l'URL définissait notre schéma existant, que nous appellerons, pour des raisons de simplicité, le schéma par défaut (même si ce n'est pas techniquement le schéma par défaut). Supposons que le schéma par défaut porte le nom de "schéma1", alors que le nouveau schéma est intitulé "schéma2". Par conséquent, sauf indication contraire, toutes nos entités d'application sont stockées dans le schéma1.

Comme il n'est pas nécessaire d'établir une nouvelle connexion à la base de données, la configuration d'éléments tels que le gestionnaire de transactions ou le pool de connexions Hikari est inutile. Il est essentiel d'être attentif à cela, car cela peut avoir certains effets secondaires qui peuvent passer inaperçus (par exemple, les requêtes vers le schéma2 utilisent le même pool de connexion Hikari que les requêtes vers le schéma1).

Mise en œuvre de la création de schémas et de l'addition de tableaux

Tout d'abord, vous devez créer un nouveau schéma dans votre base de données, une tâche réalisable manuellement ou par le biais d'un outil de gestion des changements dans la base de données. Dans notre cas, nous avons opté pour Liquibase. Liquibase ne dispose pas d'une balise pré-configurée pour la création de schémas, ce qui nous a obligés à écrire un script SQL personnalisé :

   <changeSet author="kai" id="20231010210000-1">
        <sql>
            create schema schema2;
        </sql>
    </changeSet>

Après avoir créé avec succès le nouveau schéma, la tâche suivante consiste à y introduire de nouvelles tables. Cela représente un changement par rapport à notre flux de travail habituel qui était centré exclusivement sur le schéma1.


    <createTable schemaName="schema2" tableName="first_table_in_new_schema">
        <column name="id" type="bigint" autoIncrement="true">
            <constraints primaryKey="true" nullable="false"/>
        </column>
        <column name="column_1" type="bigint">
            <constraints nullable="true"/>
        </column>
        ...
    </createTable>

En lettres grasses, j'ai souligné la partie cruciale indiquant comment instruire Liquibase pour mettre en œuvre les changements souhaités dans un schéma spécifique. C'est essentiellement tout ce qu'il faut faire pour que Liquibase utilise notre nouveau schéma. Si nous ne précisons pas schemaName, Liquibase defaults to the original schema (schema1).

Déclaration de schéma sur une entité JPA

Pour spécifier le schéma sur une entité JPA, nous devons modifier l'annotation @Table comme suit :

 @Entity
 @Table(name = "first_table_in_new_schema", catalog = "schema2")
 public class FirstTableInNewSchema {
 ...
 }  

Il est essentiel de noter que nous utilisons le champ catalog à cette fin. Bien qu'il y ait aussi un champ de schéma dans @Table, quand on travaille avec MySQL, il est crucial d'utiliser catalog.

Ceci couvre essentiellement toutes les étapes nécessaires. Lorsque vous utilisez le référentiel correspondant à l'entité ci-dessus, le nouveau schéma est automatiquement préfixé au nom de la table.

Gestion des tests de bout en bout avec H2 Database

Tout ce qui précède fonctionne parfaitement... jusqu'à ce que vous rencontriez le besoin d'exécuter vos tests de bout en bout à l'aide d'un système de base de données autre que MySQL. Alors que certains pourraient argumenter en faveur de l'utilisation de conteneurs de test MySQL pour assurer le test sur la même base de données sous-jacente, nous avons opté pour relever ce défi en utilisant notre base de données H2 existante. Afin de reproduire fidèlement la base de données de production, nous utilisons des scripts Liquibase identiques pour nos tests et nos environnements de production.

Le défi se pose lorsque H2 demande d'utiliser le champ schéma dans l'annotation @Table au lieu du champ catalogue.

Simple, non ? Il suffit d'inclure les deux annotations dans l'entité :

 @Entity
 @Table(name = "first_table_in_new_schema", schema = "schema2", catalog = "schema2")
 public class FirstTableInNewSchema {
 ...
 }

Cependant, cette approche échoue et vous remarquerez une erreur de validation de schéma au démarrage :

 Caused by : org.hibernate.tool.schema.spi.SchemaManagementException : Schema-validation : missing table [schema2.schema2.first_table_in_new_schema]

Pour résoudre ce problème, nous devons supprimer le champ de catalogue au début du test de bout en bout. Spring/Hibernate utilise l'interface PhysicalNamingStrategy pour créer des noms efficaces pour les tables et les champs. Il y a quelques implémentations par défaut qui peuvent par exemple remplacer underscore par dots, ou changer camelcase par snakecase. Nous avons utilisé la stratégie SpringPhysicalNamingStrategy, surmontée d'une stratégie de nommage personnalisée uniquement pour les tests E2E. Cette stratégie supprime la valeur du champ catalogue. Dans le fichier application.yml pour les tests E2E, nous avons configuré Spring/Hibernate pour utiliser notre CustomPhysicalNamingStrategy :

 le saut :
  jpa :
    hibernate :
      naming :
        physical-strategy : path.to.config.CustomPhysicalNamingStrategy

Le CustomPhysicalNamingStrategy ressemble à cela :

 @Configuration
 @Profile("test")
 public class CustomPhysicalNamingStrategy extends SpringPhysicalNamingStrategy {

    @Override
    public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment jdbcEnvironment) {
        if (name != null) {
            return super.toPhysicalCatalogName(null, jdbcEnvironment);
        }
        return super.toPhysicalCatalogName(name, jdbcEnvironment);
    }
 }

Explication des annotations et du code utilisé :

@Configuration: Nécessaire pour que le bean soit initialisé au démarrage de l'application.

@Profile("test")Spécifie que cette balise n'est initialisée que lorsque l'application démarre avec le profil de test (dans notre cas, pendant les tests E2E). La stratégie CustomPhysicalNamingStrategy doit être utilisée exclusivement dans les tests E2E, comme configuré dans le fichier application.yml. Cette annotation empêche le haricot d'être initialisé en dehors d'un test E2E.

toPhysicalCatalogNameCette méthode vérifie si le nom du catalogue est défini. Si c'est le cas, nous le mettons à zéro au démarrage, en "supprimant" effectivement le champ catalogue dans l'annotation @Table s'il est défini.

Information concernant la gestion des transactions à travers de multiples schémas

Comme nous utilisons la même URL de connexion pour les deux schémas, toutes les configurations de base de données sont partagées, comme mentionné précédemment. Cela signifie que lorsque l'on opère dans un contexte @Transactional, la transaction s'étend sur les deux schémas si des modifications sont apportées aux deux. Par conséquent, si vous écrivez dans le schéma par défaut et aussi dans le schéma2 dans une transaction, et qu'un problème survient lors de l'écriture dans le schéma2, l'ensemble de la transaction sera annulée. Même s'il n'y a pas eu d'erreurs lors de l'écriture dans le schéma1, les modifications seront également annulées.

Kai Müller
Ingénieur logiciel