Lors du développement de services backend, il est facile de créer des problèmes si l'intégration de la base de données est mal implémentée. Cet article vous expliquera quelques bonnes pratiques pour travailler avec des bases de données relationnelles dans des services modernes et vous montrera également que la génération et la mise à jour automatiques du schéma ne sont sans doute pas une bonne idée.
J'utiliserai Flyway pour les migrations de bases de données, Spring Boot pour une configuration facile et H2 comme exemple de base de données.
Je n'ai pas couvert les informations de base sur ce que sont les migrations et comment elles fonctionnent. Voici de bons articles de Flyway :
Il y a longtemps, les développeurs initialisaient et mettaient à jour les bases de données en appliquant des scripts séparément de l'application. Cependant, personne ne le fait de nos jours car il est difficile de se développer et de se maintenir dans un état correct, ce qui entraîne de graves problèmes.
De nos jours, les développeurs utilisent principalement deux approches :
Génération automatique, par exemple, JPA ou Hibernate - la base de données s'initialise et se tient à jour en comparant les classes et l'état actuel de la base de données ; si des changements sont nécessaires, ils s'appliquent.
Migrations de base de données - les développeurs mettent progressivement à jour la base de données et les modifications s'appliquent automatiquement au démarrage, migrations de base de données.
De plus, si nous parlons de Spring, il existe une initialisation de base de données de base prête à l'emploi, mais elle est beaucoup moins avancée que ses analogues tels que Flyway ou Liquibase.
Pour montrer comment cela fonctionne, utilisons un exemple simple. Table users avec trois champs - id
, user_name
, email
:
Jetons un œil à celui généré automatiquement par Hibernate.
Hiberner l'entité :
@Entity @Table(name = "users") public class User { @Id @GeneratedValue private UUID id; @Column(name = "user_name", length = 64, nullable = false) private String userName; @Column(name = "email", length = 128, nullable = true) private String email; }
Pour activer la mise à jour du schéma, nous avons besoin de cette ligne dans la configuration de Spring Boot et il commence à le faire au démarrage :
jpa.hibernate.ddl-auto=update
Et connectez-vous depuis l'hibernation au démarrage de l'application :
Hibernate: create table users (id binary(255) not null, email varchar(128), user_name varchar(64) not null, primary key (id))
Après la génération automatique, il a créé un id
en tant que binary
avec une taille maximale de 255, c'est trop car l' UUID
ne se compose que de 36 caractères. Nous devons donc utiliser le type UUID
à la place, cependant, il ne génère pas de cette façon. Il peut être corrigé en ajoutant cette annotation :
@Column(name = "id", columnDefinition = "uuid")
Cependant, nous écrivons déjà la définition SQL dans la colonne, ce qui rompt l'abstraction de SQL vers Java.
Et remplissons le tableau avec quelques utilisateurs :
insert into users (id, user_name, email) values ('297a848d-d406-4055-8a6f-4a4118a44001', 'Artem', null); insert into users (id, user_name, email) values ('921a9d42-bf14-4c3f-9893-60f79cdd0825', 'Antonio', '[email protected]');
Imaginons, par exemple, qu'après un certain temps, nous souhaitions ajouter des notifications à notre application, et par conséquent suivre si un utilisateur souhaite les recevoir. Nous avons donc décidé d'ajouter une colonne receive_notifications
aux utilisateurs de la table et de la rendre non nulle.
Cela signifie que dans l'entité Hibernate, nous ajoutons la nouvelle colonne :
@Column(name = "receive_notifications", nullable = false) private Boolean receiveNotifications;
Après avoir démarré l'application, nous voyons l'erreur dans les journaux et aucune nouvelle colonne. C'est parce que la table n'est pas vide et nous devons définir une valeur par défaut pour les lignes existantes :
Error executing DDL "alter table users add column receive_notifications boolean not null" via JDBC Statement
Nous pouvons définir une valeur par défaut en ajoutant à nouveau une définition de colonne SQL :
columnDefinition = "boolean default true"
Et à partir des journaux Hibernate, nous pouvons voir que cela a fonctionné :
Hibernate: alter table users add column receive_notifications boolean default true not null
Cependant, imaginons que nous ayons besoin que receive_notifications
soit quelque chose de plus complexe, par exemple vrai ou faux, selon que l'e-mail est rempli ou non. Il est impossible d'implémenter cette logique uniquement avec Hibernate, nous avons donc besoin de migrations de toute façon.
En résumé, les principaux inconvénients de l'approche des schémas générés et mis à jour automatiquement :
C'est Java d'abord et par conséquent pas flexible en termes de SQL, non prévisible, orienté sur Java d'abord, et parfois ne fait pas les choses SQL comme vous l'attendez. Vous pouvez écrire des définitions SQL pour le mener, mais c'est limité par rapport au DDL SQL pur.
Parfois, il est impossible de mettre à jour les tables existantes et de faire quelque chose avec les données, et nous avons de toute façon besoin de scripts SQL. Dans la plupart des cas, cela se termine par une mise à jour automatique du schéma et la conservation des migrations pour la mise à jour des données. Il est toujours plus facile d'éviter de générer et de faire automatiquement tout ce qui concerne la couche de base de données dans les migrations.
De plus, ce n'est pas pratique lorsqu'il s'agit de développement parallèle car il ne prend pas en charge la gestion des versions et il est difficile de dire ce qui se passe avec le schéma.
Voici à quoi cela ressemble sans générer et mettre à jour automatiquement le schéma :
Script d'initialisation de la base de données :
ressources/db/migration/V1__db_initialization.sql
create table if not exists users ( id uuid not null primary key, user_name varchar(64) not null, email varchar(128) );
Remplissage de la base de données avec certains utilisateurs :
ressources/db/migration/V2__users_some_data.sql
insert into users (id, user_name, email) values ('297a848d-d406-4055-8a6f-4a4118a44001', 'Artem', null); insert into users (ID, USER_NAME, EMAIL) values ('921a9d42-bf14-4c3f-9893-60f79cdd0825', 'Antonio', '[email protected]');
Ajout du nouveau champ et définition de la valeur par défaut non triviale sur les lignes existantes :
ressources/db/migration/V3__users_add_receive_notification.sql
alter table users add column if not exists receive_notifications boolean; -- It's not a really safe with huge amount of data but good for the example update users set users.receive_notifications = email is not null; alter table users alter column receive_notifications set not null;
Et rien ne nous empêche d'utiliser l'hibernation si nous le choisissons. Dans les configurations, nous devons définir cette propriété :
jpa.hibernate.ddl-auto=validate
Hibernate ne générera plus rien. Il vérifiera uniquement si la représentation Java correspond à DB. De plus, nous n'avons plus besoin de mélanger du Java et du SQL pour effectuer la génération automatique d'Hibernate, il peut donc être concis et sans responsabilité supplémentaire :
@Entity @Table(name = "users") public class User { @Id @Column(name = "id") @GeneratedValue private UUID id; @Column(name = "user_name", length = 64, nullable = false) private String userName; @Column(name = "email", length = 128, nullable = true) private String email; @Column(name = "receive_notifications", nullable = false) private Boolean receiveNotifications; }
if not exists
/ if exists
, comme nous l'avons fait ci-dessus.V{version+=1}__description.sql
pour nommer les migrations au lieu d'utiliser V{datetime}__description.sql
. Le second est pratique et aidera à éviter les conflits de numéros de version dans le développement parallèle. Mais parfois, il est préférable d'avoir un conflit de noms plutôt que d'appliquer avec succès des migrations sans que les développeurs ne contrôlent les versions.
C'était beaucoup d'informations, mais j'espère que vous les trouverez utiles. Si vous utilisez la génération/mise à jour automatique du schéma, examinez de près ce qui se passe avec le schéma car il peut se comporter de manière inattendue. Et c'est toujours une bonne idée d'ajouter autant de description que possible pour le mener.
Mais il vaut mieux la prochaine fois envisager des migrations car cela soulagera les entités Java, supprimera les responsabilités excessives et vous permettra de mieux contrôler DDL.
Pour résumer les bonnes pratiques :
Vous pouvez trouver l'exemple entièrement fonctionnel sur GitHub .