SQLAlchemy ve ORM

Merhabalar,

Bu başlıkta biraz ORM kavramından ve SQLAlchemy kütüphanesinden bahsetmek istiyorum. Bahsedeceklerim, hem ORM kavramı hem de SQLAlchemy kütüphanesinin özellikleri hakkında internette bulabileceklerinizin çok küçük bir parçası olacak.

Dolayısıyla bu başlığı, hali hazırda SQL sorguları yazabilen ama henüz SQLAlchemy ile tanışmamış arkadaşlara kendimce bir SQLAlchemy tanıtımı yapmak için açıyorum. Okuyucular içinde konuya meraklı olanlar anlattıklarımla yetinmeyecek ve daha fazlasını öğrenmek isteyeceklerdir diye tahmin ediyorum. Konuyu zaten bilen ama yine de okumaktan zevk alan arkadaşlardan da bir ricam var; burada anlattıklarıma bir göz atın ve yanlış bilgilerin düzeltilmesine katkı sağlayın lütfen.

SQLAlchemy; SQLite, MySQL, PostgreSQL, Oracle, SQL-Server, Firebird gibi veritabanı sistemlerinin çoğu için kullanılabilen bir veritabanı araç kitidir. Veritabanı sorguları yazmak, ilişkili tablolar oluşturup yönetmek için ideal araçlar sunar.

Nesne İlişkilerinin Haritalanması gibi bir anlamı olan ORM, bir veritabanı tasarımı yaklaşımıdır. Burada nesne, bir tabloda yer alan bir satır verisini temsil eden bir modeldir. Bu model, tabloda yer alacak satır verilerinin kaç tane sütuna sahip olacağını, bu sütunların tiplerinin ne olacaklarını, bu sütunlara eklenecek verilerin boyutlarının minimum veya maksimum değerlerinin olup olmayacağını belirlememize yardımcı olur. Yani bizim CREATE TABLE IF NOT EXISTS diye yazdığımız sorguda tanımladığımız veri modelinden bahsediyorum aslında. Ancak, mecbur kalmadıkça SQLAlchemy ile bu türden düşük seviye sorgular yazmamıza gerek yok. Nesnelerin niteliklerini sorgulayan fonksiyonları kullanarak benzer sorguları yapabileceğiz.

Şimdi yavaş yavaş kodlayarak konuyu somutlaştırmaya çalışalım.

Öncelikle, SQLAlchemy kütüphanesini sistemimize yüklü değilse yükleyelim.

pip install sqlalchemy

Şimdi, dedik ki, SQLAlchemy'de veritabanları modeller ile gösterilir. Bu modelleri oluşturmanın iki türlü yolu vardır. Bunlardan birisi, Imperative Base, diğeri de Declarative Base. SQLAlchemy ekibi, PEP484’deki typing önerilerine göre yeni bir söz dizimi önermeye başladı. Bu söz dizimi Declarative base modelini temel alıyor ve haritalama işleminde, tip referanslarının açıkça gösterildiği bir söz diziminin kullanıldığını fark ediyorsunuz. Ben de bu konuda anlatacağım örneklerde Declarative Base söz dizimini kullanacağım.

Öncelikle DeclarativeBase sınıfını programın içine aktaralım. Sonra da, DeclarativeBase'i miras alan bir sınıf oluşturalım.

from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass

Bu aşamada biraz daha ORM üzerine konuşmaya devam edelim. ORM, veritabanlarındaki farklı veri gruplarının birbirleriyle ne türden ilişkiler kurulacağını haritaladığımız bir tasarım modelidir. Temel olarak üç tip nesne ilişkisinden bahsedilebilir. Bunlar; OneToMany, OneToOne ve ManyToMany ilişkileridir. Şimdi biraz bu kavramlardan bahsedelim ve her model için uygun örnekler yapalım.

  1. OneToMany

    Bu ilişki tipi, bir tablo grubundaki verilerin, başka bir tablo grubundaki verilerin çoğu ile bağlantılı olduğu ama ikinci tablodaki her bir verinin ilk tablodaki yalnızca bir veriyle bağlantılı olduğu ilişki tipini ifade eder. Yani kısaca bir Parent - Child ilişkisidir bu. Birazdan OneToMany ilişkisini gösteren bir örnek yapacağız ama yukarda, Base temel sınıfımızı oluşturmuş ve öylece bırakmıştık. Bu sınıf, bütün tabloların miras alacağı temel sınıf olacak. Declarative bir şekilde sınıf tanımlamaları yapmadan önce, bu temel sınıfı oluşturmamız gerektiği SQLAlchemy dokümantasyonunda yazıyor. Ayrıca kurucu fonksiyonu çağırarak da bir temel declarative sınıf oluşturabiliriz.

    from sqlalchemy.orm import declarative_base
    
    Base = declarative_base()
    
    
    class Parent(Base):
        __tablename__ = "parents"  # Tablonun ismini yazmak, bu isme göre bir tablo oluşmasını sağlar.
        __abstract__ = False  # Değerin `True` olması durumunda bu tablo oluşturulmaz. 
                              # Yani başka tabloların miras alacağı bir model tablo oluşturmak ama bu model tablonun
                              # kendisinin oluşmamasını istiyorsanız, bu niteliği True olarak değiştirin. Ön-tanımlı olarak
                              # bu değer False'dur, yani biz bu değeri True yaptığımızda zaten mevcut olan
                              # bir sınıf niteliğinin değerini değiştirmiş oluruz.
    

    SQLAlchemy'nin 2.0 sürümünde, tip tanımlamalarının PEP484'deki typing önerilerine uyumlu hale geldiğinden bahsetmiştim. Mesela 2.0 sürümünden önce şöyle bir kullanım vardı:

    from sqlalchemy import Column, Integer
    from sqlalchemy.orm import DeclarativeBase
    
    
    class Base(DeclarativeBase):
        pass
    
    
    class Parent(Base):
        __tablename__ = "parents"
        __abstract__ = False
        id = Column(Integer, primary_key=True)
    

    SQLAlchemy'nin 2.0 sürümünden önce, yukardaki gibi bir model oluştururken, modelde yer alacak sütunları oluşturmak için Column sınıfı, sütunun tipini belirtmek için ilgili tip sınıfı, hepsi tek tek programın içine aktarılıyordu. Yeni sürümle birlikte model sütunlarının Mapped ve MappedColumn sınıfları kullanılarak tanımlandığını görüyoruz.

    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
    
    
    class Base(DeclarativeBase):
        pass
    
    
    class Parent(Base):
        __tablename__ = "parents"
        __abstract__ = False
        id: Mapped[int] = mapped_column(primary_key=True)
    

    Gördüğünüz gibi, bu yaklaşımda Python’ın kendi tiplerini, veritabanı sütun tipi olarak yazabiliriz. Tabi, yazılabilecek tipler sadece Python tipleri ile sınırılı değil. Ancak bu başlıkta veri tipleri konusuna girmeyeceğim. Şimdi oluşturmak istediğimiz nesne ilişkisini yani ebeveyn ve çocuk ilişkisini tanımlayalım.

    from sqlalchemy import ForeignKey
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
    
    
    class Base(DeclarativeBase):
        pass
    
    
    class Parent(Base):
        __tablename__ = "parents"
        id: Mapped[int] = mapped_column(primary_key=True)
        children: Mapped[list["Child"]] = relationship(back_populates="parent", cascade="all, delete-orphan")
    
    
    class Child(Base):
        __tablename__ = "children"
        id: Mapped[int] = mapped_column(primary_key=True)
        parent_id: Mapped[int] = mapped_column(ForeignKey("parents.id"))
        parent: Mapped["Parent"] = relationship(back_populates="children")
    

    Veritabanında yer alacak tabloların nasıl bir yapıya sahip olması gerektiğini tanımladık. Şimdi, sıra geldi tablolarımızı oluşturmaya; sonra da bu tablolara kayıtlar ekleyeceğiz.
    SQLALchemy'de tablo oluşturmak için, önce veritabanı motoruna bağlanmak gerekir. Sonra da Base sınıfını kullanarak tablolar oluşturulur.

    import os
    
    from sqlalchemy import ForeignKey, create_engine
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
    
    
    class Base(DeclarativeBase):
        pass
    
    
    class Parent(Base):
        __tablename__ = "parents"
        id: Mapped[int] = mapped_column(primary_key=True)
        children: Mapped[list["Child"]] = relationship(back_populates="parent", cascade="all, delete-orphan")
    
    
    class Child(Base):
        __tablename__ = "children"
        id: Mapped[int] = mapped_column(primary_key=True)
        parent_id: Mapped[int] = mapped_column(ForeignKey("parents.id"))
        parent: Mapped["Parent"] = relationship(back_populates="children")
    
    
    engine = create_engine(os.environ["SQLALCHEMY_DATABASE_URI"])
    Base.metadata.create_all(engine)
    

    Buradaki os.environ["SQLALCHEMY_DATABASE_URI"] ifadesi, benim bilgisayarımda postgresql://username:password@host:port/db_name biçimine benzer bir değer döndürür. Ben bu örnekte PostgreSQL veritabanı sistemini kullanacağım. PostgreSQL'i kullanabilmek için PostgreSQL yazılımına ek olarak psycopg2 kütüphanesinin de bilgisayarımızda kurulu olması gerekir.

    pip3 install psycopg2

    Şimdi, sıra geldi tablolarımıza veri eklemeye. Bunun için Session sınıfından bir tane örnek oluşturup bu örneği kullanarak veritabanı işlemlerimizi yapacağız.

    import os
    
    from sqlalchemy import ForeignKey, create_engine
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
    
    
    class Base(DeclarativeBase):
        pass
    
    
    class Parent(Base):
        __tablename__ = "parents"
        id: Mapped[int] = mapped_column(primary_key=True)
        children: Mapped[list["Child"]] = relationship(back_populates="parent", cascade="all, delete-orphan")
    
    
    class Child(Base):
        __tablename__ = "children"
        id: Mapped[int] = mapped_column(primary_key=True)
        parent_id: Mapped[int] = mapped_column(ForeignKey("parents.id"))
        parent: Mapped["Parent"] = relationship(back_populates="children")
    
    
    engine = create_engine(os.environ["SQLALCHEMY_DATABASE_URI"])
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        parent = Parent()
        session.add(parent)
        session.commit()
        child = Child(parent_id=parent.id)
        session.add(child)
        session.commit()
        parent = Parent()
        session.add(parent)
        session.commit()
    

    Yukardaki kodun, 2 tane ebeveyn sınıfından, 1 tane de çocuk sınıfından toplamda 3 adet nesne oluşturması gerekir. Sorgulama işlemleri için session nesnesinin query fonksiyonunu kullanıyoruz. query fonksiyonuna ise sorgulamanın yapılacağı tablo modelini argüman olarak yazıyoruz.

    import os
    
    from sqlalchemy import ForeignKey, create_engine
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
    
    
    class Base(DeclarativeBase):
        pass
    
    
    class Parent(Base):
        __tablename__ = "parents"
        id: Mapped[int] = mapped_column(primary_key=True)
        children: Mapped[list["Child"]] = relationship(back_populates="parent", cascade="all, delete-orphan")
    
    
    class Child(Base):
        __tablename__ = "children"
        id: Mapped[int] = mapped_column(primary_key=True)
        parent_id: Mapped[int] = mapped_column(ForeignKey("parents.id"))
        parent: Mapped["Parent"] = relationship(back_populates="children")
    
    
    engine = create_engine(os.environ["SQLALCHEMY_DATABASE_URI"])
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        print(session.query(Parent).all())
        print(session.query(Child).all())
    

    Yukardaki kodların aşağıdakine benzer bir çıktı üretmesi lazım:

    [<__main__.Parent object at 0x7feb2ce156c0>, <__main__.Parent object at 0x7feb2ce17d00>]

    [<__main__.Child object at 0x7feb2ce17760>]

    Çocuk nesnesinin ebeveyn id’sini kullanarak ebeveyn sınıfı üzerinde bir sorgu gerçekleştirelim:

    import os
    
    from sqlalchemy import ForeignKey, create_engine
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
    
    
    class Base(DeclarativeBase):
        pass
    
    
    class Parent(Base):
        __tablename__ = "parents"
        id: Mapped[int] = mapped_column(primary_key=True)
        children: Mapped[list["Child"]] = relationship(back_populates="parent", cascade="all, delete-orphan")
    
    
    class Child(Base):
        __tablename__ = "children"
        id: Mapped[int] = mapped_column(primary_key=True)
        parent_id: Mapped[int] = mapped_column(ForeignKey("parents.id"))
        parent: Mapped["Parent"] = relationship(back_populates="children")
    
    
    engine = create_engine(os.environ["SQLALCHEMY_DATABASE_URI"])
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        child = session.query(Child).first()
        print(session.query(Parent).filter_by(id=child.parent_id).first())
    

    Yukardaki kod ile, çocuk nesnesinin ebeveynini gösteren bir çıktı almamız gerekir:

    <__main__.Parent object at 0x7fd072769870>

    Bu arada, parent_id'ye, child.parent.id üzerinden de ulaşabilirdik.

  2. OneToOne

    Bu nesne ilişkisinde, farklı tablolarda yer alan farklı veriler, birbirleriyle yalnızca bir sefer ilişki kurar. Örneğin “normal şartlarda” bir kişinin sadece bir tane kimlik numarası olabilir değil mi? Bu türden bir ilişki OneToOne ilişkisidir. OneToOne nesne ilişkisi modelinde hangi modelin id değerinin ForeignKey olarak kullanılacağı nesnelerin oluşum sıralarıyla ilişkili olabilir. Örneğin, bir kimlik numarasının oluşabilmesi için, önce bir insanın oluşması gerekir. Dolayısıyla ilişki “insan” → “kimlik numarası” şeklinde kurulur. Bahsettiğim örnek için, ForeignKey insan'a ait olacaktır. Yani burada ForeignKey'i kimlik numarasına vermemeliyiz.

    import os
    
    from sqlalchemy import ForeignKey, create_engine
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
    
    
    class Base(DeclarativeBase):
        pass
    
    
    class Person(Base):
        __tablename__ = "people"
        id: Mapped[int] = mapped_column(primary_key=True)
        idno: Mapped["Idno"] = relationship(back_populates="person", cascade="all, delete-orphan")
    
    
    class Idno(Base):
        __tablename__ = "idnos"
        id: Mapped[int] = mapped_column(primary_key=True)
        person_id: Mapped[int] = mapped_column(ForeignKey("people.id"))
        person: Mapped["Person"] = relationship(back_populates="idno")
    
    
    engine = create_engine(os.environ["SQLALCHEMY_DATABASE_URI"])
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        person = Person()
        session.add(person)
        session.commit()
        idno = Idno(person=person)
        session.add(idno)
        session.commit()
    
  3. ManyToMany

    Bu nesne ilişkisinde, bir tabloda yer alan bir veri, başka bir tabloda yer alan birden çok veriyle ilişki kurabilir. Benzer şekilde, ikinci tablodaki veriler de birçok kez ilk tabloyla ilişki kurabilirler. Örneğin, diyelim otel kayıtlarını tuttuğunuz bir veritabanınız var. Ayrıca diyelim ki bu otelleri web sayfalarında yayınlayan turizm acentelerinin kayıtlarını da tutuyorsunuz. Bu örnekte, bir otel birçok sitede gösteriliyor olabilir değil mi? İşte bu türden ilişkilere ManyToMany ilişkisi denir. Peki bu örnekte, PrimaryKey hangi modele verilmelidir? Otel kayıtlarını tutan tablo modeli mi yoksa web sitelerinin kayıtlarını tutan tablo modeli mi önce gelir?

    ManyToMany ilişkisine sahip nesne modellerinde, iki modelin birbiriyle birçok kez ilişki kurabilmesi için üçüncü bir ara model oluşturulur ve her iki model de, bu üçüncü modele id’lerini ForeignKey olarak verirler.

    import os
    
    from sqlalchemy import ForeignKey, create_engine
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
    
    
    class Base(DeclarativeBase):
        pass
    
    
    class Hotel(Base):
        __tablename__ = "hotels"
        id: Mapped[int] = mapped_column(primary_key=True)
        websites: Mapped[list["Association"]] = relationship(back_populates="hotel")
    
    
    class Website(Base):
        __tablename__ = "websites"
        id: Mapped[int] = mapped_column(primary_key=True)
        hotels: Mapped[list["Association"]] = relationship(back_populates="website")
    
    
    class Association(Base):
        __tablename__ = "associations"
        id: Mapped[int] = mapped_column(primary_key=True, nullable=True, autoincrement=True)
        hotel_id: Mapped[int] = mapped_column(ForeignKey("hotels.id"), primary_key=True)
        website_id: Mapped[int] = mapped_column(ForeignKey("websites.id"), primary_key=True)
        hotel: Mapped["Hotel"] = relationship(back_populates="websites")
        website: Mapped["Website"] = relationship(back_populates="hotels")
    
    
    engine = create_engine(os.environ["SQLALCHEMY_DATABASE_URI"])
    
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        hotel1 = Hotel()
        session.add(hotel1)
        session.commit()
    
        hotel2 = Hotel()
        session.add(hotel2)
        session.commit()
    
        website1 = Website()
        session.add(website1)
        session.commit()
    
        website2 = Website()
        session.add(website2)
        session.commit()
    
        a1 = Association(hotel=hotel1, website=website1)
        session.add(a1)
        session.commit()
    
        a2 = Association(hotel=hotel1, website=website2)
        session.add(a2)
        session.commit()
    
        a3 = Association(hotel=hotel2, website=website1)
        session.add(a3)
        session.commit()
    
        a4 = Association(hotel=hotel2, website=website2)
        session.add(a4)
        session.commit()
        
        print(session.query(Hotel).first().websites)
        print(session.query(Website).first().hotels)
    

    Bu kodları çalıştırdığımız zaman şöyle bir çıktı almamız gerekir:

    [<__main__.Association object at 0x7fa179341210>, <__main__.Association object at 0x7fa179341270>]

    [<__main__.Association object at 0x7fa179341930>, <__main__.Association object at 0x7fa179341990>]

Bu üç ilişki modeli ORM’deki temel ilişki modelleridir. Kendi ihtiyacınıza göre, örneğin OneToMany tasarım modelini, ManyToOne olacak şekilde değiştirebilirsiniz. Hatta bir modelin ebeveyninin kendi sınıfından türetilecek bir nesneye ait olmasını da sağlayabilirsiniz. Örneğin, Cevapların tutulacağı bir tablo modeli tasarlamamız gerekiyor olsun. Bildiğiniz gibi, web sitelerindeki cevap nesneleri kendi üzerlerinde de çalışırlar. Yani bir cevabın kendisi de cevaplanabilirdir. Kayıtların bu türden bir ilişkisi varsa, bu ilişki tipine de self-referential veya recursive nesne ilişkisi diyebiliriz.

import os

from sqlalchemy import ForeignKey, create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session


class Base(DeclarativeBase):
    pass


class Reply(Base):
    __tablename__ = "replies"
    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("replies.id"), nullable=True)
    children: Mapped[list["Reply"]] = relationship(back_populates="parent")
    parent: Mapped["Reply"] = relationship(back_populates="children", remote_side=[id])


engine = create_engine(os.environ["SQLALCHEMY_DATABASE_URI"])

Base.metadata.create_all(engine)
with Session(engine) as session:
    r1 = Reply()
    session.add(r1)
    session.commit()

    r2 = Reply()
    r2.parent = r1
    session.add(r2)
    session.commit()

Evet, gördüğünüz gibi, yukardaki örnekte, aynı sınıfıtan üretilen nesnelerden birini diğerinin ebeveyni haline getirdik. Örnekler çoğaltılabilir elbette. SQLAlchemy, oldukça geniş bir kütüphane, ben de henüz keşfetme aşamasındayım. Öğrendiklerimden henüz bahsetmediğim ve başka bir başlığın konusu olmayı hakettiğini düşündüğüm ayrıntılar var. Onları da imkan olursa başka zaman anlatmak üzere. Kendinize çok iyi bakın.

7 Beğeni