GraphQL: Sorgu Dili, Çözümleyici ve N+1 Sorunu
GraphQL, REST API'lerin temel sorunlarından birini çözüyor: istemci kontrol edemiyor
hangi verileri alıyor. REST ile, GET /users/123 her zaman tüm kullanıcı alanlarını döndürür,
müşterinin yalnızca iki tanesine ihtiyacı olsa bile. Siparişlerle birlikte eksiksiz bir kullanıcı profili yüklemek için
yeni ve üretilmiş, genellikle 3-4 ayrı HTTP çağrısına ihtiyaç duyar. GraphQL bu sorunu ortadan kaldırır
istemcinin tek bir sorguda tam olarak ihtiyaç duyulan verileri belirtmesine olanak tanır.
Ancak GraphQL kendi karmaşıklığını da beraberinde getirir: N+1 problemi ve risk zararsız görünen bir sorguyu sorguya dönüştürebilen, daha sinsi bir mimariye sahip. düzinelerce veya yüzlerce veritabanı sorgusu çığ gibi büyüyor. Bu kılavuzda bunun nasıl çalıştığı açıklanmaktadır çözümleyici sistem, N+1 sorununun kendini nasıl gösterdiği ve nasıl Veri Yükleyici bunu otomatik toplu işlem ve önbelleğe alma ile çözer.
Ne Öğreneceksiniz
- GraphQL şeması: türler, sorgular, mutasyonlar ve abonelikler
- Çözme Döngüsü: GraphQL bir sorguyu nasıl yürütür?
- Çözümleyici: Her alan için veri sağlayan işlevler
- N+1 sorunu: kendini nasıl gösterir ve neden tehlikelidir
- DataLoader: N+1'i çözmek için toplu işlem ve önbelleğe alma
- Apollo Server ve TypeScript ile kurulumu tamamlayın
GraphQL Şeması: Yazılan Sözleşme
Her GraphQL API şemayla başlar: mevcut tüm türleri tanımlayan bir sözleşme, sorgular (okuma), mutasyonlar (yazma) ve abonelikler (gerçek zamanlı akışlar). Şema SDL (Şema Tanımlama Dili) ile yazılmıştır:
// schema.graphql
type User {
id: ID!
name: String!
email: String!
createdAt: String!
orders: [Order!]! # Un utente ha molti ordini
profile: UserProfile # Nullable: non tutti gli utenti hanno un profilo
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
createdAt: String!
items: [OrderItem!]! # Un ordine ha molti item
user: User! # Relazione bidirezionale
}
type OrderItem {
id: ID!
quantity: Int!
price: Float!
product: Product! # L'item ha un prodotto
}
type Product {
id: ID!
name: String!
price: Float!
category: String!
}
type UserProfile {
bio: String
avatarUrl: String
website: String
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
# Query: operazioni di lettura (equivalente GET in REST)
type Query {
user(id: ID!): User
users(limit: Int = 20, offset: Int = 0): [User!]!
order(id: ID!): Order
searchProducts(query: String!, limit: Int = 10): [Product!]!
}
# Mutation: operazioni di scrittura (equivalente POST/PUT/DELETE)
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createOrder(input: CreateOrderInput!): Order!
}
# Subscription: stream real-time (WebSocket)
type Subscription {
orderStatusChanged(orderId: ID!): Order!
newUserRegistered: User!
}
input CreateUserInput {
name: String!
email: String!
password: String!
}
input UpdateUserInput {
name: String
email: String
}
Çözüm Döngüsü: GraphQL Bir Sorguyu Nasıl Yürütür?
Bir istemci bir GraphQL sorgusu gönderdiğinde, sunucu bunu adı verilen bir işlem aracılığıyla yürütür. Şemayı bir ağaç gibi geçerek işlevi çağıran "çözünürlük" çözümleyiciler gerekli her alan için:
// Questa query del client:
query {
users(limit: 5) {
id
name
orders {
id
total
status
}
}
}
// Produce questo albero di risoluzione:
// 1. Query.users -> chiama il resolver "users"
// 2. Per ogni User restituito:
// - User.id -> valore direttamente dall'oggetto
// - User.name -> valore direttamente dall'oggetto
// - User.orders -> chiama il resolver "orders" con parent = user
// 3. Per ogni Order dentro ogni User:
// - Order.id -> valore direttamente dall'oggetto
// - Order.total -> valore direttamente dall'oggetto
// - Order.status -> valore direttamente dall'oggetto
Apollo Server ile Çözümleyicileri Uygulama
Çözümleyici, belirli bir alan için verilerin nasıl alınacağını bilen bir işlevdir. Her çözümleyici dört argüman alır: ebeveyn (ana öğenin nesnesi), argümanlar (sorgu bağımsız değişkenleri), bağlam (veritabanı ve kimlik doğrulama gibi paylaşılan veriler) ve bilgi (meta verileri sorgula):
// src/resolvers/index.ts
import { Resolvers } from './generated/types'; // Tipi generati da GraphQL Code Generator
import DataLoader from 'dataloader';
// Context condiviso tra tutti i resolver
export interface GraphQLContext {
db: DatabaseClient;
currentUser: User | null;
loaders: {
userById: DataLoader<string, User>;
ordersByUserId: DataLoader<string, Order[]>;
productById: DataLoader<string, Product>;
};
}
export const resolvers: Resolvers<GraphQLContext> = {
Query: {
// parent = {} (root query), args = { limit, offset }, ctx = context
users: async (_, { limit = 20, offset = 0 }, { db, currentUser }) => {
if (!currentUser) throw new GraphQLError('Authentication required', {
extensions: { code: 'UNAUTHENTICATED' }
});
return db.users.findMany({
take: Math.min(limit, 100), // Limite massimo di sicurezza
skip: offset,
orderBy: { createdAt: 'desc' }
});
},
user: async (_, { id }, { db }) => {
return db.users.findUnique({ where: { id } });
},
searchProducts: async (_, { query, limit }, { db }) => {
return db.products.findMany({
where: {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ category: { contains: query, mode: 'insensitive' } }
]
},
take: Math.min(limit, 50)
});
}
},
// Resolver per i campi del tipo User
User: {
// parent = User object, args = {}, ctx = context
orders: async (user, _, { loaders }) => {
// USA DataLoader invece di db.orders.findMany({ where: { userId: user.id } })
// Spiegazione nella sezione successiva!
return loaders.ordersByUserId.load(user.id);
},
profile: async (user, _, { db }) => {
return db.userProfiles.findUnique({ where: { userId: user.id } });
}
},
// Resolver per i campi del tipo Order
Order: {
items: async (order, _, { db }) => {
return db.orderItems.findMany({ where: { orderId: order.id } });
},
user: async (order, _, { loaders }) => {
return loaders.userById.load(order.userId);
}
},
// Resolver per i campi del tipo OrderItem
OrderItem: {
product: async (item, _, { loaders }) => {
return loaders.productById.load(item.productId);
}
},
Mutation: {
createUser: async (_, { input }, { db }) => {
const hashedPassword = await bcrypt.hash(input.password, 12);
return db.users.create({
data: { ...input, password: hashedPassword }
});
},
createOrder: async (_, { input }, { db, currentUser }) => {
if (!currentUser) throw new GraphQLError('Authentication required');
return db.orders.create({
data: { ...input, userId: currentUser.id, status: 'PENDING' }
});
}
}
};
Problem N+1: Gizli Tehlike
N+1 sorunu GraphQL'deki en yaygın mimari risktir. Bu sorguyu hayal edin:
query {
users(limit: 10) { # 1 query: SELECT * FROM users LIMIT 10
name
orders { # 10 query: SELECT * FROM orders WHERE userId = ?
total # Una per ogni utente!
}
}
}
Bu "masum" sorgu 11 veritabanı sorgusu üretir: kullanıcılar için 1 + her kullanıcı için 1 Onun emirleri için. 10 kullanıcıyla 11 sorgu. 100 kullanıcıyla (sınırı artırırsanız) 101 kullanıcı var. Bir yönetici sayfasında 1000 kullanıcı var mı? 1001 sorgu. Sorgu sayısı N (kullanıcı) + 1'dir - itibaren burada adı "N+1 problemi".
// SENZA DataLoader: N+1 in azione
const User_orders = async (user, _, { db }) => {
// Questa riga viene chiamata UNA VOLTA PER OGNI UTENTE nella lista
return db.orders.findMany({ where: { userId: user.id } });
// Con 10 utenti = 10 query separate al database
// SELECT * FROM orders WHERE userId = '1'
// SELECT * FROM orders WHERE userId = '2'
// ... (10 volte!)
};
// SENZA DataLoader: prodotti per ogni item di ogni ordine
const OrderItem_product = async (item, _, { db }) => {
return db.products.findUnique({ where: { id: item.productId } });
// Se hai 10 utenti x 5 ordini x 3 items = 150 query solo per i prodotti!
};
DataLoader: N+1 Sorununun Çözümü
Veri Yükleyici (Facebook tarafından oluşturuldu, artık topluluk tarafından sürdürülüyor) sorunu çözüyor İki mekanizmalı N+1 problemi: harmanlama (birden fazla isteği tek bir istekte gruplandırır tek sorgu) e önbelleğe alma (aynı anahtar için sonuçları yeniden kullanın):
// src/loaders/index.ts
import DataLoader from 'dataloader';
import { DatabaseClient } from './db';
export function createLoaders(db: DatabaseClient) {
return {
// Batch function: riceve un ARRAY di chiavi, ritorna un ARRAY di risultati
// L'ordine dei risultati DEVE corrispondere all'ordine delle chiavi
userById: new DataLoader<string, User | null>(
async (userIds) => {
// UNA SOLA QUERY per tutti gli ID! (invece di N query)
const users = await db.users.findMany({
where: { id: { in: [...userIds] } }
});
// Mappa risultati mantenendo l'ordine originale delle chiavi
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id) ?? null);
}
),
ordersByUserId: new DataLoader<string, Order[]>(
async (userIds) => {
// UNA SOLA QUERY invece di N query
const orders = await db.orders.findMany({
where: { userId: { in: [...userIds] } }
});
// Raggruppa per userId
const ordersByUser = new Map<string, Order[]>();
for (const order of orders) {
const existing = ordersByUser.get(order.userId) ?? [];
ordersByUser.set(order.userId, [...existing, order]);
}
return userIds.map(id => ordersByUser.get(id) ?? []);
},
{
// Ottimizzazione: raggruppa le chiavi duplicate
cacheKeyFn: (key) => key,
// Massimo 100 chiavi per batch (evita query enormi)
maxBatchSize: 100
}
),
productById: new DataLoader<string, Product | null>(
async (productIds) => {
const products = await db.products.findMany({
where: { id: { in: [...productIds] } }
});
const productMap = new Map(products.map(p => [p.id, p]));
return productIds.map(id => productMap.get(id) ?? null);
},
{
// Cache per tutta la durata della request (default)
cache: true
}
)
};
}
// Come funziona DataLoader internamente:
// 1. Il resolver chiama loader.load('userId-1') e loader.load('userId-2')
// 2. DataLoader aspetta il "tick" successivo di JavaScript
// 3. Raggruppa tutte le chiamate .load() accumulate in quel tick
// 4. Chiama la batch function UNA SOLA VOLTA con ['userId-1', 'userId-2']
// 5. Distribuisce i risultati ai chiamanti originali
DataLoader ile: Sorgu 111 Sorgu Yerine 3 Sorgu Üretiyor
// Query: 10 utenti con 5 ordini ciascuno, ogni ordine con 3 prodotti
// SENZA DataLoader: 1 + 10 + 50 + 150 = 211 query database
// SENZA DataLoader con 100 utenti: 1 + 100 + 500 + 1500 = 2101 query!
// CON DataLoader:
// 1 query: SELECT * FROM users LIMIT 10
// 1 query: SELECT * FROM orders WHERE userId IN (1,2,3,...,10)
// 1 query: SELECT * FROM products WHERE id IN (product-id-1, ..., product-id-15)
// = 3 query totali, indipendentemente da quanti utenti/ordini/prodotti!
Apollo Sunucu Kurulumunu TypeScript ile Tamamlayın
// src/server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { readFileSync } from 'fs';
import { resolvers } from './resolvers';
import { createLoaders } from './loaders';
import { GraphQLContext } from './types';
import { db } from './db';
import { authenticateRequest } from './auth';
const typeDefs = readFileSync('./schema.graphql', 'utf8');
const server = new ApolloServer<GraphQLContext>({
typeDefs,
resolvers,
// Plugin per logging e monitoring
plugins: [
// Log ogni richiesta in production
{
requestDidStart: async () => ({
didResolveOperation: async ({ request, document }) => {
console.log('GraphQL operation:', request.operationName);
}
})
}
],
// Limiti di sicurezza
introspection: process.env.NODE_ENV !== 'production'
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => {
// I DataLoader devono essere creati PER REQUEST (non globali!)
// Ogni request ha il suo scope di caching
return {
db,
currentUser: await authenticateRequest(req.headers.authorization),
loaders: createLoaders(db)
// IMPORTANTE: crea nuovi loaders per ogni request
// (altrimenti il cache persiste tra requests e puo causare
// problemi di autorizzazione)
};
}
});
console.log(`GraphQL server running at: ${url}`);
Derinlik Sınırlaması ve Karmaşıklık Analizi
GraphQL, istemcilerin isteğe bağlı olarak derin veya karmaşık sorgular yapmasına olanak tanır. DoS saldırıları için kullanılabilir. Korumaların uygulanması önemlidir:
// Protezione con graphql-depth-limit e graphql-query-complexity
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// Massima profondita della query: previene query tipo user.orders.items.product.vendor.orders.items...
depthLimit(7),
// Massima complessita calcolata
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('Query complexity:', cost),
formatErrorMessage: (cost) =>
`Query too complex (${cost}). Maximum complexity: 1000`
})
]
});
GraphQL En İyi Uygulamaları
- İlişkiler için her zaman DataLoader'ı kullanın: türdeki çözümleyicilerde (User.orders, Order.items, vb.) asla doğrudan veritabanı sorguları yapmayın.
- DataLoader'ı globaller için değil, istekler için oluşturun: DataLoader önbelleğe alma işleminin, her isteğin sonunda sona ermesi gerekir. Farklı kullanıcılar arasındaki yetkilendirme sorunları
- Derinlik sınırlama ve karmaşıklık analizini uygulayın: Bu kontroller olmadan GraphQL, karmaşık sorgular yoluyla DoS saldırılarına karşı savunmasızdır
- Üretimde kalıcı sorguları kullanın: istemci tarafı sorgularını önceden kaydeder ve yalnızca karma değeri göndererek sorguları önler üretimde keyfi
- Üretimde iç gözlemi devre dışı bırakın: iç gözlem, tüm planın potansiyel saldırganlara açık olmasını sağlar
Sonuçlar ve Sonraki Adımlar
GraphQL, heterojen ihtiyaçları olan müşterilere hizmet vermesi gereken API'ler için güçlü bir araçtır. ancak N+1 sorunu gerçektir ve DataLoader'ın doğru şekilde çözmesini gerektirir. Bir API DataLoader olmadan ve derinlik sınırlaması olmadan üretimde GraphQL ve teknik olarak güvensiz ve verimsiz. Doğru konfigürasyonla Apollo Server'lı GraphQL, REST ile mükemmel geliştirici deneyimi ve rekabetçi performans.
Bir sonraki makale araştırıyor GraphQL Federasyonu: Apollo Yönlendirici gibi (Rust'ta yazılmıştır), birden fazla bağımsız alt grafiği birleştirilmiş bir üst grafik halinde birleştirerek, özerk ekipler planın bazı bölümlerini ayrı ayrı geliştirecek.
Seri: API Tasarımı — REST, GraphQL, gRPC ve tRPC Karşılaştırması
- Madde 1: 2026'da API Ortamı - Karar Matrisi
- Makale 2: 2026'da REST - En İyi Uygulama, Sürüm Oluşturma ve Richardson Olgunluk Modeli
- Madde 3 (bu): GraphQL — Sorgu Dili, Çözümleyici ve N+1 Sorunu
- Madde 4: GraphQL Federasyonu - Üst Grafik, Alt Grafik ve Apollo Yönlendirici
- Madde 5: gRPC — Protobuf, Performans ve Hizmetten Hizmete İletişim
- Madde 6: tRPC — Kod Oluşturmadan Uçtan Uca Güvenlik Türü
- Madde 7: Web Kancaları — Kalıplar, Güvenlik ve Yeniden Deneme Mantığı
- Madde 8: API Sürümü Oluşturma - URI, Başlık ve Kullanımdan Kaldırma Politikası
- Madde 9: Hız Sınırlama ve Azaltma - Algoritmalar ve Uygulamalar
- Makale 10: Hibrit API Mimarisi — 2026'da REST + tRPC + gRPC







