Veri Sorgulama: GraphQL Query Yazımı ve Çalışma Mantığı

GraphQL ile ilk kez çalışmaya başladığımda, REST API’lere o kadar alışmıştım ki “neden tek bir endpoint’e bu kadar karmaşık sorgular yazayım ki?” diye düşündüm. Ama birkaç hafta içinde, özellikle büyük bir e-ticaret projesinde müşteri verilerini çekerken, neden bu kadar çok insanın GraphQL’e geçtiğini anladım. Şimdi bu yazıda, GraphQL sorgu yazımının temellerini ve çalışma mantığını gerçek dünya senaryolarıyla birlikte ele alacağız.

GraphQL Query Nedir ve REST’ten Farkı Ne?

REST API kullanırken muhtemelen şu durumla karşılaştınız: Kullanıcı profilini göstermek için /users/1 endpoint’ini çağırıyorsunuz, ama bu endpoint size ihtiyacınız olmayan 20 alan döndürüyor. Sonra kullanıcının siparişlerini almak için /users/1/orders endpoint’ini çağırıyorsunuz, oradan da gereksiz veri geliyor. Bu soruna over-fetching deniyor.

Tam tersi de oluyor: Bir sayfada hem kullanıcı bilgisine hem de siparişlerine hem de ürün detaylarına ihtiyacınız var, ama tek bir REST endpoint bunların hepsini döndürmüyor. Birden fazla istek atmak zorunda kalıyorsunuz. Buna da under-fetching deniyor.

GraphQL tam olarak bu iki problemi çözmek için tasarlanmış. Tek bir endpoint üzerinden, istediğiniz alanları istediğiniz derinlikte sorgulayabiliyorsunuz.

GraphQL’de üç temel operasyon tipi var:

  • Query: Veri okuma işlemleri (GET karşılığı)
  • Mutation: Veri yazma/güncelleme/silme işlemleri (POST/PUT/DELETE karşılığı)
  • Subscription: Gerçek zamanlı veri akışları (WebSocket karşılığı)

Bu yazıda odak noktamız Query operasyonları olacak.

Temel Query Yazımı

GraphQL sorgularının sözdizimi oldukça okunabilir. Aşağıdaki örneğe bakın:

# Basit bir kullanici sorgulama
query {
  user(id: "1") {
    id
    name
    email
    createdAt
  }
}

Bu sorguyu çalıştırdığınızda sunucu size tam olarak istediğiniz alanları döndürür, fazlasını değil. Response şu şekilde görünür:

# Sunucudan gelen yanit
{
  "data": {
    "user": {
      "id": "1",
      "name": "Ahmet Yilmaz",
      "email": "[email protected]",
      "createdAt": "2024-01-15T10:30:00Z"
    }
  }
}

Dikkat ettiniz mi? Sunucunun döndürdüğü JSON yapısı, sorgunuzun yapısını birebir yansıtıyor. Bu GraphQL’in en güzel özelliklerinden biri: Sorgunun şekli, yanıtın şeklini belirliyor.

İç İçe Sorgular (Nested Queries)

GraphQL’in gerçek gücü iç içe sorgularda ortaya çıkıyor. Bir kullanıcının siparişlerini ve her siparişin ürünlerini tek bir sorguda çekebilirsiniz:

# Kullanici, siparisleri ve urunleri tek sorguda
query {
  user(id: "1") {
    id
    name
    email
    orders {
      id
      totalAmount
      status
      createdAt
      items {
        quantity
        product {
          id
          name
          price
          category {
            name
          }
        }
      }
    }
  }
}

Bunu REST ile yapmak isteseniz en az 3-4 farklı API çağrısı yapmanız gerekirdi. Burada tek bir HTTP isteği ile tüm ağaç yapısını çekiyorsunuz. Mobil uygulama geliştiriyorsanız bu durum performans açısından ciddi fark yaratıyor.

Argümanlar ve Filtreleme

Gerçek dünya uygulamalarında verilerinizi filtrelemek, sıralamak ve sayfalamak zorunda kalırsınız. GraphQL bunu argümanlar aracılığıyla yapıyor:

# Filtreleme ve siralama ile urun listesi
query {
  products(
    filter: {
      category: "elektronik"
      minPrice: 100
      maxPrice: 5000
      inStock: true
    }
    orderBy: { field: "price", direction: ASC }
    limit: 10
    offset: 0
  ) {
    id
    name
    price
    stockQuantity
    seller {
      companyName
      rating
    }
  }
}

Burada önemli bir nokta var: Bu argümanların nasıl işleneceği tamamen sunucu tarafında schema ve resolver tanımlarına bağlı. Yani filter, orderBy, limit, offset gibi argüman isimlerini ve yapılarını API’yi tasarlayan kişi belirliyor. Bir GraphQL API belgesi okurken her zaman hangi argümanların desteklendiğini kontrol etmeniz gerekiyor.

Named Queries ve Variables

Yazılımın production ortamında çalışırken, sorguları dinamik değerlerle çalıştırmanız gerekiyor. Bunu iki şekilde yapabilirsiniz: inline argümanlarla ya da variables kullanarak. İkincisi çok daha temiz ve güvenli:

# Named query ve variable kullanimi
query GetUserProfile($userId: ID!, $includeOrders: Boolean!) {
  user(id: $userId) {
    id
    name
    email
    profileImage
    orders @include(if: $includeOrders) {
      id
      status
      totalAmount
    }
  }
}

# Variables JSON olarak ayri gonderilir
{
  "userId": "42",
  "includeOrders": true
}

Birkaç önemli nokta:

  • $userId: ID!: Dolar işareti değişken, ID tipi, ünlem işareti zorunlu olduğunu belirtir
  • $includeOrders: Boolean!: Boolean tipinde zorunlu değişken
  • @include(if: ...): Directive kullanımı, bu alanı koşullu olarak dahil eder

Named query kullanmak sadece okunabilirlik için değil, aynı zamanda sunucu tarafındaki loglama ve cache mekanizmaları için de önemli. Sorgunuza anlamlı isimler verin.

Aliases: Aynı Field’ı Farklı İsimle Çağırmak

Bazen aynı sorgu alanını farklı argümanlarla birden fazla kez çağırmak istersiniz. Normal şartlarda bu çakışmaya yol açar ama alias kullanarak çözebilirsiniz:

# Alias kullanimi ile ayni endpoint'i farkli argümanlarla cagirma
query CompareProducts {
  cheapProduct: product(id: "101") {
    name
    price
    rating
    reviewCount
  }
  expensiveProduct: product(id: "202") {
    name
    price
    rating
    reviewCount
  }
  featuredItems: products(featured: true, limit: 5) {
    id
    name
    price
  }
  newArrivals: products(sortBy: "newest", limit: 5) {
    id
    name
    price
  }
}

Response’da cheapProduct ve expensiveProduct olarak ayrı ayrı gelecek. Bu özelliği özellikle karşılaştırma sayfalarında veya dashboard ekranlarında çok kullanıyorsunuz.

Fragments: Tekrar Eden Alanları Yönetmek

Büyük uygulamalarda aynı alanları farklı sorgularda tekrar tekrar yazmak zorunda kalırsınız. Fragment‘lar bu tekrarı ortadan kaldırır:

# Fragment tanimi
fragment ProductBasicInfo on Product {
  id
  name
  price
  imageUrl
  rating
  inStock
}

fragment SellerInfo on Seller {
  id
  companyName
  rating
  verifiedSeller
}

# Fragment kullanan sorgular
query GetFeaturedProducts {
  featuredProducts(limit: 10) {
    ...ProductBasicInfo
    seller {
      ...SellerInfo
    }
    discount {
      percentage
      validUntil
    }
  }
}

query SearchProducts($query: String!) {
  searchResults(query: $query) {
    ...ProductBasicInfo
    category {
      name
      slug
    }
    seller {
      ...SellerInfo
    }
  }
}

...ProductBasicInfo sözdizimi spread operatörü gibi çalışıyor ve o fragment’ın tüm alanlarını oraya enjekte ediyor. Bir ürünün temel alanlarını değiştirmeniz gerektiğinde tek bir yerde değiştiriyorsunuz, her sorguyu güncellemenize gerek kalmıyor.

Introspection: Schema’yı Keşfetmek

GraphQL’in az bilinen ama son derece güçlü özelliklerinden biri introspection. Bir GraphQL API’ye bağlandığınızda, o API’nin schema’sını doğrudan sorgu yazarak keşfedebilirsiniz:

# Schema'yi kesfetme
query IntrospectSchema {
  __schema {
    types {
      name
      kind
      description
      fields {
        name
        type {
          name
          kind
        }
        description
      }
    }
  }
}

# Belirli bir type'i inceleme
query IntrospectUserType {
  __type(name: "User") {
    name
    fields {
      name
      type {
        name
        kind
        ofType {
          name
          kind
        }
      }
      isDeprecated
      deprecationReason
    }
  }
}

Bu özellik özellikle dökümantasyonu eksik olan bir API ile çalışırken hayat kurtarıcı. Ayrıca GraphiQL ve Apollo Studio gibi araçlar bu introspection mekanizmasını kullanarak otomatik tamamlama ve dökümantasyon ekranı sunuyor.

Directives: Koşullu Alan Yönetimi

GraphQL iki built-in directive ile geliyor: @include ve @skip. İkisi de field’ları koşullu olarak dahil etmek ya da hariç tutmak için kullanılıyor:

# @include ve @skip directive kullanimi
query GetDashboard(
  $userId: ID!
  $showStats: Boolean!
  $isAdmin: Boolean!
  $hidePersonalInfo: Boolean!
) {
  user(id: $userId) {
    id
    username
    email @skip(if: $hidePersonalInfo)
    phoneNumber @skip(if: $hidePersonalInfo)
    
    # Sadece admin gorur
    internalNotes @include(if: $isAdmin)
    accountFlags @include(if: $isAdmin)
    
    # Sadece istatistik istendiyse
    stats @include(if: $showStats) {
      totalOrders
      totalSpent
      averageOrderValue
      loyaltyPoints
    }
  }
}

@include(if: true) alanı dahil eder, @skip(if: true) alanı hariç tutar. İkisi de aynı işi yapar ama farklı mantıksal yönelimler için kullanışlıdır: “Bunu dahil et eğer…” vs “Bunu atla eğer…”

Gerçek Dünya Senaryosu: E-Ticaret Dashboard

Şimdi tüm öğrendiklerimizi bir araya getirelim. Bir e-ticaret yönetim paneli için karmaşık bir sorgu yazalım:

# Admin dashboard icin kapsamli sorgu
query AdminDashboard(
  $period: DateRange!
  $sellerId: ID
  $hasSellerId: Boolean!
  $topProductsLimit: Int = 5
) {
  # Genel istatistikler
  salesSummary(period: $period) {
    totalRevenue
    orderCount
    averageOrderValue
    growthRate
    revenueByDay {
      date
      amount
    }
  }
  
  # Satici bazli istatistik (opsiyonel)
  sellerStats(id: $sellerId) @include(if: $hasSellerId) {
    seller {
      companyName
      verifiedSeller
    }
    revenue
    orderCount
    returnRate
  }
  
  # En cok satan urunler
  topProducts: products(
    orderBy: { field: "soldCount", direction: DESC }
    limit: $topProductsLimit
    filter: { period: $period }
  ) {
    ...ProductBasicInfo
    soldCount
    revenue
    seller {
      companyName
    }
  }
  
  # Bekleyen siparisler
  pendingOrders: orders(
    filter: { status: PENDING }
    orderBy: { field: "createdAt", direction: ASC }
    limit: 20
  ) {
    id
    createdAt
    totalAmount
    customer {
      name
      email
    }
    items {
      quantity
      product {
        name
      }
    }
  }
  
  # Son kayit olan kullanicilar
  recentUsers: users(
    orderBy: { field: "createdAt", direction: DESC }
    limit: 10
  ) {
    id
    name
    email
    createdAt
    orderCount
  }
}

fragment ProductBasicInfo on Product {
  id
  name
  price
  imageUrl
  rating
  inStock
}

Bu tek sorgu ile bir admin dashboard için gerekli olan tüm veriyi tek bir HTTP isteğinde çekiyorsunuz. REST API ile bunu yapmak için en az 5-6 farklı endpoint çağrısı yapmanız gerekirdi.

HTTP ile GraphQL Sorgusu Göndermek

GraphQL sorguları genellikle HTTP POST isteği olarak gönderilir. curl ile nasıl yapıldığına bakalım:

# curl ile GraphQL sorgusu gonderme
curl -X POST https://api.ornek.com/graphql 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer TOKEN_BURAYA" 
  -d '{
    "query": "query GetUser($id: ID!) { user(id: $id) { id name email } }",
    "variables": { "id": "1" },
    "operationName": "GetUser"
  }'

# Daha okunaklı hali - dosyadan sorgu gonderme
# once query.graphql dosyasi olusturun
cat > query.graphql << 'EOF'
query GetUserOrders($userId: ID!, $status: OrderStatus) {
  user(id: $userId) {
    name
    orders(filter: { status: $status }, limit: 10) {
      id
      status
      totalAmount
      createdAt
    }
  }
}
EOF

# Sonra bu dosyayi kullanarak istek gonder
QUERY=$(cat query.graphql | tr -d 'n')
curl -X POST https://api.ornek.com/graphql 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer $TOKEN" 
  -d "{
    "query": "$QUERY",
    "variables": {"userId": "1", "status": "PENDING"},
    "operationName": "GetUserOrders"
  }"

Production ortamında curl’ü bu şekilde doğrudan kullanmak zahmetli olabiliyor. Bunun yerine Apollo Client, urql veya basit fetch/axios ile çalışmak çok daha pratik. Ama debug yaparken veya hızlı test için curl’ün bu kullanımını bilmek işinize yarıyor.

Hata Yönetimi

GraphQL, REST’ten farklı bir hata yönetim anlayışına sahip. Her zaman HTTP 200 döner (bağlantı seviyesinde bir problem olmadıkça) ve hataları response body’sindeki errors alanında taşır:

# Basarili sorgu yaniti ornegi
{
  "data": {
    "user": {
      "id": "1",
      "name": "Ahmet Yilmaz",
      "orders": null
    }
  },
  "errors": [
    {
      "message": "Orders service temporarily unavailable",
      "locations": [{ "line": 4, "column": 5 }],
      "path": ["user", "orders"],
      "extensions": {
        "code": "SERVICE_UNAVAILABLE",
        "service": "order-service"
      }
    }
  ]
}

Dikkat edin: data alanı da var, errors alanı da var. Bu GraphQL’in partial success kavramı. Bir sorgunun bazı alanları başarıyla çözülürken diğerleri hata verebilir. REST API’lerde ya tüm response başarılı olur ya da başarısız. GraphQL’de kısmi başarı mümkün.

Bu davranışa kodunuzda doğru şekilde tepki vermeniz gerekiyor: Sadece errors var mı diye kontrol etmek yetmez, data alanındaki null değerleri de kontrol etmeniz lazım.

Performans Konuları ve N+1 Problemi

GraphQL öğrenirken bir sysadmin olarak mutlaka karşılaşacağınız konu N+1 problemi. Şöyle düşünün: 100 kullanıcı listeliyorsunuz ve her kullanıcının son siparişini çekiyorsunuz. Naif bir implementasyonda bu 1 (kullanıcı listesi) + 100 (her kullanıcı için ayrı sipariş sorgusu) = 101 veritabanı sorgusu anlamına gelir.

Sunucu tarafında bu sorunu DataLoader gibi araçlarla çözersiniz, ama sorgu yazarken de bazı önlemler alabilirsiniz:

  • Derin iç içe sorgulardan kaçının, gerçekten ihtiyacınız olan derinliği kullanın
  • Pagination kullanın, limit koymadan büyük listeler çekmeyin
  • Yalnızca ihtiyacınız olan alanları isteyin, “belki lazım olur” diye ekstra alan eklemeyin

Sunucu tarafında da query complexity ve query depth limiting mekanizmalarının kurulu olduğundan emin olun. Aksi takdirde kötü niyetli veya dikkatsiz bir sorgu veritabanınızı çökertebilir.

Sonuç

GraphQL query yazımı ilk bakışta karmaşık görünebilir ama mantığı kavradıktan sonra REST’e geri dönmek zor oluyor. Temel prensipleri özetleyelim:

  • Sorgular hiyerarşik yapıda yazılır ve response aynı hiyerarşiyi yansıtır
  • Variables kullanarak sorgularınızı dinamik ve yeniden kullanılabilir yapın
  • Fragment’lar tekrar eden alanları yönetmenizi sağlar
  • Directive’ler koşullu alan dahil etme için kullanılır
  • Introspection ile bilinmeyen API’leri keşfedebilirsiniz
  • Partial success kavramını anlayın ve hata yönetiminizi buna göre yazın

Bir sonraki adım olarak Mutation’ları ve gerçek zamanlı veri için Subscription’ları incelemenizi tavsiye ederim. Ama önce bu yazıdaki örnekleri kendi projenize uyarlayarak pratik yapın. GraphiQL veya Apollo Sandbox gibi araçları kullanarak sorgularınızı görsel olarak test edin, çok daha hızlı öğrenirsiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir