Lekce 1 - SQLAlchemy - Úvod a instalace
Dnes se budeme zabývat temným středověkem a vařit různé elixíry v malé alchymistické dílně pomocí novodobých surovin. Tedy uvedu tato slova na pravou míru. Zavedeme jistou abstrakci nad SQL kódem, která mírně usnadní další život programátora (nikoli chemika) a budeme pracovat s databázovými dotazy jako s běžným datovým typem v Pythonu.
Předpokladem k tomuto článku je znalost Pythonu, objektového programování v Pythonu (nebo všeobecná znalost) a k tomu SQL databáze. Alespoň nějaké.
Jistě jste již slyšeli o ORM
(Object-Relational Mapping)
jako určité programové vrstvě mezi relační databází a objektovými typy.
Existuje nesčetné množství knihoven, jež tento problém řeší. Jednou z
nich je například Django.ORM
nebo Tortoise
ORM, ovšem pro tuto chvíli jsem zvolil SQLAlchemy a to hned z několika
důvodů:
- dlouhá historie (2006),
- komplexnost,
- poměrně snadné,
- používané v mnoha aplikacích,
- velmi dobrá rychlost,
- bývá užívána ve frameworcích pro web (např. Flask), nikoli součástí
Instalace
Jak již je v Pythonu obvyklé, nejsnadněji knihovnu sqlalchemy
přidáme do systému pomocí příkazu (obvyklá a notoricky známá akce):
pip3 install sqlalchemy
Instalaci můžeme provést i přes vývojové prostředí (klasická instalace balíčku). Např. v PyCharm: File => Settings... => Project: JmenoVasehoProjektu => Python Interpreter => + => zadání názvu balíčku do vyhledávací řádky - v tomto případně: SQLAlchemy => vybrání balíčku z nabídky => Install Package
První kroky
Vzhledem k tomu, že Alchemy (prostě budu pojmenování zkracovat) je velmi rozsáhlá a popsat komplexně její schopnosti by bylo v rozsahu set stránkové knihy, podíváme se jen na část. Hlavní cíl snažení je použitelnost v různých aplikacích.
Začneme s jednou třídou, jinak vzato, s jednou tabulkou v databázi a postupně budeme pokračovat a rozšiřovat do komplexnější struktury dat.
Prvotní úkol je připojit se k databázi nebo případně ji stvořit. Tedy založit databázový stroj, který se postará o práci s daty na nízké úrovni a nám bude jedno, který typ databáze se používá. Přesto se uchyluji k jednoduchosti a volím SQLite a její souborovou reprezentaci:
from sqlalchemy import create_engine db = create_engine("sqlite:///database.db", echo=True) # Varianty pro jiné systémy. Podobně lze použít například: Oracle, MS SQL atd... db = create_engine('postgresql://uživatel:heslo@localhost:5432/mojedata') db = create_engine('mysql://uživatel:heslo@localhost/databáze')
Tedy funkce create_engine()
vrátí instanci Engine
,
jež lze dále přizpůsobovat dle dialektu databáze. Dále přes
DBAPI
se spojí se samotným databázovým strojem. Tohle je však
trochu "vyšší dívčí" a možná se jí budeme věnovat mnohem později,
pokud bude zájem.
Za zmínku stojí parametr echo
, který umožní zobrazení
všech výstupů Alchemy včetně prováděných dotazů, velmi vhodné
pro další vyladění.
Připojení k databázi v Alchemy je tzv. líné, tedy k žádnému
fyzickému spojení nedojde až do okamžiku nutnosti. Pro tento případ je
volání metod z Engine
jako jsou connect()
,
execute()
atp. Nutno však upozornit, že prakticky se tyto metody
nepoužívají přímo, nýbrž se toto odehrává za scénou.
Deklarativní mapování
Již samotná deklarace dat může proběhnout mnoha způsoby a zde si
představíme snadný přístup na základě tříd. Budeme však potřebovat
nějakou základní modelovou třídu jež umožní pracovat s daty. Tu
Alchemy nabízí jako declarative_base
, z níž
vytvoříme prvotní objekt:
from sqlalchemy.orm import declarative_base Base = declarative_base(db)
Metoda declarative_base()
je ovšem pouze jistá zkratka
odkazující na objekt registry
. Stejně by to šlo zapsat třeba
tímto způsobem:
from sqlalchemy.orm import registry mapper_registry = registry() Base = mapper_registry.generate_base()
Na druhou stranu proč si ztěžovat práci a navíc se odchylovat od tříd používáním jen nových typů. Pouze proto, že to jde?
Od třídy Base
budeme dále odvozovat datové struktury.
Abychom si předvedli něco použitelného, budeme pracovat s daty reálného
programu pro skladovou evidenci (tedy velmi zjednodušenou verzi):
Každá tabulka bude mít vlastní třídu, kde budou definovány jednotlivé sloupce s jejich typem a jednu funkci, které bude vracet textovou reprezentaci dat.
Datové typy SQLAlchemy
Držíme se konceptu Pythonu. Vše je objekt, tedy i Alchymie se
drží této receptury a používá vlastní datové typy, jimiž se odkazuje na
nativní databáze. Pro základ bych mohl vyjmenovat datový typ
Integer
, Float
, String
,
Datetime
, Boolean
... a spoustu
dalších. Pozitivní je, že jsou si s Pythonem spřízněné svým
chováním a toto přenáší i do databázových strojů bez ohledu na
druh.
Deklarace bez relací
Pro nás bude důležitý objekt typu Column
, který
představuje jeden sloupek v tabulce (databázové) a může mít i další
atributy, jako jsou klíče, indexy a podobně. Vlastně každý sloupek, který
má být uložený bude tohoto typu:
from sqlalchemy import Column, Integer, Float class Vat(Base): __tablename__ = "vat" # Pojmenování tabulky v databázi vat_id = Column(Integer, primary_key=True, autoincrement=False) # ID položky rate = Column(Float, nullable=False) # Sazba v procentech def __repr__(self): """Textová reprezentace řádku z tabulky""" return "<DPH : id={self.vat_id}; sazba={self.rate}>".format(self=self)
Nutností je definovat název tabulky __tablename__ = "tabulka"
.
Pojmenování třídy neřeší uložení, tenhle problém spadá pod
metadata
a jistou interní chemii na bázi rtuti a olova. Přesto
si pojďme trochu rozebrat programovou ukázku.
Proměnná vat_id
je normální ID položky, který má být
celočíselný. Navíc je to primární klíč (primary_key=True
).
Alchemy vyžaduje alespoň jeden atribut definovaný s primárním
klíčem.
Též obvykle u těchto dat požadujeme zvýšení hodnoty při novém
záznamu (autoincrement=True
). Zde ovšem budou pouze 3 položky a
budou pevné. Bez daně, nižší sazba (10 %) a vyšší sazba (21 %). Pokud se
státní legislativa nějak nezmění, tyto položky jsou relativně stálé.
rate
je sloupek s reálným číslem typu Float
, zde
je prostě jen procentuální hodnota. Víc není co řešit.
Parametr nullable
(jak již pojmenování naznačuje) nám
říká, zda atribut může nabývat hodnoty NULL
či nikoli.
Metoda __repr__(self)
jen vrátí nějaký text, který obsahuje
informace o datech v jednom řádku tabulky. Používám stručnou verzi
formátovaných argumentů. Myslím, že vy, ostřílení Pythonýrové, byste
našli i jiné možnosti, jak udělat hezký textový výstup 😃 Tato metoda
však není povinná a klidně se bez ní obejdete - je použita jen pro
pochopitelnější výstupy v rámci kurzu.
Několik poznámek k typům
Malý problém může nastat při použití dalších druhů databází. Například Oracle vyžaduje sekvenční identifikátor. To lze řešit kódem:
vat_id = Column(Integer, Sequence("vat_id_seq"), ...
Dále může potíže způsobit deklarace položky typu String
,
například:
note = Column(String)
se vygeneruje jako note VARCHAR
, což třeba SQLite nebo PostgreSQL bude akceptovat. Ovšem MySQL by se to nelíbilo, neboť vyžaduje
explicitně zadanou délku řetězce. Je tedy lepší použít preventivně:
note = Column(String(100))
Další možnosti
Data lze deklarovat třeba imperativním způsobem. Na rozdíl od klasického
postupu, kde jsou "metadata" vytvářena odděleně, při použití Table
se stávají součástí třídy.
Tento postup však nepatří mezi doporučované, proto od něj ustoupím a výhradně se budu věnovat pouze výše uvedenému stylu:
from sqlalchemy import Table, Column, Integer, String, ForeignKey from sqlalchemy.orm import registry mapper_registry = registry() vat_table = Table( 'vat', # název tabulky mapper_registry.metadata, # Zaregistrujeme metadata a definujeme sloupky tabulky Column('vat_id', Integer, primary_key=True, autoincrement=False), Column('rate', Float), ) # Prázdná třída postačí class Vat: pass # Namapujeme tabulku na třídu mapper_registry.map_imperatively(Vat, vat_table)
Abych vás nepřehltil informacemi, dokončíme si naše deklarativní mapování v příští lekci 🙂
V příští lekci, SQLAlchemy - Session a základní práce s daty, si probereme Session a základní práci s daty.
Měl jsi s čímkoli problém? Zdrojový kód vzorové aplikace je ke stažení každých pár lekcí. Zatím pokračuj dál, a pak si svou aplikaci porovnej se vzorem a snadno oprav.