Dernière modification : Dec 08 , 2024

Objectifs

Nous allons dans cette leçon, remplacer le AdoptionRepository qui est basé sur une implémentation mémoire par une implémentation utilisant la base de données H2.

Création d'un nouvel adapteur pour les adoptions

Nous ne souhaitons pas supprimer le code qui utilise actuellement l'implémentation mémoire pour notre gestion d'adoptions. Nous allons donc utiliser des profils pour démarrer soit la version utilisant la mémoire soit la version H2.

Nous aurons donc un profil particulier pour charger la version H2 :

  • h2 : pour démarrer l'application avec l'implémentation H2
  • les autres profils utilisent l'implémentation mémoire.

Pour définir si la version H2 ou mémoire doit être activée, nous allons utiliser un feature toggle, soit une propriété qui contient une valeur qui change en fonction du profil et qui nous permet d'activer certaines portions du code.

Cette propriété sera nommée com.byoskill.adoptions.adapter.

Nous allons donc créer un fichier de configuration supplémentaire application-h2.properties dans le répertoire src/main/resources du projet backend-monster-adoption-store.

Le fichier application-h2.properties contiendra les propriétés suivantes :

com.byoskill.adoptions.adapter=h2
# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.active=true
quarkus.hibernate-orm.enabled=true
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.generate-ddl=true
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:file:../src/main/resources/data/database;AUTO_SERVER=true;DB_CLOSE_DELAY=-1
quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
quarkus.hibernate-orm.log.sql=true

Il est aussi nécessaire d'ajouter notre feature toggle dans le fichier application.properties pour activer l'implémentation mémoire quand le profile h2 n'est pas souhaité.

# Activer l'implémentation mémoire
com.byoskill.adoptions.adapter=memory

Avec Quarkus, il est possible de modifier au moment du build, les Beans qui doivent être activés en fonction du profil.

Créer la classe suivante dans le package com.byoskill :

package com.byoskill;

import com.byoskill.adapters.adoption.h2.H2AdoptionRepository;
import com.byoskill.adapters.adoptions.memory.AdoptionMemoryRepository;
import com.byoskill.domain.adoption.repository.AdoptionRepository;
import io.quarkus.arc.DefaultBean;
import io.quarkus.arc.properties.IfBuildProperty;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Produces;
import jakarta.persistence.EntityManager;

@Dependent
public class AdoptionConfiguration {

    @Produces
    @ApplicationScoped
    @IfBuildProperty(name = "com.byoskill.adoptions.adapter", stringValue = "h2")
    public AdoptionRepository H2AdoptionRepository(final EntityManager entityManager) {
        return new H2AdoptionRepository(entityManager);
    }

    @Produces
    @DefaultBean
    @ApplicationScoped
    public AdoptionRepository memoryAdoptionRepository() {
        return new AdoptionMemoryRepository();
    }
}

Créer notre implémentation pour H2

Nous allons utiliser les documentations suivantes pour cette partie de l'exercice :

Nous allons commencer par ajouter l'extension n quarkus-jdbc-h2 dans le fichier pom.xml du projet backend-monster-adoption-store :

quarkus ext add io.quarkus:quarkus-hibernate-orm
quarkus ext add io.quarkus:quarkus-jdbc-h2

Vérification de la santé de la source de données

Si vous utilisez l'extension quarkus-smallrye-health, les extensions quarkus-agroal et reactive client ajoutent automatiquement une vérification de la disponibilité pour valider la source de données.

Lorsque vous accédez au point de terminaison de disponibilité de santé de votre application, /q/health/ready par défaut, vous recevez des informations sur l'état de validation de la source de données. Si vous avez plusieurs sources de données, toutes les sources de données sont vérifiées, et en cas d'échec de validation d'une seule source de données, le statut passe à DOWN.

Ce comportement peut être désactivé en utilisant la propriété quarkus.datasource.health.enabled.

Pour exclure uniquement une source de données particulière de la vérification de santé, utilisez :

quarkus.datasource."nom-de-la-source-de-données".health-exclude=true

Métriques de la source de données

Si vous utilisez l'extension quarkus-micrometer ou quarkus-smallrye-metrics, quarkus-agroal peut contribuer à certaines métriques liées à la source de données dans le registre des métriques. Cela peut être activé en définissant la propriété quarkus.datasource.metrics.enabled sur true.

Traçage de la source de données

Pour utiliser le traçage avec une source de données, vous devez ajouter l'extension quarkus-opentelemetry à votre projet.

Vous n'avez pas besoin de déclarer un pilote différent car vous avez besoin de traçage. Si vous utilisez un pilote JDBC, vous devez suivre les instructions de l'extension OpenTelemetry ici.

Même avec toute l'infrastructure de traçage en place, le traçage de la source de données n'est pas activé par défaut, et vous devez l'activer en définissant cette propriété :


# Activer le traçage
quarkus.datasource.jdbc.telemetry=true

Ajouter la dépendance le POM

<dependency>
  <groupId>io.opentelemetry.instrumentation</groupId>
  <artifactId>opentelemetry-jdbc</artifactId>
</dependency>

Création du repository pour H2

Nous allons utiliser la documentation suivante.

Ajoutons l'extension Hibernate :

quarkus ext add io.quarkus:quarkus-hibernate-orm

Nous allons créer la classe MonsterEntity dans le package com.byoskill.adapters.adoption.h2.

Copier le code suivant dans la classe MonsterEntity :

package adapters.h2;

import adoption.model.Monster;
import jakarta.persistence.*;


@Entity
@Table(name = "monsters")
public class MonsterEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "name")
    private String name;

    @Column(name = "description")
    private String description;

    @Column(name = "image_url")
    private String imageUrl;

    @Column(name = "monsterUUID")
    private String monsterUUID;

    @Column(name = "price")
    private Integer price;

    @Column(name = "age")
    private Integer age;

    @Column(name = "location")
    private String location;

    @Column(name = "monsterId")
    private Integer monsterId;

    // Getters and Setters
    public MonsterEntity() {
    }

    public static MonsterEntity fromModel(final Monster monster) {
        final MonsterEntity monsterEntity = new MonsterEntity();
        monsterEntity.id = monster.getId();
        monsterEntity.name = monster.getName();
        monsterEntity.description = monster.getDescription();
        monsterEntity.monsterUUID = monster.getMonsterUUID();
        monsterEntity.price = monster.getPrice();
        monsterEntity.age = monster.getAge();
        monsterEntity.location = monster.getLocation();
        monsterEntity.monsterId = monster.getMonsterId();
        return monsterEntity;
    }

    public Monster toModel() {
        final Monster monster = new Monster();
        monster.setId(id);
        monster.setName(name);
        monster.setDescription(description);

        monster.setMonsterUUID(monsterUUID);
        monster.setPrice(price);
        monster.setAge(age);
        monster.setLocation(location);
        monster.setMonsterId(monsterId);
        return monster;
    }

    public Integer getId() {
        return id;
    }

    public void setId(final Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(final String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(final String description) {
        this.description = description;
    }

    public String getImageUrl() {
        return imageUrl;
    }

    public void setImageUrl(final String imageUrl) {
        this.imageUrl = imageUrl;
    }

    public String getMonsterUUID() {
        return monsterUUID;
    }

    public void setMonsterUUID(final String monsterUUID) {
        this.monsterUUID = monsterUUID;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(final Integer price) {
        this.price = price;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(final Integer age) {
        this.age = age;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(final String location) {
        this.location = location;
    }

    public Integer getMonsterId() {
        return monsterId;
    }

    public void setMonsterId(final Integer monsterId) {
        this.monsterId = monsterId;
    }
}

Nous devons désormais créer la classe H2AdoptionsRepository dans le package com.byoskill.adapters.adoption.h2.

Cette classe fournira le remplacement de notre composant d'adoption qui gérait les adoptions en mémoire.

Voici un code dont vous pouvez vous inspirer :

package com.byoskill.adapters.adoption.h2;

import com.byoskill.domain.adoption.model.Monster;
import com.byoskill.domain.adoption.repository.AdoptionRepository;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional;

import java.time.Duration;
import java.util.List;

public class H2AdoptionRepository implements AdoptionRepository {

    private final EntityManager entityManager;

    @Inject
    public H2AdoptionRepository(final EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Override

    public Multi<Monster> getAllMonsters() {
        final List<MonsterEntity> monsters = entityManager
                .createQuery("SELECT monster from MonsterEntity monster", MonsterEntity.class)
                .getResultList();
        return Multi.createFrom().items(monsters.stream()
                .map(MonsterEntity::toModel)
        );
    }


    @Transactional
    @Override
    public Uni<Monster> addMonsterToAdopt(final Monster monster) {
        final MonsterEntity entity = MonsterEntity.fromModel(monster);
        entityManager.persist(entity);
        return Uni.createFrom().item(entity.toModel());
    }


    @Override
    public Uni<Monster> getMonsterByUuid(final String uuid) {
        final TypedQuery<MonsterEntity> query = entityManager.createQuery("SELECT monster from MonsterEntity monster where monster.monsterUUID = :id", MonsterEntity.class);
        query.setParameter("id", uuid);
        final List<MonsterEntity> resultList = query.getResultList();
        return 1 == resultList.size() ? Uni.createFrom().item(resultList.get(0).toModel()) : Uni.createFrom().nullItem();
    }


    @Override
    public Multi<Monster> searchMonstersByName(final String name) {
        final TypedQuery<MonsterEntity> query = entityManager.createQuery("SELECT monster from MonsterEntity monster where monster.name = :name", MonsterEntity.class);
        query.setParameter("name", name);
        final List<MonsterEntity> resultList = query.getResultList();
        return Multi.createFrom().items(resultList.stream()
                .map(MonsterEntity::toModel)
        );
    }


    @Transactional
    @Override
    public void deleteMonsterByUuid(final String uuid) {
        entityManager.createQuery("DELETE FROM MonsterEntity monster where monster.monsterUUID = :id");
        entityManager.flush();
    }


    @Transactional
    @Override
    public Uni<Monster> updateMonsterByUUID(final String uuid, final Monster monster) {
        final var query = """
                UPDATE MonsterEntity monster set monster.name = :name,
                monster.age = :age, 
                monster.description = :description, 
                monster.imageUrl = :imageUrl, 
                monster.location = :location, 
                monster.price = :price 
                where monster.monsterUUID = :uuid
                """;
        entityManager.createQuery(query)
                .setParameter("uuid", uuid)
                .setParameter("name", monster.getName())
                .setParameter("age", monster.getAge())
                .setParameter("description", monster.getDescription())
                .setParameter("imageUrl", monster.getImage_url())
                .setParameter("location", monster.getLocation())
                .setParameter("price", monster.getPrice())
                .executeUpdate();

        return Uni.createFrom().item(monster);
    }


    @Override
    public Multi<Monster> searchMonstersByAge(final Integer age) {
        final TypedQuery<MonsterEntity> query = entityManager.createQuery("SELECT monster from MonsterEntity monster where monster.age = :age", MonsterEntity.class);
        query.setParameter("age", age);
        final List<MonsterEntity> resultList = query.getResultList();
        return Multi.createFrom().items(resultList.stream()
                .map(MonsterEntity::toModel)
        );
    }


    @Transactional
    @Override
    public Uni<Monster> changeName(final Monster entityToBeUpdated, final String newName) {
        final Uni<Monster> monsterByUuid = getMonsterByUuid(entityToBeUpdated.getMonsterUUID());
        return monsterByUuid
                .map(monster -> {
                    monster.setName(newName);
                    return monster;
                })
                .log("monster::changeName")
                .ifNoItem().after(Duration.ofMillis(500)).fail()
                .onItem().invoke(updatedMonster -> {
                    entityManager.merge(updatedMonster);
                });
    }
}

Redémarrez le backend en utilisant les profils h2 et dev pour substituer le backend vers la base de données H2.

Correction des erreurs d'exécution

Nous remarquons que l'appel à nos ressources REST plante désormais. En effet l'API Hibernate n'est pas réactive par défaut et bloque le thread principal de l'application.

Pour remédier le problème, nous allons annoter nos ressources REST avec l'annotation @Blocking.

Vous pouvez utiliser l'annotation @Blocking pour indiquer que le traitement est bloquant et doit être exécuté sur un thread de travail. Vous pouvez en savoir plus sur le traitement bloquant.

image

Worker threads

E utilisant cette annotation, nous indiquons que l'exécution de la méthode se réalisera dans un thread dédié dans la limite des threads disponibles sur la machine.

Voici un exemple de code qui corrige le souci avec Hibernate :

 @Blocking
    @GET
    public Uni<MonsterView> getAllMonsters() {

        return adoptionRepository.getAllMonsters()
                .log("getAllMonsters")
                .collect()
                .asList()
                .map(MonsterView::new);
    }

Mise à jour des tests unitaires

Vos tests devraient fonctionner avec l'implémentation H2 ou la version mémoire sans refactoring.

Implémentation d'Hibernate Reactive

En option vous pouvez remplacer Hibernate par Hibernate Reactive.

  • Quels sont les avantages d'Hibernate reactive avec Quarkus ?

Hibernate reactive]

Implémentation d'Hibernate avec Panache

Panache utilise le pattern ActiveRecord pour rendre la programmation avec Hibernate plus simple.

Vous pouvez trouver plus d'information ici : Panache