Yan projeler geliştirmek benim için hem bir hobi hem de yeni teknolojileri deneme alanı. Genellikle hızlı sonuç almak isterim; çünkü motivasyonumu canlı tutan şey, fikirleri bir an önce çalışır hale getirebilmek. Bu hız arayışı içinde, özellikle veri tabanı etkileşimlerinde ORM (Object-Relational Mapping) araçları hep bir ikilem yaratır: bana muazzam bir başlangıç hızı sunsalar da, zamanla kontrolü kaybetme riski de beraberinde gelir. “Hız için kontrolden vazgeçilir mi?” sorusu, her yeni yan projede zihnimde dönüp durur.
Kendi Android spam uygulamamın backend’ini yazarken veya finansal hesaplayıcılarım için veri modellerini oluştururken, bu sorunun cevabı projenin kapsamına ve beklentilerime göre değişiyor. Bazen anlık hız kritikken, bazen de uzun vadeli performans ve esneklik daha ağır basıyor. Bu yazıda, yan projelerimde ORM kullanımına dair kişisel deneyimlerimi, ne zaman hangi tercihi yaptığımı ve bu kararların sonuçlarını anlatacağım.
Yan Projelerde Zaman Baskısı ve ORM’in Cazibesi
Yan projelerde en değerli kaynak zamandır. Çoğu zaman kendi kişisel zamanımdan çalarak bu projeleri yürütürüm ve bu nedenle her saniyeyi verimli kullanmak isterim. ORM’ler, bu konuda gerçekten hayat kurtarıcı olabiliyor. Bir tablonun modelini tanımlamak, CRUD (Create, Read, Update, Delete) operasyonlarını birkaç satır kodla halletmek, manuel SQL yazmaktan çok daha hızlıdır. Özellikle FastAPI ile bir backend yazarken SQLAlchemy gibi bir ORM’i entegre etmek, veri tabanı etkileşimlerini adeta bir sihirbaz gibi otomatikleştirebiliyor.
Kısa süre önce, kendi siteme eklediğim basit bir not alma uygulamasının backend’ini geliştirirken, hızlıca bir API ayağa kaldırmam gerekiyordu. sqlalchemy-pydantic ve FastAPI kombinasyonu ile model tanımlarını birleştirip, dakikalar içinde bir Note modeli ve bunun için gerekli tüm API endpoint’lerini oluşturmuştum. Bu, saatler sürecek manuel SQL şema tanımlama ve sorgu yazma işini ortadan kaldırdı. Sadece Note modelini tanımlayıp, basit CRUD operasyonları için hazır metotları çağırmak yeterli oldu.
# models.py
from sqlalchemy import Column, Integer, String, Text
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Note(Base):
__tablename__ = "notes"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
content = Column(Text)
# main.py (FastAPI endpoint)
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from database import SessionLocal, engine
from models import Base, Note
Base.metadata.create_all(bind=engine)
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/notes/", response_model=NoteSchema)
def create_note(note: NoteCreate, db: Session = Depends(get_db)):
db_note = Note(**note.dict())
db.add(db_note)
db.commit()
db.refresh(db_note)
return db_note
Bu örnekte, Note modelini tanımladıktan sonra create_note fonksiyonu ile yeni bir not eklemek, neredeyse tek bir satırda Note(**note.dict()) ile hallediliyor. ORM, bu Pydantic modelini alıp doğrudan veri tabanına yazabilecek bir SQL INSERT sorgusuna çeviriyor. Bu tür bir otomasyon, bir yan projenin ilk adımlarında motivasyonu ve hızı inanılmaz derecede artırır. Şema değişiklikleri olduğunda da, Alembic gibi araçlarla migration’ları yönetmek, manuel ALTER TABLE komutları yazmaktan çok daha az zahmetlidir.
Kontrol Kaybının Geri Dönülmez Yüzü: Performans ve Debugging
ORM’lerin getirdiği hızın bir bedeli var: kontrol kaybı. Özellikle karmaşık sorgular veya performans-kritik senaryolarda, ORM’in arkada ürettiği SQL sorgularının ne kadar optimize olduğunu tahmin etmek zorlaşabiliyor. Benim deneyimimde, bu durum genellikle “N+1 problemi” veya gereksiz JOIN’ler şeklinde kendini gösteriyor. Kendi yan ürünümün finansal hesaplayıcılarında, bir kullanıcının geçmiş işlemlerini ve bu işlemlerle ilişkili kategorileri listelerken, basit bir ORM sorgusu yüzünden sayfa yükleme süresi belirgin biçimde uzamıştı.
Sorunun kaynağını bulmak için veri tabanı loglarını ve EXPLAIN ANALYZE çıktılarını incelemem gerekti. ORM, her bir işlem için ayrı ayrı kategori detaylarını çekmeye çalıştığından, 100 işlem için 100’den fazla ayrı sorgu çalıştırıyordu. Bu durum, veri tabanı bağlantı overhead’i ve ağ gecikmesi nedeniyle performansı felç etmişti. O an anladım ki, ORM’in “akıllı” davrandığını düşündüğüm yerlerde, aslında benim kontrolümden çıkmış ve verimsiz bir iş yapmıştı.
Aşağıdaki gibi bir ORM sorgusu, N+1 problemine yol açabilir:
# Verimsiz ORM sorgusu örneği (N+1 problemi)
transactions = db.query(Transaction).all() # Tüm işlemleri çek
for transaction in transactions:
# Her işlem için ayrı bir kategori sorgusu
category = db.query(Category).filter_by(id=transaction.category_id).first()
print(f"Transaction: {transaction.amount}, Category: {category.name}")
Bu kod bloğu, eğer 100 işlem varsa, 1 (tüm işlemleri çekmek için) + 100 (her bir işlemin kategorisini çekmek için) = 101 sorgu çalıştıracaktır. Oysa, tek bir JOIN ile bu işlem çok daha verimli yapılabilirdi. PostgreSQL’de EXPLAIN ANALYZE çıktısı, bu sorguların ne kadar maliyetli olduğunu ve indeks kullanımını göstererek sorunu ortaya çıkarmama yardımcı oldu. Orada gördüğüm yüzlerce SELECT sorgusu, beni bu konuda daha dikkatli olmaya itti.
Raw SQL’in Gücü ve Getirdiği Yük
Performansın kritikleştiği veya ORM’in yeteneklerinin yetersiz kaldığı anlarda, raw SQL’e dönmek benim için kaçınılmaz oluyor. Özellikle üretim ERP’sinde karmaşık raporlama veya AI ile üretim planlama algoritmaları için veri çekerken, ORM’in ürettiği sorgular asla yeterli gelmezdi. PostgreSQL’in gelişmiş pencere fonksiyonları (window functions), CTE’ler (Common Table Expressions) veya özel indeks stratejileri (GIN, BRIN) gibi özelliklerini kullanabilmek için raw SQL tek yoldu.
Raw SQL’in en büyük avantajı, veri tabanına tam kontrol sağlamasıdır. İhtiyacım olan sorguyu milisaniye hassasiyetinde optimize edebilir, veri tabanının tüm gücünden faydalanabilirim. Kendi yan ürünümde aylık finansal özet raporları oluştururken, GROUP BY, SUM ve LAG gibi fonksiyonları içeren karmaşık bir sorguyu ORM ile yazmak hem çok zor olurdu hem de performansı tatmin edici olmazdı. Bu tür durumlarda doğrudan veri tabanı bağlantısını kullanarak sorguyu manuel olarak çalıştırırım.
-- Raw SQL örneği: Aylık finansal özet raporu (PostgreSQL)
WITH MonthlySummary AS (
SELECT
DATE_TRUNC('month', transaction_date) AS month_start,
SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) AS total_income,
SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) AS total_expense
FROM transactions
WHERE user_id = :user_id
GROUP BY month_start
ORDER BY month_start
),
PreviousMonth AS (
SELECT
month_start,
total_income,
total_expense,
LAG(total_income, 1, 0) OVER (ORDER BY month_start) AS prev_month_income,
LAG(total_expense, 1, 0) OVER (ORDER BY month_start) AS prev_month_expense
FROM MonthlySummary
)
SELECT
month_start,
total_income,
total_expense,
(total_income - prev_month_income) AS income_delta,
(total_expense - prev_month_expense) AS expense_delta
FROM PreviousMonth;
Bu sorgu, bir kullanıcının aylık gelir ve giderlerini hesaplayıp, bir önceki aya göre değişimleri gösteriyor. Bu karmaşıklıkta bir sorguyu ORM ile oluşturmak, çoğu zaman ORM’in kendi DSL’ini (Domain Specific Language) zorlayarak okunaksız ve bakımı zor kodlara yol açar. Ancak raw SQL’in de getirdiği bazı yükler var: SQL injection riskini manuel olarak yönetmek, şema değişikliklerini takip etmek ve sorguların sonuçlarını programlama dilindeki objelere manuel olarak eşlemek gibi. Özellikle güvenlik konusunda, parametreli sorgular kullanmak elzemdir.
Hibrit Yaklaşım: En İyisini Almak
Yan projelerimde ideal çözüm genellikle bir hibrit yaklaşım oluyor. ORM’i, uygulamanın büyük çoğunluğunu oluşturan basit CRUD işlemleri ve hızlı prototipleme için kullanırken, performansın kritik olduğu veya veri tabanının özel yeteneklerine ihtiyaç duyduğum kısımlarda raw SQL’e başvuruyorum. Bu, bana hem geliştirme hızı hem de performans kontrolü sağlıyor.
Örneğin, bir yan ürünümün ERP modülünde, operatör ekranları için basit veri giriş-çıkışları ORM ile yönetilirken, üretim planlama motorunun ihtiyaç duyduğu karmaşık stok optimizasyon sorguları doğrudan raw SQL ile yazıldı. ORM’in connection pool’unu kullanarak raw SQL sorgularını çalıştırmak, mevcut veri tabanı bağlantı yönetimini yeniden yazma derdinden kurtarıyor. FastAPI’da SessionLocal objesini alıp, db.execute(text("...")) şeklinde kullanmak oldukça pratiktir.
from sqlalchemy import text
from fastapi import Depends
from sqlalchemy.orm import Session
from database import get_db
@app.get("/advanced_report/")
def get_advanced_report(user_id: int, db: Session = Depends(get_db)):
# Raw SQL sorgusu
query = text("""
SELECT
p.product_name,
SUM(oi.quantity * oi.unit_price) AS total_sales,
COUNT(DISTINCT o.order_id) AS total_orders
FROM products p
JOIN order_items oi ON p.product_id = oi.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.customer_id = :user_id
GROUP BY p.product_name
ORDER BY total_sales DESC
""")
result = db.execute(query, {"user_id": user_id}).fetchall()
# Sonuçları işleme (örneğin dict'e çevirme)
report_data = []
for row in result:
report_data.append({
"product_name": row[0],
"total_sales": float(row[1]), # Decimal'ı float'a çevirme
"total_orders": row[2]
})
return report_data
Bu yaklaşım, ORM’in getirdiği kolaylıkları terk etmeden, gerektiğinde veri tabanının derinliklerine inebilme esnekliğini sunar. Benim için bu denge, bir projenin ömrü boyunca hem hızlı iterasyon yapabilmeyi hem de karşılaşabileceğim performans sorunlarına karşı hazırlıklı olmayı sağlıyor. Hızlı prototiplemeyle başlarım, ancak bir sorgu loglarında gözle görülür şekilde yavaşladığında veya bir SELECT ifadesi beklediğimden fazla JOIN içerdiğinde alarm zilleri çalar ve raw SQL’e geçişi düşünürüm.
Güvenlik ve Sürdürülebilirlik Perspektifi
Güvenlik, yan projelerde bile asla göz ardı etmediğim bir konu. ORM’ler, SQL injection gibi yaygın güvenlik açıklarına karşı otomatik bir koruma katmanı sunar. Parametreleri otomatik olarak sanitize ederler, bu da geliştiricinin bu konuda manuel hata yapma olasılığını azaltır. Kendi Android spam uygulamamın backend’i gibi kamuya açık bir API’de bu otomasyon büyük bir rahatlık sağlar. fail2ban kuralları yazarken, sıkça gördüğüm SQL injection denemelerine karşı, ORM kullanan endpoint’lerim genellikle daha dirençli oluyor.
Raw SQL kullanırken ise bu sorumluluk tamamen bana ait olur. Her zaman parametreli sorgular (prepared statements) kullanırım ve asla string birleştirme ile sorgu oluşturmam. Bu, SQL injection riskini minimize etmenin en temel yoludur.
Sürdürülebilirlik açısından bakıldığında, ORM’ler şema değişikliklerini yönetmek için migration araçlarıyla (Alembic gibi) entegre çalışır. Bu, veri tabanı şemasının evrimini takip etmeyi ve farklı ortamlar arasında senkronizasyonu kolaylaştırır. Bir yan projemin ilk aşamalarında, sürekli yeni özellikler ekleyip şemayı değiştirdiğimde, Alembic benim için paha biçilmez bir araç oldu. Manuel ALTER TABLE komutları yazmak yerine, alembic revision --autogenerate komutuyla otomatik olarak migration dosyaları oluşturup uygulayabiliyordum.
Raw SQL sorguları ise daha statik olma eğilimindedir. Şema değiştiğinde, bu sorguları manuel olarak güncellemek gerekir. Bu durum, projenin büyüklüğüne ve değişim sıklığına bağlı olarak ciddi bir bakım yükü oluşturabilir. Bu yüzden, kritik ve performans odaklı raw SQL parçalarını genellikle iyi test edilmiş ve nadiren değişen alanlarda kullanmaya özen gösteririm. Bir bankanın iç platformu gibi, güvenlik ve sürdürülebilirlik standartlarının çok yüksek olduğu yerlerde, bu tür trade-off’lar daha dikkatli yönetilmelidir.
Benim Karar Mekanizmam ve Sonuç
Yan projelerimde ORM kullanımı kararım, projenin başlangıç aşaması, büyüklüğü, beklenen trafik ve performans gereksinimleri gibi birçok faktöre bağlıdır. Genellikle şu adımları izlerim:
- Hızlı Prototipleme Aşaması: Projeye başlarken, fikirleri hızlıca test etmek için genellikle ORM’i tercih ederim. Bu aşamada temel CRUD işlemleri için ORM’in sağladığı hız paha biçilmezdir. Birkaç gün içinde çalışan bir MVP (Minimum Viable Product) çıkarmak, motivasyonumu yüksek tutar.
- Performans İzleme: Uygulama bir miktar kullanıcı çekmeye veya veri hacmi artmaya başladığında, performans izlemeye başlarım. Veri tabanı sorgu loglarını aktif olarak takip ederim. Herhangi bir sorgunun yavaşladığını veya gereksiz yere karmaşıklaştığını gördüğümde, oraya müdahale etme zamanı gelmiş demektir.
- Raw SQL’e Geçiş: Eğer bir bölümün performansı tatmin edici değilse veya ORM’in DSL’i karmaşık bir sorguyu ifade etmekte yetersiz kalıyorsa, o bölüm için raw SQL’e geçerim. Bu geçişi, ORM’in connection pool’unu kullanarak mümkün olduğunca sorunsuz yapmaya çalışırım.
- Güvenlik Kontrolleri: Her zaman parametreli sorgular kullanarak SQL injection’dan kaçınırım. Gerekirse
auditdgibi sistem araçlarıyla veri tabanı etkileşimlerini izlerim.
Sonuç olarak, yan projelerde “hız için kontrolden vazgeçilir mi?” sorusunun cevabı, benim için “evet, ama kontrollü bir şekilde”dir. Başlangıçta hız için biraz kontrolü feda ederim, ancak projenin olgunlaşmasıyla birlikte kontrolü geri kazanmak için stratejiler geliştiririm. Bu bir denge oyunudur ve her projenin kendi dinamikleri vardır. Önemli olan, bu trade-off’ların farkında olmak ve doğru zamanda doğru aracı kullanabilmektir.
Daha önce VPS migration sürecinde de benzer bir trade-off yaşamıştım; hızlıca ayağa kaldırmak için hazır imajlar kullanırken, zamanla performansı optimize etmek için kernel ayarlarıyla oynamam gerekmişti. Bu, teknolojinin her alanında karşılaşılan bir durum. Bir sonraki yazımda, kendi yan ürünümün backend’inde yaşadığım “container disk yangını” sorununu ve nasıl çözdüğümü anlatacağım.