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.
-
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. BirazdanOneToMany
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ğiSQLAlchemy
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ınPEP484
'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çinColumn
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ınMapped
veMappedColumn
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 daBase
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ımdapostgresql://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çinPostgreSQL
yazılımına ek olarakpsycopg2
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
nesnesininquery
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. -
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 modelinid
değerininForeignKey
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 buradaForeignKey
'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()
-
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’leriniForeignKey
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.