4 TP 4 GraphQL Spring Boot
4 TP 4 GraphQL Spring Boot
4 TP 4 GraphQL Spring Boot
I. Objectif du TP ................................................................................................................................................... 3
Conclusion ..............................................................................................................................................................44
GraphQL (Graph Query Language) a été développé par Facebook, qui a commencé à l'utiliser pour les
applications mobiles en 2012. La spécification de GraphQL est devenue Open Source en 2015. Elle est
désormais supervisée par GraphQL Foundation (https://graphql.org/foundation/).
GraphQL est un langage de requête et un environnement d'exécution côté serveur pour les APIs qui
permet de fournir aux clients uniquement les données qu'ils ont demandées.
GraphQL est conçu pour mettre à la disposition des développeurs des API rapides, flexibles et faciles à
utiliser.
Utilisé à la place de REST, GraphQL permet aux développeurs de créer des requêtes qui extraient les
données de plusieurs sources à l'aide d'un seul appel d'API.
En outre, avec GraphQL, les équipes chargées de la maintenance des API peuvent librement ajouter ou
retirer des champs sans perturber les requêtes existantes. De leur côté, les développeurs peuvent créer des
API en utilisant les méthodes de leur choix. La spécification de GraphQL garantit que leur fonctionnement
sera prévisible auprès des clients.
Un schéma GraphQL est constitué de types d'objets, qui définissent le genre d'objet qu'il est possible
de demander et les champs qu'il contient.
Lorsque les requêtes arrivent, GraphQL les compare au schéma, puis exécute celles qui ont été
validées.
Le développeur de l'API associe chaque champ d'un schéma à une fonction nommée résolveur. Le
résolveur est appelé pour produire une valeur au cours de l'exécution.
III. Prérequis
- IntelliJ IDEA ;
- JDK version 17 ;
- Une connexion Internet pour permettre à Maven de télécharger les librairies.
c. Le fichier pom.xml
- Ajouter au niveau du fichier pom.xml la dépendance de la librairie ModelMapper suivante :
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.0</version>
</dependency>
Explications :
ModelMapper permet de convertir les objets DTO (Data Transfer Object) vers les objets BO
(Business Object) et vice versa. L’objectif est de ne pas utiliser les BO dans la couche
présentation.
d. Développement des classes Model
- Créer le package ma.formations.graphql.enums et ensuite créer les enums suivants :
L’enum AccountStatus :
package ma.formations.graphql.enums;
L’enum TransactionType :
package ma.formations.graphql.enums;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
@Inheritance(strategy = InheritanceType.JOINED)
public class User {
@Id
@GeneratedValue
protected Long id;
protected String username;
protected String firstname;
protected String lastname;
@OneToMany(mappedBy = "user")
private List<BankAccountTransaction> bankAccountTransactionList;
}
}
La classe Customer :
package ma.formations.graphql.service.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToMany;
import jakarta.persistence.PrimaryKeyJoinColumn;
import lombok.*;
import java.util.List;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
@PrimaryKeyJoinColumn(name = "id")
public class Customer extends User {
@Column(unique = true)
private String identityRef;
@OneToMany(mappedBy = "customer")
private List<BankAccount> bankAccounts;
}
La classe BankAccount :
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import ma.formations.graphql.enums.AccountStatus;
import java.util.Date;
import java.util.List;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class BankAccount {
@Id
@GeneratedValue
private Long id;
private String rib;
private Double amount;
private Date createdAt;
@Enumerated(EnumType.STRING)
private AccountStatus accountStatus;
@ManyToOne
private Customer customer;
@OneToMany(mappedBy = "bankAccount")
private List<BankAccountTransaction> bankAccountTransactionList;
La classe BankAccountTransaction :
package ma.formations.graphql.service.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import ma.formations.graphql.enums.TransactionType;
import java.util.Date;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class BankAccountTransaction {
@Id
@GeneratedValue
private Long id;
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;
@ManyToOne
private User user;
}
La classe GetTransactionListBo :
package ma.formations.graphql.service.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class GetTransactionListBo {
private String rib;
private Date dateTo;
private Date dateFrom;
}
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
@Data
public class CommonTools {
@Value("${graphql.date.format}")
private String dateFormat;
Explications :
L’annotation @Value permet d’injecter la valeur de la clé graphql.date.format qui existe au
niveau du fichier application.properties.
La syntaxe ${key} est de Spring EL (Spring Expression Language).
f. Configuration de ModelMapper
- Créer le package ma.formations.graphql.config.
- Dans ce dernier, créer la classe ModelMapperConfig suivante :
package ma.formations.graphql.config;
import lombok.AllArgsConstructor;
import ma.formations.graphql.common.CommonTools;
import ma.formations.graphql.service.exception.BusinessException;
import org.modelmapper.AbstractConverter;
import org.modelmapper.Converter;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.text.ParseException;
import java.util.Date;
@Configuration
@AllArgsConstructor
public class ModelMapperConfig {
private CommonTools tools;
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().
setMatchingStrategy(MatchingStrategies.LOOSE).
setFieldMatchingEnabled(true).
setSkipNullEnabled(true).
setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE);
Converter<Date, String> dateToStringConverter = new AbstractConverter<>() {
@Override
public String convert(Date date) {
return tools.dateToString(date);
}
};
Converter<String, Date> stringToDateConverter = new AbstractConverter<>() {
@Override
public Date convert(String s) {
try {
return modelMapper;
}
}
Explications :
La stratégie LOOSE permet le mappage entre des champs portant des noms et des types
différents, tandis que la stratégie STRICT applique des règles de mappage strictes et déclenche
une exception lorsqu'un mappage ne peut pas être effectué.
setSkipNullEnabled(true) permet au ModelMapper d’ignorer les champs nuls dans la
conversion.
setFieldMatchingEnabled(true) et setFieldAccessLevel(AccessLevel.PRIVATE) sont nécessaires
lorsque vous souhaitez convertir une JavaBean vers des classes qui sont des Builders. Dans ce
cas, le ModelMapper va passer les valeurs en utilisant les variables d’instances directement
sans passer par les Setters. Remarquer qu’il n’y a pas les Setters dans les classes qui respectent
le Design Pattern Builder.
Remarquer que nous avons ajouté deux convertisseurs (Converter interface) qui seront utilisés
par ModelMapper pour convertir les dates en format String et vice versa.
import lombok.AllArgsConstructor;
import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class CustomerDto {
private Long id;
private String username;
private String identityRef;
private String firstname;
private String lastname;
}
La classe AddCustomerRequest :
package ma.formations.graphql.dtos.customer;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class AddCustomerRequest {
private String username;
private String identityRef;
private String firstname;
private String lastname;
}
La classe AddCustomerResponse :
package ma.formations.graphql.dtos.customer;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class AddCustomerResponse {
private String message;
private Long id;
private String username;
private String identityRef;
private String firstname;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import ma.formations.graphql.dtos.customer.CustomerDto;
import ma.formations.graphql.enums.AccountStatus;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class BankAccountDto {
private Long id;
private String rib;
private Double amount;
private String createdAt;
private AccountStatus accountStatus;
private CustomerDto customer;
}
La classe AddBankAccountRequest :
package ma.formations.graphql.dtos.bankaccount;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class AddBankAccountRequest {
private String rib;
private Double amount;
private String customerIdentityRef;
}
La classe AddBankAccountResponse :
package ma.formations.graphql.dtos.bankaccount;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.AllArgsConstructor;
import lombok.Builder;
GraphQL et Spring Boot 15
import lombok.Data;
import lombok.NoArgsConstructor;
import ma.formations.graphql.dtos.customer.CustomerDto;
import ma.formations.graphql.enums.AccountStatus;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class AddBankAccountResponse {
private String message;
private Long id;
private String rib;
private Double amount;
private String createdAt;
private AccountStatus accountStatus;
private CustomerDto customer;
}
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import ma.formations.graphql.dtos.bankaccount.BankAccountDto;
import ma.formations.graphql.dtos.user.UserDto;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class TransactionDto {
private Long id;
private String createdAt;
private String transactionType;
private Double amount;
private BankAccountDto bankAccount;
private UserDto user;
}
La classe AddWirerTransferRequest :
package ma.formations.graphql.dtos.transaction;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
La classe AddWirerTransferResponse :
package ma.formations.graphql.dtos.transaction;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AddWirerTransferResponse {
private String message;
private TransactionDto transactionFrom;
private TransactionDto transactionTo;
}
La classe GetTransactionListRequest :
package ma.formations.graphql.dtos.transaction;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class GetTransactionListRequest {
private String rib;
private String dateTo;
private String dateFrom;
}
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class UserDto {
import ma.formations.graphql.service.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
L’interface CustomerRepository :
package ma.formations.graphql.dao;
import ma.formations.graphql.service.model.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
L’interface BankAccountRepository :
package ma.formations.graphql.dao;
import ma.formations.graphql.service.model.BankAccount;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
L’interface BankAccountTransactionRepository :
package ma.formations.graphql.dao;
import ma.formations.graphql.service.model.BankAccountTransaction;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Date;
import java.util.List;
Explications :
Dans ce fichier, nous configurons l’URL de la base de données, le Driver, le nom d’utilisateur, le
mot de passe, le Dialect, la stratégie pour la génération du schéma, l’activation de la console
H2 et le path pour y accéder. Pour rappel, Spring Boot gère par défaut ces paramètres au cas
ou vous ne les renseignez pas. Dans ce cas de figure :
le nom de la base de données sera généré automatiquement.
Le path par défaut pour accéder à la console est /h2-console.
La clé spring.jpa.properties.hibernate.globally_quoted_identifiers permet de résoudre le
problème de l’utilisation des noms résérvés (déjà hutilisés par Hibernate) dans les noms des
identificateurs. Dans notre cas, le nom de la classe User est réservé. En ajoutant ce paramètre,
Hibernate mettra le nom de la classe User entre deux quotes (" user ").
j. Développement de la couche Service (la couche Métier)
- Créer le package ma.formations.graphql.service.exception et créer ensuite la classe BusinessException
suivante :
package ma.formations.graphql.service.exception;
import ma.formations.graphql.dtos.customer.*;
import java.util.List;
L’interface IBankAccountService :
package ma.formations.graphql.service;
import ma.formations.graphql.dtos.bankaccount.AddBankAccountRequest;
import ma.formations.graphql.dtos.bankaccount.AddBankAccountResponse;
import ma.formations.graphql.dtos.bankaccount.BankAccountDto;
import java.util.List;
L’interface ITransactionService :
package ma.formations.graphql.service;
import ma.formations.graphql.dtos.transaction.AddWirerTransferRequest;
import ma.formations.graphql.dtos.transaction.AddWirerTransferResponse;
import ma.formations.graphql.dtos.transaction.GetTransactionListRequest;
import ma.formations.graphql.dtos.transaction.TransactionDto;
import java.util.Date;
import java.util.List;
public interface ITransactionService {
AddWirerTransferResponse wiredTransfer(AddWirerTransferRequest dto);
List<TransactionDto> getTransactions(GetTransactionListRequest dto);
}
import lombok.AllArgsConstructor;
import ma.formations.graphql.dao.CustomerRepository;
import ma.formations.graphql.dtos.customer.*;
import ma.formations.graphql.service.exception.BusinessException;
import ma.formations.graphql.service.model.Customer;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
@AllArgsConstructor
public class CustomerServiceImpl implements ICustomerService {
@Override
public List<CustomerDto> getAllCustomers() {
return customerRepository.findAll().stream().
map(customer -> modelMapper.map(customer, CustomerDto.class)).
collect(Collectors.toList());
}
@Override
public AddCustomerResponse createCustomer(AddCustomerRequest addCustomerRequest) {
Customer bo = modelMapper.map(addCustomerRequest, Customer.class);
String identityRef = bo.getIdentityRef();
String username = bo.getUsername();
customerRepository.findByIdentityRef(identityRef).ifPresent(a ->
{
throw new BusinessException(String.format("Customer with the same identity [%s] exist", identityRef));
}
);
customerRepository.findByUsername(username).ifPresent(a ->
{
throw new BusinessException(String.format("The username [%s] is already used", username));
}
);
AddCustomerResponse response = modelMapper.map(customerRepository.save(bo), AddCustomerResponse.class);
response.setMessage(String.format("Customer : [identity= %s,First Name= %s, Last Name= %s, username= %s] was
created with success",
response.getIdentityRef(), response.getFirstname(), response.getLastname(), response.getUsername()));
return response;
}
@Override
@Override
public CustomerDto getCustomByIdentity(String identity) {
return modelMapper.map(customerRepository.findByIdentityRef(identity).orElseThrow(
() -> new BusinessException(String.format("No Customer with identity [%s] exist !", identity))),
CustomerDto.class);
}
@Override
public String deleteCustomerByIdentityRef(String identityRef) {
if (identityRef == null || identityRef.isEmpty())
throw new BusinessException("Enter a correct identity customer");
Customer customerFound = customerRepository.findAll().stream().filter(customer ->
customer.getIdentityRef().equals(identityRef)).findFirst().orElseThrow(
() -> new BusinessException(String.format("No customer with identity %s exist in database", identityRef))
);
customerRepository.delete(customerFound);
return String.format("Customer with identity %s is deleted with success", identityRef);
}
}
La classe BankAccountServiceImpl :
package ma.formations.graphql.service;
import lombok.AllArgsConstructor;
import ma.formations.graphql.dao.BankAccountRepository;
import ma.formations.graphql.dao.CustomerRepository;
import ma.formations.graphql.dtos.bankaccount.AddBankAccountRequest;
import ma.formations.graphql.dtos.bankaccount.AddBankAccountResponse;
import ma.formations.graphql.dtos.bankaccount.BankAccountDto;
import ma.formations.graphql.enums.AccountStatus;
import ma.formations.graphql.service.exception.BusinessException;
import ma.formations.graphql.service.model.BankAccount;
import ma.formations.graphql.service.model.Customer;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
@Service
@Transactional
@AllArgsConstructor
public class BankAccountServiceImpl implements IBankAccountService {
private final BankAccountRepository bankAccountRepository;
private final CustomerRepository customerRepository;
private ModelMapper modelMapper;
@Override
public AddBankAccountResponse saveBankAccount(AddBankAccountRequest dto) {
BankAccount bankAccount = modelMapper.map(dto, BankAccount.class);
Customer customerP =
customerRepository.findByIdentityRef(bankAccount.getCustomer().getIdentityRef()).orElseThrow(
() -> new BusinessException(String.format("No customer with the identity: %s exist",
dto.getCustomerIdentityRef())));
bankAccount.setAccountStatus(AccountStatus.OPENED);
bankAccount.setCustomer(customerP);
bankAccount.setCreatedAt(new Date());
AddBankAccountResponse response = modelMapper.map(bankAccountRepository.save(bankAccount),
AddBankAccountResponse.class);
response.setMessage(String.format("RIB number [%s] for the customer [%s] has been successfully created",
dto.getRib(), dto.getCustomerIdentityRef()));
return response;
}
@Override
public List<BankAccountDto> getAllBankAccounts() {
return bankAccountRepository.findAll().stream().
map(bankAccount -> modelMapper.map(bankAccount, BankAccountDto.class)).
collect(Collectors.toList());
}
@Override
public BankAccountDto getBankAccountByRib(String rib) {
return modelMapper.map(bankAccountRepository.findByRib(rib).orElseThrow(
() -> new BusinessException(String.format("No Bank Account with rib [%s] exist", rib))), BankAccountDto.class);
}
}
La classe TransactionServiceImpl :
package ma.formations.graphql.service;
import lombok.AllArgsConstructor;
import ma.formations.graphql.dao.BankAccountRepository;
import ma.formations.graphql.dao.BankAccountTransactionRepository;
import ma.formations.graphql.dao.UserRepository;
import ma.formations.graphql.dtos.transaction.AddWirerTransferRequest;
import ma.formations.graphql.dtos.transaction.AddWirerTransferResponse;
import ma.formations.graphql.dtos.transaction.GetTransactionListRequest;
import ma.formations.graphql.dtos.transaction.TransactionDto;
import ma.formations.graphql.enums.AccountStatus;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
@AllArgsConstructor
public class TransactionServiceImpl implements ITransactionService {
@Override
public AddWirerTransferResponse wiredTransfer(AddWirerTransferRequest dto) {
transactionFrom.setCreatedAt(new Date());
transactionFrom.setUser(user);
transactionFrom.setBankAccount(bankAccountFrom);
transactionTo.setCreatedAt(new Date());
transactionTo.setUser(user);
transactionTo.setBankAccount(bankAccountTo);
bankAccountTransactionRepository.save(transactionFrom);
bankAccountTransactionRepository.save(transactionTo);
return AddWirerTransferResponse.builder().
message(String.format("the transfer of an amount of %s from the %s bank account to %s was carried out
successfully",
dto.getAmount(), dto.getRibFrom(), dto.getRibTo())).
transactionFrom(modelMapper.map(transactionFrom, TransactionDto.class)).
transactionTo(modelMapper.map(transactionTo, TransactionDto.class)).
build();
}
if (bankAccountFrom.getAccountStatus().equals(AccountStatus.CLOSED))
throw new BusinessException(String.format("the bank account %s is closed !!", bankAccountFrom.getRib()));
if (bankAccountFrom.getAccountStatus().equals(AccountStatus.BLOCKED))
throw new BusinessException(String.format("the bank account %s is blocked !!", bankAccountFrom.getRib()));
if (bankAccountTo.getAccountStatus().equals(AccountStatus.CLOSED))
throw new BusinessException(String.format("the bank account %s is closed !!", bankAccountTo.getRib()));
if (bankAccountTo.getAccountStatus().equals(AccountStatus.BLOCKED))
throw new BusinessException(String.format("the bank account %s is blocked !!", bankAccountTo.getRib()));
@Override
public List<TransactionDto> getTransactions(GetTransactionListRequest requestDTO) {
GetTransactionListBo data = modelMapper.map(requestDTO, GetTransactionListBo.class);
return bankAccountTransactionRepository.findByBankAccount_RibAndCreatedAtBetween(
data.getRib(), data.getDateFrom(), data.getDateTo()).
stream().map(bo -> modelMapper.map(bo, TransactionDto.class)).collect(Collectors.toList());
}
}
bankAccountService.saveBankAccount(AddBankAccountRequest.builder().
rib("RIB_1").
amount(1000000d).
customerIdentityRef("A100").
build());
bankAccountService.saveBankAccount(AddBankAccountRequest.builder().
rib("RIB_11").
amount(2000000d).
customerIdentityRef("A100").
build());
customerService.createCustomer(AddCustomerRequest.builder().
username("user2").
identityRef("A200").
firstname("FIRST_NAME2").
lastname("LAST_NAME2").
build());
bankAccountService.saveBankAccount(AddBankAccountRequest.builder().
rib("RIB_2").
amount(2000000d).
customerIdentityRef("A200").
build());
customerService.createCustomer(AddCustomerRequest.builder().
username("user3").
identityRef("A900").
firstname("FIRST_NAME9").
lastname("LAST_NAME9").
build());
bankAccountService.saveBankAccount(AddBankAccountRequest.builder().
rib("RIB_9").
amount(-25000d).
customerIdentityRef("A900").
build());
customerService.createCustomer(AddCustomerRequest.builder().
username("user4").
identityRef("A800").
bankAccountService.saveBankAccount(AddBankAccountRequest.builder().
rib("RIB_8").
amount(0.0).
customerIdentityRef("A800").
build());
transactionService.wiredTransfer(AddWirerTransferRequest.builder().
ribFrom("RIB_1").
ribTo("RIB_2").
amount(10000.0).
username("user1").
build());
transactionService.wiredTransfer(AddWirerTransferRequest.builder().
ribFrom("RIB_1").
ribTo("RIB_9").
amount(20000.0).
username("user1").
build());
transactionService.wiredTransfer(AddWirerTransferRequest.builder().
ribFrom("RIB_1").
ribTo("RIB_8").
amount(500.0).
username("user1").
build());
transactionService.wiredTransfer(AddWirerTransferRequest.builder().
ribFrom("RIB_2").
ribTo("RIB_11").
amount(300.0).
username("user2").
build());
};
}
V. Configuration de GraphQL
a. Activation du GraphQL
- Ajouter la ligne suivante au niveau du fichier application.properties :
spring.graphql.graphiql.enabled=true
- Dans resources, créer le dossier graphql et dans ce dernier, créer le fichier schema.graphqls suivant :
type Query{
customers:[CustomerDto]
customerByIdentity(identity:String):CustomerDto
bankAccounts : [BankAccountDto]
bankAccountByRib (rib:String):BankAccountDto
getTransactions (dto:GetTransactionListRequest):[TransactionDto]
}
type Mutation {
createCustomer(dto:AddCustomerRequest):AddCustomerResponse
addBankAccount(dto:AddBankAccountRequest):AddBankAccountResponse
addWirerTransfer(dto:AddWirerTransferRequest):AddWiredTransferResponse
updateCustomer(identityRef:String,dto:UpdateCustomerRequest):UpdateCustomerResponse
deleteCustomer(identityRef:String):String
}
type CustomerDto {
username : String,
identityRef : String,
firstname:String,
lastname:String,
}
type BankAccountDto {
rib:String,
amount:Float,
createdAt:String,
accountStatus:AccountStatus,
customer:CustomerDto,
}
type TransactionDto {
createdAt:String,
transactionType:TransactionType,
amount:Float,
bankAccount:BankAccountDto
user:userDto
}
type userDto {
username:String
enum AccountStatus {
OPENED, CLOSED, BLOCKED
}
enum TransactionType {
CREDIT,DEBIT
}
input AddCustomerRequest {
username : String,
identityRef : String,
firstname:String,
lastname:String
}
input UpdateCustomerRequest {
username : String,
firstname:String,
lastname:String
}
type UpdateCustomerResponse {
id:Int,
message:String,
username:String,
identityRef:String,
firstname:String,
lastname:String
}
type AddCustomerResponse {
message:String,
username:String,
identityRef:String,
firstname:String,
lastname:String
}
input AddBankAccountRequest {
rib:String,
amount:Float,
customerIdentityRef:String
}
type AddBankAccountResponse {
message:String,
rib:String,
amount:Float,
createdAt:String,
accountStatus:AccountStatus,
customer:CustomerDto
}
input AddWirerTransferRequest {
type AddWiredTransferResponse {
message:String,
transactionFrom:TransactionDto,
transactionTo:TransactionDto
}
input GetTransactionListRequest {
rib:String,
dateTo : String,
dateFrom:String
}
La classe CustomerGraphqlController :
package ma.formations.graphql.presentation;
import ma.formations.graphql.dtos.customer.*;
import ma.formations.graphql.service.ICustomerService;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
public class CustomerGraphqlController {
@QueryMapping
List<CustomerDto> customers() {
return customerService.getAllCustomers();
}
@QueryMapping
CustomerDto customerByIdentity(@Argument String identity) {
return customerService.getCustomByIdentity(identity);
}
@MutationMapping
@MutationMapping
public UpdateCustomerResponse updateCustomer(@Argument("identityRef") String identityRef, @Argument("dto")
UpdateCustomerRequest dto) {
return customerService.updateCustomer(identityRef, dto);
}
@MutationMapping
public String deleteCustomer(@Argument("identityRef") String identityRef) {
return customerService.deleteCustomerByIdentityRef(identityRef);
}
La classe BankAccountGraphqlController :
package ma.formations.graphql.presentation;
import lombok.AllArgsConstructor;
import ma.formations.graphql.dtos.bankaccount.AddBankAccountRequest;
import ma.formations.graphql.dtos.bankaccount.AddBankAccountResponse;
import ma.formations.graphql.dtos.bankaccount.BankAccountDto;
import ma.formations.graphql.service.IBankAccountService;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
@AllArgsConstructor
public class BankAccountGraphqlController {
private final IBankAccountService bankAccountService;
@QueryMapping
List<BankAccountDto> bankAccounts() {
return bankAccountService.getAllBankAccounts();
}
@QueryMapping
BankAccountDto bankAccountByRib(@Argument String rib) {
return bankAccountService.getBankAccountByRib(rib);
}
@MutationMapping
public AddBankAccountResponse addBankAccount(@Argument("dto") AddBankAccountRequest dto) {
return bankAccountService.saveBankAccount(dto);
}
import lombok.AllArgsConstructor;
import ma.formations.graphql.common.CommonTools;
import ma.formations.graphql.dtos.transaction.AddWirerTransferRequest;
import ma.formations.graphql.dtos.transaction.AddWirerTransferResponse;
import ma.formations.graphql.dtos.transaction.GetTransactionListRequest;
import ma.formations.graphql.dtos.transaction.TransactionDto;
import ma.formations.graphql.service.ITransactionService;
import ma.formations.graphql.service.exception.BusinessException;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.Date;
import java.util.List;
@Controller
@AllArgsConstructor
@MutationMapping
public AddWirerTransferResponse addWirerTransfer(@Argument("dto") AddWirerTransferRequest dto) {
return transactionService.wiredTransfer(dto);
}
@QueryMapping
public List<TransactionDto> getTransactions(@Argument GetTransactionListRequest dto) {
return transactionService.getTransactions(dto);
}
}
- Utiliser l’explorateur de l’interface GraphiQL, cliquer sur le service customerByIdentity, cocher les
données que vous voulez récupérer, entrer le numéro d’identité du client que vous voulez consulter et
ensuite cliquer sur la flèche au centre pour exécuter la requête :
- Cliquer sur createCustomer, sélectionner les données que vous voulez récupérer, entrer les données du
client à créer (firstname, lastname, identityRef et username) et cliquer ensuite sur la flèche au centre
pour exécuter la requête :
GraphQL et Spring Boot 37
i. Tester la requête addBankAccount
- Cliquer sur addBankAccount, sélectionner les données que vous voulez récupérer, entrer les données
du compte bancaire à créer (RIB, le montant et l’identité du client) et cliquer ensuite sur la flèche au
centre pour exécuter la requête :
- Pour intercepter les exceptions GraphQL et personnaliser les messages à retourner aux clients, créer la
classe GraphQlExceptionHandler suivante :
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.stereotype.Component;
@Component
public class GraphQlExceptionHandler extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
Explications :
Les exceptions GraphQL sont traitées par la méthode resolveToSingleError(Throwable ex,
DataFetchingEnvironment env) de la classe DataFetcherExceptionResolverAdapter. C’est
pourquoi, pour intercepter ces exceptions, il suffit d’étendre cette classe et redéfinir la
méthode resolveToSingleError.
Notre méthode redéfinie resolveToSingleError traite deux types d’exceptions :
L’exception de type BusinessException
Et les autres exceptions.
- Lancer votre application et refaire le même test. Remarquer que votre message en cas d’exception de
type BusinessException a été bien affichée :