Takip et

Flask-SQLAlchemy ile Çoka-Çok Veritabanı İlişkilerini Kullanma

Flask-SQLAlchemy ile Çoka-Çok Veritabanı İlişkilerini Kullanma Flask-SQLAlchemy, Flask uygulamaları için SQLAlchemy ORM’ini entegre eden güçlü bir uzantıdır.

Flask-SQLAlchemy ile Çoka-Çok Veritabanı İlişkilerini Kullanma

Flask-SQLAlchemy, Flask uygulamaları için SQLAlchemy ORM’ini entegre eden güçlü bir uzantıdır. Modern web uygulamalarının karmaşık veri yapılarını yönetmek için veritabanı ilişkileri kritik öneme sahiptir. Bu ilişkiler arasında en esnek ve genellikle en yanlış anlaşılanlardan biri çoka-çok (many-to-many) ilişkileridir. Bu makalede, çoka-çok ilişkilerinin ne olduğunu, neden gerekli olduklarını ve Flask-SQLAlchemy kullanarak bu ilişkileri nasıl etkili bir şekilde tanımlayacağınızı, yöneteceğinizi ve optimize edeceğinizi ayrıntılı olarak inceleyeceğiz. Amacımız, hem teorik bilgiyi pekiştirmek hem de pratik kod örnekleriyle Flask-SQLAlchemy’nin bu alandaki gücünü göstermektir.

Veritabanı İlişkilerine Genel Bakış

Veritabanları, gerçek dünyadaki varlıkları ve bu varlıklar arasındaki bağlantıları modellemek için tasarlanmıştır. Bu bağlantılar, ilişkisel veritabanlarının temelini oluşturur ve genellikle üç ana türde sınıflandırılır: bire-bir, bire-çok (veya çoka-bir) ve çoka-çok.

İlişki Çeşitleri ve Çoka-Çok İlişkisinin Önemi

* Bire-Bir (One-to-One): Bir tablodaki her kaydın, diğer tablodaki yalnızca bir kayda karşılık geldiği ve bunun tersinin de geçerli olduğu ilişkidir. Örneğin, bir Kullanıcı tablosu ile KullanıcıProfili tablosu arasında bire-bir ilişki olabilir; her kullanıcının bir profili ve her profilin bir kullanıcısı vardır. Genellikle, büyük bir tablonun belirli sütunlarını ayırarak performansı artırmak veya gizliliği yönetmek için kullanılır.

* Bire-Çok / Çoka-Bir (One-to-Many / Many-to-One): En yaygın ilişki türüdür. Bir tablodaki bir kaydın diğer tablodaki birden çok kayda karşılık geldiği, ancak diğer tablodaki her kaydın ilk tablodaki yalnızca bir kayda karşılık geldiği ilişkidir. Örneğin, bir Yazar birçok Kitap yazabilir, ancak bir Kitap genellikle tek bir Yazar tarafından yazılır (bu basitleştirilmiş bir örnektir, çoka-çok ilişkisiyle karıştırılmamalıdır). Bu ilişki, genellikle “çok” tarafındaki tabloda, “bir” tarafındaki tablonun birincil anahtarını yabancı anahtar (foreign key) olarak tutarak modellenir.

* Çoka-Çok (Many-to-Many): Bir tablodaki bir kaydın diğer tablodaki birden çok kayda karşılık geldiği ve diğer tablodaki bir kaydın da ilk tablodaki birden çok kayda karşılık geldiği ilişkidir. Örneğin, bir Öğrenci birden çok Ders alabilir ve bir Ders de birden çok Öğrenci tarafından alınabilir. Bu ilişki türü, doğrudan iki tablo arasında bir yabancı anahtar ile modellenemez. İlişkisel veritabanı teorisine göre, çoka-çok ilişkilerini modellemek için üçüncü bir tabloya, yani bir ara tabloya (association table) ihtiyaç duyulur. Bu ara tablo, iki ana tablonun birincil anahtarlarını yabancı anahtar olarak içerir ve bu iki ana tablo arasındaki bağlantıyı sağlar. Çoka-çok ilişkileri, gerçek dünya senaryolarında oldukça yaygın olduğundan ve karmaşık veri yapılarını esnek bir şekilde temsil etmeye olanak tanıdığından, veritabanı tasarımında kritik bir role sahiptir.

Çoka-Çok İlişkilerinin Temeli: Ara Tablo

Çoka-çok ilişkilerini anlamanın anahtarı, ara tablo veya diğer adıyla birleşim tablosu (join table) veya bağlantı tablosu (linking table) kavramını kavramaktır. Bu tablo, iki ana varlık arasındaki doğrudan çoka-çok ilişkisini, iki ayrı bire-çok ilişkisine dönüştürerek çözer.

Kavram ve Yapı

Ara tablo, ilişkilendirmek istediğimiz iki ana tablonun birincil anahtarlarını yabancı anahtar olarak içeren özel bir tablodur. Bu tablonun kendisinin genellikle bir birincil anahtarı olmaz, ancak iki yabancı anahtarın birleşimi genellikle bir bileşik birincil anahtar (composite primary key) görevi görür ve her bir ilişkinin benzersiz olmasını sağlar.

Örneğin, bir Yazar ve Kitap arasında çoka-çok ilişkisi olduğunu düşünelim (bir kitap birden fazla yazar tarafından yazılabilir ve bir yazar birden fazla kitap yazabilir). Bu durumda, yazar_kitap adında bir ara tablo oluştururuz. Bu tablo aşağıdaki sütunlara sahip olabilir:

* yazar_id: Yazar tablosunun birincil anahtarına referans veren bir yabancı anahtar.
* kitap_id: Kitap tablosunun birincil anahtarına referans veren bir yabancı anahtar.

Bu iki sütun, birlikte yazar_kitap tablosunun bileşik birincil anahtarını oluşturur. Bu sayede, aynı yazarın aynı kitaba birden fazla kez atanması engellenir. Bu ara tablo, Yazar ile yazar_kitap arasında bire-çok, ve Kitap ile yazar_kitap arasında da bire-çok ilişki kurar. Böylece, orijinal çoka-çok ilişkisi iki bire-çok ilişkisine ayrıştırılmış olur.

Ara tablolar sadece bu iki yabancı anahtarı içermek zorunda değildir. İlişkinin kendisine ait ek nitelikler (payload attributes) de içerebilirler. Örneğin, bir Öğrenci ve Ders ilişkisinde, ara tablo öğrenci_ders aynı zamanda öğrencinin dersten aldığı not veya kayıt_tarihi gibi bilgileri de tutabilir. Bu senaryo, Flask-SQLAlchemy’de “Association Object Pattern” olarak bilinen daha gelişmiş bir yaklaşımla ele alınır ve makalenin ilerleyen bölümlerinde ayrıntılı olarak ele alınacaktır.

Flask-SQLAlchemy ile Çoka-Çok İlişkisi Tanımlama

Flask-SQLAlchemy, çoka-çok ilişkilerini tanımlamak için sezgisel ve güçlü bir API sunar. İlk olarak temel kurulumu ve modelleri ele alalım, ardından db.relationship() metodunun parametrelerini inceleyelim.

Temel Kurulum ve Modeller

Öncelikle bir Flask uygulaması ve Flask-SQLAlchemy uzantısı kurulumunu yapalım. Daha sonra, bir Yazar (Author) ve Kitap (Book) modeli üzerinden çoka-çok ilişkisini örnekleyelim.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///many_to_many_example.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

Ara tabloyu tanımlama

Bu, db.Model'den türemeyen, sadece bir tablo nesnesidir.

İki yabancı anahtar ve bileşik birincil anahtar içerir.

kitap_yazarlari = db.Table('kitap_yazarlari', db.Column('kitap_id', db.Integer, db.ForeignKey('kitap.id'), primary_key=True), db.Column('yazar_id', db.Integer, db.ForeignKey('yazar.id'), primary_key=True) ) class Yazar(db.Model): id = db.Column(db.Integer, primary_key=True) ad = db.Column(db.String(100), nullable=False) # Bu yazarın yazdığı kitaplar # secondary parametresi, ara tabloyu işaret eder. # backref, Kitap modelinde 'yazarlar' adında otomatik bir ilişki oluşturur. kitaplar = db.relationship('Kitap', secondary=kitap_yazarlari, backref=db.backref('yazarlar', lazy=True)) def __repr__(self): return f'' class Kitap(db.Model): id = db.Column(db.Integer, primary_key=True) baslik = db.Column(db.String(200), nullable=False) yayin_yili = db.Column(db.Integer) # Yazarlar ilişkisi backref tarafından oluşturulduğu için burada explicit olarak tanımlamaya gerek yok. # Ancak tanımlansaydı şöyle olurdu: # yazarlar = db.relationship('Yazar', secondary=kitap_yazarlari, # back_populates='kitaplar') def __repr__(self): return f''

Veritabanını oluşturma

with app.app_context(): db.create_all()

Yukarıdaki kodda dikkat edilmesi gerekenler:

1. db.Table(): Bu, Flask-SQLAlchemy’nin ara tabloyu tanımlamak için kullandığı özel bir yapıdır. db.Model‘den türemeyen, sadece bir tablo nesnesi olarak var olan bir yapıdır. İlk argüman tablonun adıdır ('kitap_yazarlari'). Diğer argümanlar ise sütun tanımlamalarıdır. Her iki sütun da ilgili ana tabloların birincil anahtarlarına referans veren db.ForeignKey‘ler olmalıdır. Ayrıca, her iki sütun da primary_key=True olarak işaretlenerek bileşik birincil anahtar oluşturulur.
2. Yazar ve Kitap Modelleri: Her iki model de standart db.Model‘den türetilmiştir ve kendi birincil anahtarlarına ve diğer niteliklerine sahiptir.

db.relationship() Kullanımı

Çoka-çok ilişkisini kurmanın kalbi db.relationship() metodunda yatar. Bu metot, ORM’in ilişkili nesneleri nasıl yükleyeceğini ve yöneteceğini tanımlar.

Yazar modelindeki kitaplar ilişkisini inceleyelim:

kitaplar = db.relationship('Kitap', secondary=kitap_yazarlari,
                           backref=db.backref('yazarlar', lazy=True))

* 'Kitap': İlişkinin diğer tarafındaki modelin adıdır. Yani, bir Yazar nesnesi üzerinden hangi türden nesnelere erişeceğimizi belirtir (bu durumda Kitap nesnelerine).
* secondary=kitap_yazarlari: Bu parametre, bu ilişkinin bir çoka-çok ilişkisi olduğunu ve kitap_yazarlari ara tablosunu kullanarak kurulduğunu belirtir. Bu, db.Table() ile tanımladığımız tablo nesnesidir.
* backref=db.backref('yazarlar', lazy=True): backref parametresi, ilişkinin diğer tarafında (bu durumda Kitap modelinde) otomatik olarak bir ters ilişki oluşturur. Yani, Kitap modeli üzerinde yazarlar adında bir özellik oluşturur. Bu özellik, ilgili kitaba ait yazarları sorgulamak için kullanılabilir. lazy=True ise bu ters ilişkinin nasıl yükleneceğini belirtir. lazy parametresi, performans açısından önemlidir ve farklı değerler alabilir:
* select (varsayılan): İlişkili nesneler, erişildiğinde ayrı bir SELECT sorgusu ile yüklenir. Çoğu zaman iyi bir dengedir.
* joined: Ana sorgu ile aynı anda, JOIN kullanarak yüklenir. N+1 sorgu problemini önlemek için iyidir ancak ana sorguyu daha karmaşık hale getirebilir.
* subquery: İlişkili nesneler, ana sorgu ile birlikte bir alt sorgu kullanılarak yüklenir. joined‘a benzer ancak bazı durumlarda daha iyi performans gösterebilir.
* dynamic: İlişkili nesneler hemen yüklenmez, bunun yerine bir sorgu nesnesi döndürülür. Bu, ilişkili nesneler üzerinde daha fazla filtreleme veya sıralama yapmak istediğinizde kullanışlıdır.
* False / True: False hemen yükler (eager loading), True ise select ile aynıdır.

backref yerine back_populates de kullanılabilir. back_populates, her iki modelde de ilişkinin açıkça tanımlanmasını gerektirir ve daha net bir kod okunabilirliği sağlayabilir, ancak backref daha kısadır ve otomatik olarak ters ilişkiyi oluşturur. Çoka-çok ilişkilerinde genellikle backref tercih edilir.

İlişkili Verilerle Çalışma: CRUD İşlemleri

Flask-SQLAlchemy ile çoka-çok ilişkilerini tanımladıktan sonra, sıra bu ilişkiler üzerinde temel CRUD (Create, Read, Update, Delete) işlemlerini gerçekleştirmeye gelir.

Veri Ekleme ve Sorgulama

Yeni yazarlar ve kitaplar oluşturup bunları ilişkilendirelim.

with app.app_context():
    # Yeni yazarlar oluşturma
    yazar1 = Yazar(ad='George Orwell')
    yazar2 = Yazar(ad='Aldous Huxley')
    yazar3 = Yazar(ad='J.K. Rowling')

    db.session.add_all([yazar1, yazar2, yazar3])
    db.session.commit()

    # Yeni kitaplar oluşturma
    kitap1 = Kitap(baslik='1984', yayin_yili=1949)
    kitap2 = Kitap(baslik='Hayvan Çiftliği', yayin_yili=1945)
    kitap3 = Kitap(baslik='Cesur Yeni Dünya', yayin_yili=1932)
    kitap4 = Kitap(baslik='Harry Potter ve Felsefe Taşı', yayin_yili=1997)

    db.session.add_all([kitap1, kitap2, kitap3, kitap4])
    db.session.commit()

    # Kitapları yazarlarla ilişkilendirme
    # Yazar1 (George Orwell) 1984 ve Hayvan Çiftliği'ni yazdı
    yazar1.kitaplar.append(kitap1)
    yazar1.kitaplar.append(kitap2)

    # Yazar2 (Aldous Huxley) Cesur Yeni Dünya'yı yazdı
    yazar2.kitaplar.append(kitap3)

    # Yazar3 (J.K. Rowling) Harry Potter'ı yazdı
    yazar3.kitaplar.append(kitap4)

    # Bir kitap birden fazla yazar tarafından yazılmışsa
    # Örneğin, kitap1'e (1984) başka bir yazar ekleyelim (sadece örnek amaçlı)
    # yazar2.kitaplar.append(kitap1) # Bu durumda kitap1 hem Orwell hem Huxley'e ait olurdu.

    db.session.commit()

    print("--- Veri Ekleme Başarılı ---")

    # Veri Sorgulama
    print("\n--- Kitapları ve Yazarlarını Sorgulama ---")
    all_books = Kitap.query.all()
    for book in all_books:
        print(f"Kitap: {book.baslik} ({book.yayin_yili})")
        print(f"  Yazarları:")
        for author in book.yazarlar: # backref ile oluşturulan yazarlar özelliği
            print(f"    - {author.ad}")

    print("\n--- Yazarları ve Kitaplarını Sorgulama ---")
    all_authors = Yazar.query.all()
    for author in all_authors:
        print(f"Yazar: {author.ad}")
        print(f"  Kitapları:")
        for book in author.kitaplar:
            print(f"    - {book.baslik} ({book.yayin_yili})")

    # Belirli bir yazarın kitaplarını sorgulama
    orwell = Yazar.query.filter_by(ad='George Orwell').first()
    if orwell:
        print(f"\n{orwell.ad}'ın kitapları:")
        for book in orwell.kitaplar:
            print(f"  - {book.baslik}")

    # Belirli bir kitabın yazarlarını sorgulama
    kitap_1984 = Kitap.query.filter_by(baslik='1984').first()
    if kitap_1984:
        print(f"\n'{kitap_1984.baslik}' kitabının yazarları:")
        for author in kitap_1984.yazarlar:
            print(f"  - {author.ad}")

Veri ekleme işlemi oldukça basittir: İlgili nesneleri oluşturduktan sonra, ilişkinin bir tarafındaki liste benzeri özelliğe (yazar1.kitaplar) diğer tarafın nesnesini (kitap1) append() metodu ile eklersiniz. SQLAlchemy, db.session.commit() çağrıldığında ara tabloya gerekli girişleri otomatik olarak ekler.

Sorgulama da aynı şekilde kolaydır. İlişki özelliği (yazar.kitaplar veya kitap.yazarlar) üzerinden doğrudan ilgili nesnelerin bir listesine erişebilirsiniz.

Veri Güncelleme ve Silme

İlişkileri güncellemek veya kaldırmak da benzer şekilde liste manipülasyonu ile yapılır.

with app.app_context():
    # Örnek veri ekleme (yukarıdaki koddan sonra çalıştırmak için)
    # yazar1 = Yazar(ad='George Orwell')
    # yazar2 = Yazar(ad='Aldous Huxley')
    # kitap1 = Kitap(baslik='1984')
    # kitap3 = Kitap(baslik='Cesur Yeni Dünya')
    # db.session.add_all([yazar1, yazar2, kitap1, kitap3])
    # yazar1.kitaplar.append(kitap1)
    # yazar2.kitaplar.append(kitap3)
    # db.session.commit()

    # Mevcut bir yazar ve kitap alalım
    orwell = Yazar.query.filter_by(ad='George Orwell').first()
    brave_new_world = Kitap.query.filter_by(baslik='Cesur Yeni Dünya').first()
    huxley = Yazar.query.filter_by(ad='Aldous Huxley').first()

    if orwell and brave_new_world:
        print(f"\n--- İlişki Güncelleme ---")
        print(f"'{brave_new_world.baslik}' kitabının mevcut yazarları: {[a.ad for a in brave_new_world.yazarlar]}")
        
        # Orwell'i Cesur Yeni Dünya'nın yazarı olarak ekleyelim (örnek)
        orwell.kitaplar.append(brave_new_world)
        db.session.commit()
        print(f"Orwell, '{brave_new_world.baslik}' kitabına yazar olarak eklendi.")
        print(f"'{brave_new_world.baslik}' kitabının yeni yazarları: {[a.ad for a in brave_new_world.yazarlar]}")

    if orwell and brave_new_world:
        print(f"\n--- İlişki Silme ---")
        print(f"'{brave_new_world.baslik}' kitabının mevcut yazarları: {[a.ad for a in brave_new_world.yazarlar]}")

        # Orwell'i Cesur Yeni Dünya'nın yazarlarından çıkaralım
        orwell.kitaplar.remove(brave_new_world)
        db.session.commit()
        print(f"Orwell, '{brave_new_world.baslik}' kitabından yazar olarak çıkarıldı.")
        print(f"'{brave_new_world.baslik}' kitabının yeni yazarları: {[a.ad for a in brave_new_world.yazarlar]}")

    # Bir yazarın veya kitabın tamamen silinmesi
    # Cascade opsiyonları olmadan, sadece ana kayıt silinir, ara tablodaki referanslar kalır
    # Ancak, ForeignKey'ler genellikle ON DELETE CASCADE ile tanımlanırsa, ilgili ara tablo kayıtları da silinir.
    # Flask-SQLAlchemy'de db.relationship'te cascade="all, delete-orphan" kullanılarak ORM seviyesinde de bu kontrol sağlanabilir.

    # Örneğin, Aldous Huxley'i silelim. Onunla ilişkili kitaplar kalır, sadece ara tablodaki bağlantı silinir.
    if huxley:
        print(f"\n--- Yazar Silme ---")
        print(f"Silinmeden önce Aldous Huxley'in kitapları: {[b.baslik for b in huxley.kitaplar]}")
        db.session.delete(huxley)
        db.session.commit()
        print(f"Aldous Huxley silindi.")
        
        # Kontrol edelim, Brave New World hala var mı ve yazarları boş mu?
        brave_new_world_after_delete = Kitap.query.filter_by(baslik='Cesur Yeni Dünya').first()
        if brave_new_world_after_delete:
            print(f"'{brave_new_world_after_delete.baslik}' kitabı hala mevcut.")
            print(f"Yazarları: {[a.ad for a in brave_new_world_after_delete.yazarlar]}")

İlişkileri güncellemek için append() veya remove() metotlarını kullanabilirsiniz. Bir nesneyi bir ilişkiden çıkarmak, ara tablodaki ilgili kaydı siler.

Ana bir nesneyi (örneğin bir Yazar veya Kitap) sildiğinizde, varsayılan olarak sadece o ana nesne silinir. Ara tablodaki ilgili kayıtlar, eğer yabancı anahtarlarınız ON DELETE CASCADE ile tanımlanmamışsa, “yetim” kalabilir ve veri tutarsızlığına yol açabilir. Bu nedenle, veritabanı şemasında ON DELETE CASCADE kısıtlamalarını kullanmak veya Flask-SQLAlchemy’nin db.relationship tanımında cascade="all, delete-orphan" gibi seçenekleri kullanmak önemlidir. Bu seçenekler, bir ana nesne silindiğinde ilişkili ara tablo kayıtlarının da otomatik olarak silinmesini sağlar.

Ara Tabloya Ek Nitelikler Ekleme (Association Object Pattern)

Bazı durumlarda, çoka-çok ilişkisinin kendisi hakkında bilgi saklamamız gerekebilir. Örneğin, bir Öğrenci bir Derse kaydolduğunda, bu kaydolma olayının bir kayıt_tarihi veya öğrencinin o dersten aldığı bir notu olabilir. Bu tür ek nitelikler, ara tabloya doğrudan eklenebilir. Ancak, db.Table() ile tanımlanan basit ara tablolar bu ek niteliklere ORM seviyesinde doğrudan erişim sağlamaz. Bu durumu ele almak için Association Object Pattern kullanılır.

Neden ve Nasıl

Association Object Pattern, ara tabloyu ayrı bir db.Model sınıfı olarak tanımlayarak çalışır. Bu yeni model, her iki ana modele de bire-çok ilişkisi kurar ve çoka-çok ilişkisinin kendisine ait ek nitelikleri (payload attributes) içerir.

Örnek olarak, Öğrenci ve Ders modelleri arasında bir DersKaydi (Enrollment) association object’i oluşturalım:

from datetime import datetime

... (Flask ve SQLAlchemy kurulumu aynı kalır) ...

Eski kitap_yazarlari tablosunu kaldırın veya yeni bir veritabanı kullanın

db.drop_all() # Geliştirme ortamında kullanışlı olabilir

class DersKaydi(db.Model): __tablename__ = 'ders_kaydi' ogrenci_id = db.Column(db.Integer, db.ForeignKey('ogrenci.id'), primary_key=True) ders_id = db.Column(db.Integer, db.ForeignKey('ders.id'), primary_key=True) kayit_tarihi = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) notu = db.Column(db.String(2)) # Örneğin, 'AA', 'BB', 'FF' # Bu ilişki nesnesinden Öğrenci ve Ders nesnelerine erişim ogrenci = db.relationship('Ogrenci', back_populates='ders_kayitlari') ders = db.relationship('Ders', back_populates='ogrenci_kayitlari') def __repr__(self): return f'' class Ogrenci(db.Model): id = db.Column(db.Integer, primary_key=True) ad = db.Column(db.String(100), nullable=False) soyad = db.Column(db.String(100), nullable=False) # Bir öğrencinin birden çok ders kaydı olabilir (One-to-Many) ders_kayitlari = db.relationship('DersKaydi', back_populates='ogrenci', cascade="all, delete-orphan") # Öğrenci silinince kayıtları da silinsin # Öğrencinin aldığı derslere doğrudan erişim için # Bu, association object'i atlayarak derslere doğrudan erişim sağlar dersler = db.relationship('Ders', secondary='ders_kaydi', viewonly=True, overlaps="ders_kayitlari,ders") # SQLAlchemy 2.0+ için overlaps ipucu def __repr__(self): return f'' class Ders(db.Model): id = db.Column(db.Integer, primary_key=True) kod = db.Column(db.String(10), unique=True, nullable=False) ad = db.Column(db.String(200), nullable=False) # Bir dersin birden çok öğrenci kaydı olabilir (One-to-Many) ogrenci_kayitlari = db.relationship('DersKaydi', back_populates='ders', cascade="all, delete-orphan") # Ders silinince kayıtları da silinsin # Derse kayıtlı öğrencilere doğrudan erişim için ogrenciler = db.relationship('Ogrenci', secondary='ders_kaydi', viewonly=True, overlaps="ogrenci_kayitlari,ogrenci") # SQLAlchemy 2.0+ için overlaps ipucu def __repr__(self): return f'' with app.app_context(): db.create_all()

Bu yapıda:

1. DersKaydi Modeli: Bu, artık bir db.Model‘dir ve ogrenci_id ile ders_id‘yi yabancı anahtar olarak içerir. Ayrıca kayit_tarihi ve notu gibi ek niteliklere sahiptir. ogrenci ve ders ilişkileri, bu kayıt nesnesinden ilgili Ogrenci ve Ders nesnelerine erişim sağlar. back_populates kullanılarak her iki taraftaki ilişki açıkça tanımlanır.
2. Ogrenci ve Ders Modelleri: Bu modeller artık doğrudan çoka-çok ilişkisi kurmak yerine, DersKaydi modeliyle bire-çok ilişkisi kurar. Örneğin, Ogrenci modelindeki ders_kayitlari özelliği, bu öğrencinin tüm DersKaydi nesnelerini döndürür.
3. viewonly=True ve secondary='ders_kaydi': Ogrenci modelindeki dersler ve Ders modelindeki ogrenciler ilişkileri, DersKaydi association object’ini atlayarak doğrudan ilgili derslere veya öğrencilere erişim sağlamak için kullanılır. secondary parametresi burada ara tablo adını alır (modelin kendisi değil, tablonun adı). viewonly=True bu ilişkilerin sadece sorgulama amaçlı olduğunu, bu ilişkiler üzerinden ekleme/silme yapılmadığını belirtir. Ekleme/silme işlemleri DersKaydi nesneleri üzerinden yapılır. overlaps parametresi, SQLAlchemy 2.0’da gerekli hale gelen bir ipucudur ve ilişkilerin çakışmasını önlemek için kullanılır.

İlişkili Verilerle Çalışma (Association Object)

Association Object Pattern ile CRUD işlemleri biraz farklılık gösterir:

with app.app_context():
    # Öğrenci ve dersler oluşturalım
    ogrenci1 = Ogrenci(ad='Ayşe', soyad='Yılmaz')
    ogrenci2 = Ogrenci(ad='Can', soyad='Demir')

    ders1 = Ders(kod='CS101', ad='Giriş Bilgisayar Bilimi')
    ders2 = Ders(kod='MATH201', ad='Lineer Cebir')
    ders3 = Ders(kod='PHY101', ad='Fizik I')

    db.session.add_all([ogrenci1, ogrenci2, ders1, ders2, ders3])
    db.session.commit()

    # Kayıtlar oluşturma (ilişkiyi ek niteliklerle birlikte kurma)
    kayit1 = DersKaydi(ogrenci=ogrenci1, ders=ders1, notu='AA')
    kayit2 = DersKaydi(ogrenci=ogrenci1, ders=ders2, notu='BA')
    kayit3 = DersKaydi(ogrenci=ogrenci2, ders=ders1, notu='BB')
    kayit4 = DersKaydi(ogrenci=ogrenci2, ders=ders3, notu='CC')

    db.session.add_all([kayit1, kayit2, kayit3, kayit4])
    db.session.commit()

    print("--- Association Object ile Veri Ekleme Başarılı ---")

    # Veri Sorgulama
    print("\n--- Öğrencilerin Aldığı Dersler ve Notları ---")
    all_students = Ogrenci.query.all()
    for student in all_students:
        print(f"Öğrenci: {student.ad} {student.soyad}")
        for enrollment in student.ders_kayitlari:
            print(f"  - Ders: {enrollment.ders.ad} ({enrollment.ders.kod}), Not: {enrollment.notu}, Kayıt Tarihi: {enrollment.kayit_tarihi.strftime('%Y-%m-%d')}")

    print("\n--- Bir Derse Kayıtlı Öğrenciler ve Notları ---")
    cs101_ders = Ders.query.filter_by(kod='CS101').first()
    if cs101_ders:
        print(f"Ders: {cs101_ders.ad} ({cs101_ders.kod})")
        for enrollment in cs101_ders.ogrenci_kayitlari:
            print(f"  - Öğrenci: {enrollment.ogrenci.ad} {enrollment.ogrenci.soyad}, Not: {enrollment.notu}")

    # Doğrudan derslere erişim (viewonly=True sayesinde)
    ayse = Ogrenci.query.filter_by(ad='Ayşe').first()
    if ayse:
        print(f"\nAyşe'nin doğrudan erişilebilen dersleri: {[d.ad for d in ayse.dersler]}")

    # Veri Güncelleme
    print("\n--- Kayıt Güncelleme ---")
    ayse_cs101_kayit = DersKaydi.query.filter_by(ogrenci=ayse, ders=cs101_ders).first()
    if ayse_cs101_kayit:
        print(f"Ayşe'nin CS101 dersindeki mevcut notu: {ayse_cs101_kayit.notu}")
        ayse_cs101_kayit.notu = 'AA+'
        db.session.commit()
        print(f"Ayşe'nin CS101 dersindeki yeni notu: {ayse_cs101_kayit.notu}")

    # Veri Silme
    print("\n--- Kayıt Silme ---")
    can = Ogrenci.query.filter_by(ad='Can').first()
    fizik_ders = Ders.query.filter_by(kod='PHY101').first()

    if can and fizik_ders:
        can_fizik_kayit = DersKaydi.query.filter_by(ogrenci=can, ders=fizik_ders).first()
        if can_fizik_kayit:
            print(f"Can'ın Fizik I dersindeki kaydı siliniyor: {can_fizik_kayit}")
            db.session.delete(can_fizik_kayit)
            db.session.commit()
            print("Kayıt silindi.")

        # Kontrol edelim
        print(f"Can'ın kalan dersleri: {[d.ad for d in can.dersler]}")

Association Object Pattern, çoka-çok ilişkisinin kendisine ait ek verileri saklamanız gerektiğinde daha esnek ve güçlü bir çözüm sunar. İşlemler, doğrudan ana modelin ilişki listesine nesne eklemek yerine, DersKaydi gibi association object’leri oluşturup yönetmek üzerine kuruludur.

Performans ve Optimizasyon İpuçları

Çoka-çok ilişkileri, özellikle büyük veri kümeleriyle çalışırken performans sorunlarına yol açabilir. N+1 sorgu problemi ve yanlış yükleme stratejileri yaygın tuzaklardır.

N+1 Sorgu Problemi ve Çözümleri

N+1 sorgu problemi, bir ana sorgunun N adet ilişkili nesne döndürmesi ve her bir ilişkili nesnenin detaylarına erişildiğinde ayrı bir sorgu daha tetiklenmesiyle ortaya çıkar. Bu, toplamda N+1 sorgu anlamına gelir ve veritabanı performansını ciddi şekilde düşürebilir.

Örneğin, tüm yazarları ve her yazarın kitaplarını listelemek istediğimizi varsayalım:

# Kötü örnek: N+1 sorgu problemi
with app.app_context():
    yazarlar = Yazar.query.all() # 1 sorgu
    for yazar in yazarlar:
        print(f"Yazar: {yazar.ad}")
        for kitap in yazar.kitaplar: # Her yazar için ayrı bir sorgu (N sorgu)
            print(f"  - {kitap.baslik}")

Bu durumda, Yazar.query.all() bir sorgu çalıştırır. Ardından, her bir yazar için yazar.kitaplar‘a erişildiğinde, ilişkili kitapları yüklemek için veritabanına ayrı bir sorgu daha gönderilir. Eğer 100 yazar varsa, bu 1 (yazarlar) + 100 (kitaplar) = 101 sorgu demektir.

Bu problemi çözmek için eager loading (hevesli yükleme) kullanılır. Flask-SQLAlchemy, db.relationship() tanımındaki lazy parametresi veya sorgulama anında joinedload() / subqueryload() gibi yöntemlerle eager loading’i destekler.

* joinedload(): İlişkili nesneleri ana sorguyla birlikte bir LEFT OUTER JOIN kullanarak yükler.

with app.app_context():
        # joinedload ile N+1 problemini çözme
        # Tek bir sorgu ile tüm yazarları ve ilgili kitaplarını getirir
        yazarlar = Yazar.query.options(db.joinedload(Yazar.kitaplar)).all()
        for yazar in yazarlar:
            print(f"Yazar: {yazar.ad}")
            for kitap in yazar.kitaplar: # Bu erişim ek sorgu tetiklemez
                print(f"  - {kitap.baslik}")

* subqueryload(): İlişkili nesneleri ayrı bir alt sorgu kullanarak yükler. Bu, ana sorgunun karmaşıklığını azaltırken N+1 problemini çözer.

with app.app_context():
        # subqueryload ile N+1 problemini çözme
        # İki sorgu çalıştırır: biri yazarlar için, diğeri tüm kitaplar için (tek alt sorgu)
        yazarlar = Yazar.query.options(db.subqueryload(Yazar.kitaplar)).all()
        for yazar in yazarlar:
            print(f"Yazar: {yazar.ad}")
            for kitap in yazar.kitaplar: # Bu erişim ek sorgu tetiklemez
                print(f"  - {kitap.baslik}")

Hangi eager loading stratejisinin kullanılacağı, uygulamanızın ihtiyaçlarına ve veritabanı yapısına bağlıdır. joinedload genellikle daha az sorgu sayısı nedeniyle tercih edilirken, çok sayıda ilişki veya büyük veri setleri için subqueryload daha iyi olabilir.

Ek olarak, veritabanı seviyesinde doğru indeksleme yapmak (özellikle yabancı anahtar sütunları üzerinde) sorgu performansını önemli ölçüde artırır. Flask-SQLAlchemy, db.Column tanımlarken index=True parametresini kullanarak indeks oluşturmayı kolaylaştırır.

Sonuç

Flask-SQLAlchemy ile çoka-çok veritabanı ilişkilerini yönetmek, modern web uygulamaları geliştiren herkes için temel bir beceridir. Bu makalede, çoka-çok ilişkilerinin altında yatan ara tablo kavramını, Flask-SQLAlchemy’nin bu ilişkileri db.Table() ve db.relationship() kullanarak nasıl tanımladığını detaylıca inceledik. Basit çoka-çok ilişkilerinden, ilişkinin kendisine ait ek nitelikleri barındıran Association Object Pattern’e kadar çeşitli senaryoları ele aldık.

Veri ekleme, sorgulama, güncelleme ve silme gibi temel CRUD işlemlerini kod örnekleriyle gösterdik. Ayrıca, N+1 sorgu problemi gibi yaygın performans tuzaklarına ve joinedload() ile subqueryload() gibi eager loading teknikleriyle bu sorunların nasıl çözülebileceğine değindik.

Flask-SQLAlchemy’nin sağladığı güçlü ORM yetenekleri sayesinde, karmaşık veritabanı ilişkilerini Python kodunuzda nesne tabanlı bir yaklaşımla ele alabilir, veritabanı işlemlerini soyutlayarak daha temiz ve sürdürülebilir uygulamalar geliştirebilirsiniz. Bu bilgileri kullanarak, Flask uygulamalarınızda daha sağlam ve performanslı veri modelleri oluşturmak için iyi bir başlangıç noktasına sahipsiniz.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

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

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.