Flask/Grinberg/04 - Databáze

Jak používat klasifikační nálepkuTato stránka je součástí projektu:
Příslušnost: všeobecná


04 - Databáze

editovat

Chapter 4: Database

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)

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í:

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:

user
id INTEGER primary_key
username VARCHAR (64) unique
email 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:

  1. V souboru /app/models.py jsme si definovali náš databázový model
  2. 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ý:
    1. def upgrade():
    2. def downgrade():
  3. 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:

posts
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