Ce guide présente les meilleures pratiques pour optimiser les performances de vos APIs GraphQL générées avec GraphQL AutoGen.
Les requêtes N+1 sont l'un des problèmes de performance les plus courants dans les APIs GraphQL. Elles se produisent lorsque vous chargez une liste d'objets, puis exécutez une requête séparée pour chaque objet afin de récupérer une relation associée.
query {
allOrders { # 1 requête pour charger toutes les commandes
id
total
customer { # N requêtes pour charger le client de chaque commande
id
name
}
}
}
GraphQL AutoGen génère automatiquement des DataLoaders pour vos relations entre entités. Voici comment les configurer de manière optimale :
@Entity
@GraphQLType
public class Order {
@Id
private Long id;
private BigDecimal total;
@ManyToOne
@GraphQLField
@GraphQLDataLoader // Active le DataLoader pour cette relation
private Customer customer;
@OneToMany(mappedBy = "order")
@GraphQLField
@GraphQLDataLoader(batchSize = 100) // Configure la taille du batch
private List items;
// Getters et setters
}
Pour des cas plus complexes, vous pouvez implémenter vos propres DataLoaders :
@Component
@GraphQLDataLoaderRegistration
public class CustomerDataLoader implements BatchLoader {
private final CustomerRepository customerRepository;
public CustomerDataLoader(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
@Override
public CompletionStage> load(List customerIds) {
// Optimisation : une seule requête pour tous les IDs
return CompletableFuture.supplyAsync(() -> {
List customers = customerRepository.findAllByIdIn(customerIds);
// Préserver l'ordre des résultats selon les IDs demandés
Map customerMap = customers.stream()
.collect(Collectors.toMap(Customer::getId, Function.identity()));
return customerIds.stream()
.map(id -> customerMap.getOrDefault(id, null))
.collect(Collectors.toList());
});
}
}
La pagination est essentielle pour gérer efficacement les grandes collections de données :
@GraphQLQuery(name = "products")
public Connection getProducts(
@GraphQLArgument(name = "first", defaultValue = "10") int first,
@GraphQLArgument(name = "after") String after,
@GraphQLArgument(name = "filter") ProductFilter filter) {
// Conversion du cursor en offset
int offset = 0;
if (after != null) {
offset = ConnectionCursor.fromCursor(after).getOffset() + 1;
}
// Utilisation de Spring Data pour la pagination
Pageable pageable = PageRequest.of(offset / first, first);
Page productPage;
if (filter != null) {
productPage = productRepository.findAllWithFilter(filter, pageable);
} else {
productPage = productRepository.findAll(pageable);
}
// Conversion en format Connection de Relay
return new SimpleConnection<>(productPage.getContent(), pageable,
productPage.hasNext(), productPage.getTotalElements());
}
GraphQL AutoGen s'intègre facilement avec Spring Cache pour améliorer les performances :
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("products"),
new ConcurrentMapCache("categories")
));
return cacheManager;
}
}
@Service
public class ProductService {
private final ProductRepository productRepository;
@Cacheable("products")
public Product getProductById(Long id) {
return productRepository.findById(id).orElse(null);
}
@Cacheable("products")
public List getProductsByCategory(Long categoryId) {
return productRepository.findByCategoryId(categoryId);
}
}
Activez la mise en cache du schéma pour améliorer les temps de démarrage :
graphql.autogen.cache-schema=true
graphql.autogen.schema-cache-max-age=3600
Implémentez des filtres et tris efficaces pour vos API GraphQL :
@GraphQLType(name = "ProductFilter")
public class ProductFilter {
private String nameContains;
private BigDecimal minPrice;
private BigDecimal maxPrice;
private List categoryIds;
// Getters et setters
}
@Repository
public interface ProductRepository extends JpaRepository {
@Query("SELECT p FROM Product p WHERE " +
"(:#{#filter.nameContains} IS NULL OR LOWER(p.name) LIKE LOWER(CONCAT('%', :#{#filter.nameContains}, '%'))) AND " +
"(:#{#filter.minPrice} IS NULL OR p.price >= :#{#filter.minPrice}) AND " +
"(:#{#filter.maxPrice} IS NULL OR p.price <= :#{#filter.maxPrice}) AND " +
"(:#{#filter.categoryIds} IS NULL OR p.category.id IN :#{#filter.categoryIds})")
Page findAllWithFilter(@Param("filter") ProductFilter filter, Pageable pageable);
}
Utilisez des requêtes SQL natives ou des Specification de JPA pour les filtres complexes afin d'optimiser les requêtes en base de données.
Mettez en place une surveillance pour identifier les goulots d'étranglement :
@Configuration
public class GraphQLInstrumentationConfig {
@Bean
public Instrumentation requestLoggingInstrumentation() {
return new RequestLoggingInstrumentation(options -> options
.includeTiming(true)
.includeQuery(true)
.maxQueryLength(10000)
.includeVariables(true));
}
@Bean
public Instrumentation tracingInstrumentation() {
return new TracingInstrumentation(TracingInstrumentation.Options.newOptions()
.includeTrivialDataFetchers(false));
}
}