Flask/Grinberg/04 - Databáze
Tato stránka je součástí projektu: | |
Příslušnost: všeobecná |
04 - Databáze
[editovat]- https://github.com/miguelgrinberg/microblog/tree/v0.4
- https://github.com/miguelgrinberg/microblog/releases/tag/v0.4
- https://github.com/miguelgrinberg/microblog/compare/v0.3...v0.4
Databases in Flask
[editovat]Databáze není do Flasku přímo integrovaná, což je dobře – můžeme si zde používat, co chceme – zkrátka použijeme vhodnou extensi.
Databázové systémy zhruba je rozdělujeme do dvou skupin:
SQL je častější, tak se zde budeme zaobírat s ní.
Použijeme dvě extense (ta druhá viz níže)
- Flask-SQLAlchemy = wrapper pro SQLAlchemy, což je ORM = Objektově relační mapování
sudo pip3 install flask-sqlalchemy
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app) #inicializace databáze
Database Migrations
[editovat]Extense, která pomáhá řešit migraci databází:
- Flask-Migrate = wrapper pro Alembic = database migration framework pro SQLAlchemy
sudo pip3 install flask-migrate
from flask_migrate import Migrate
migrate = Migrate(app, db)
Flask-SQLAlchemy Configuration
[editovat]Začneme s SQLite:
config.py:
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
# ...
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db') # Kde je databáze? Buď řečeno v environmentu anebo 'app.db' v kořenu naší aplikace
SQLALCHEMY_TRACK_MODIFICATIONS = False # zakážeme signál při každé změně databáze
Na začátku je zapotřebí tu databázi inicializovat – takže nejlépe to vše najednou učiníme v našem souboru app/__init__.py, který teď bude vypadat:
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app) # objekt reprezentující databázi
migrate = Migrate(app, db) # objekt reprezentující migrační stroj (migration engine)
from app import routes, models # importuji také modul "models", který bude definovat strukturu databáze
Database Models
[editovat]Struktura databáze (schema)
databázový model = kolekce tříd
objekty těchto tříd ⟹ řádky tabulek
Použijeme WWW SQL Designer tool
Tabulka:
id | INTEGER | primary_key |
username | VARCHAR (64) | unique |
VARCHAR (120) | unique | |
password_hash | VARCHAR (128) |
Takovýto návrh přepíšeme do souboru app/models.py:
from app import db # app dostala db při inicializaci __init__.py
class User(db.Model): # třída User je inheritována ze základní třídy db.Model
id = db.Column(db.Integer, primary_key=True) # sloupce jsou instance třídy db.Column
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
def __repr__(self): # metoda __repr__ určuje, jak se mají objekty tisknout:
return '<User {}>'.format(self.username)
Creating The Migration Repository
[editovat]Během vývoje aplikace se struktura databáze může měnit. Migrační framework Alembic (chladič, vytvářející křivuli – v alchymii) umožňuje provádět takové změny databázových schemat, aniž by se musela znovu vytvářet celá databáze.
Alembic udržuje svůj migrační repozitář, což je adresář, do kterého se ukládají migrační skripty.
Po každé změně schematu se sem přidává další skript.
Příkazy skriptu jsou příkazy flasku.
flask-migrate nám přidá příkaz flask db
Od dřívějška (Chapter 1) bychom měli mít nastavenu proměnnou prostředí:
export FLASK_APP naše_aplikace.py
a pak můžeme z příkazové řádky shellu zadat příkaz:
flask db init
což nám vytvoří adresář migration:
- /versions
- script.py.mako
- env.py
- README
- alembic.ini
The First Database Migration
[editovat]První databázová migrace, zahrnující mapování tabulky Users do databázového modelu User
– možnosti:
- manuálně
- automaticky: Alembic porovná schema definované v modelu s aktuální databází a spustí migrační skript
Dosud jsme žádnou předchozí databázi neměli, takže z příkazového řádku spustíme (argument -m přidá komentář):
flask db migrate -m "users table"
INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'user' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']' Generating /.../04a-Database/migrations/versions/c7329ba56057_users_table.py ... done
A vidíme malý zázrak. Krátce zrekapitulujeme:
- V souboru /app/models.py jsme si definovali náš databázový model
- Alembic nám vytvořil soubor
/migrations/versions/c7329ba56057_users_table.py
, což je pythonovský skript, obsahující definice dvou funkcí, jejichž význam je jasný:- def upgrade():
- def downgrade():
- Alembic nám vytvořil SQLite databázový soubor
/app.db
Předpokládám, že máme na linuxu nainstalováno:
sudo apt-get install sqlite3
sudo apt-get install sqlitebrowser
Podíváme se na tu databázi /app.db
grafickým prohlížečem databáze:
sqlitebrowser app.db &
V případě, že není sqlitebrowser nainstalovaný, spustíme:
sqlite3 app.db
a pak můžeme spouštět příkazy jako:
sqlite> .help sqlite> .show sqlite> .databases sqlite> .tables sqlite> .fullschema sqlite> select * from user;
Vidíme ale, že tam zatím máme jen jednu prázdnou tabulku alembic_version
–
protože samotný příkaz
Abychom v té databázi vytvořili naše schema, musíme spustit příkaz:
flask db upgrade
V Browseru pro SQLite si můžu ověřit, že databáze byla aktualizována.
Database Upgrade and Downgrade Workflow
[editovat]Database Relationships
[editovat]Uděláme si další tabulku:
id | INTEGER | primary_key |
body | VARCHAR (140) | |
timestamp | DATETIME | |
user_id | INTEGER | foreign key |
Tato relace se nazývá one-to-many, protože jeden uživatel může napsat více zpráv.
Druhou tabulku vytvoříme v souboru app/models.py podobně, jako tu první:
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) # argument 'default' dostane samotnou funkci, nikoli funkční hodnotu
user_id = db.Column(db.Integer, db.ForeignKey('user.id')) # tento klíč je 'id' z tabulky 'user'
def __repr__(self):
return '<Post {}>'.format(self.body)
Do první tabulky user
ještě doplníme řádek, popisující nové pole:
posts = db.relationship('Post', backref='author', lazy='dynamic')
a na začátek souboru nezapomenu doplnit:
from datetime import datetime
Teď provedu novou migraci databáze a po ní upgrade:
flask db migrate -m "posts table"
flask db upgrade
Play Time
[editovat]Hrajeme si s databází v příkazové řádce pythonu.
>>> from app import db
>>> from app.models import User, Post
A dál si zkoušíme různé další příkazy:
u = User(username='john', email='john@example.com')
db.session.add(u)
db.session.commit()
users = User.query.all()
users
for u in users:
print(u.id, u.username)
u = User.query.get(1)
... atd.
users = User.query.all()
for u in users:
db.session.delete(u)
db.session.commit()
Shell Context
[editovat]Nemusím z příkazové řádky shellu volat python3
, ale můžu rovnou zavolat:
flask shell
a pak nemusím znovu importovat db
a ty další věci.
Vytvoříme si ještě shell context, který mi pre-importuje i další věci – instanci databáze a modely. Přidáme to do hlavního souboru microblog.py, takže bude vypadat:
from app import app, db
from app.models import User, Post
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post': Post}
Dekorátor app.shell_context_processor
teď registruje tuhle funkci jakožto kontextovou funkci flaskového shellu:
Jakmíle ze shellu operačního systému spustíme příkaz flask shell
, tak invokuje tuto funkci, která vrací slovník (dictionary).
Takže teď spustíme
flask shell
a můžeme klást takové dotazy, jako:
db
User
Post
V případě, že bychom dostali chybové výjimky, znamená to, že ta funkce make_shell_context()
nebyla Flaskem registrována.
Nejčastější chybou je, že v systémovém shellu nemáme exportovanou proměnnou FLASK_APP
(jak jsme popisovali v kapitole Flask/Grinberg/01 - Nazdárek!#A "Hello, World" Flask Application):
export FLASK_APP=mojeaplikace.py