Home » Beitrag verschlagwortet mit 'DB'

Schlagwort-Archive: DB

Flask DB Migrations

Links

Flask-Migrate ist die Library um DB-Modell-Änderungen, die mit SQLAlchemyausgedrückt sind zu kontrollieren. Es benutzt dazu die Library Alembic.

Schritte

Aufsetzen Flask Migrate Tool

Steps:

pip3 install Flask-Migrate

Projekt bezüglich Migrations-Handling aufsetzen

flask db init

Das setzt verschiedene Migrationscripts-Folders und Tools im SW-Projekt bereit.
Output:

C:\tmp\Python_Workspace1\task-app>flask db init
C:\Users\Lenovo W540\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0\LocalCache\local-packages\Python37\site-packages\flask_sqlalchemy\__init__.py:873: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
Creating directory C:\tmp\Python_Workspace1\task-app\migrations ...  done
Creating directory C:\tmp\Python_Workspace1\task-app\migrations\versions ...  done
Generating C:\tmp\Python_Workspace1\task-app\migrations\alembic.ini ...  done
Generating C:\tmp\Python_Workspace1\task-app\migrations\env.py ...  done
Generating C:\tmp\Python_Workspace1\task-app\migrations\README ...  done
Generating C:\tmp\Python_Workspace1\task-app\migrations\script.py.mako ...  done
Please edit configuration/connection/logging settings in 'C:\\tmp\\Python_Workspace1\\task-app\\migrations\\alembic.ini' before proceeding.

Funktioniert das Kommando nicht, weil die DB noch nicht gestartet ist, dann etwa folgendes absetzen:

pg_ctl -D "C:\Program Files\PostgreSQL\13\data" start

DB-Migrationen durchführen

flask db migrate

Erkennt die Anforderungen an das DB-Modell aus den Python-Scripts (aus den SQLAlchemy Model-Objekt-Definitionen) heraus und erstellt eine Migration (DDL Statements um das erforderliche DB-Model zu erstellen).

Bsp. einer SQLAlchemy Model-Objekt-Definition im *.py:

class Todo(db.Model):
    __tablename__ = 'todos'
    id = db.Column(db.Integer, primary_key=True)
    description = db.Column(db.String(), nullable=False)

Falls das erzeugte Script das ganze DB-Modell von Grund auf neu erzeugen soll, dann darf die DB diese Entitäten nicht schon enthalten. Das kann man zum Bsp. erreichen durch:

dropdb --username=postgres todoDB
createdb --username=postgres todoDB

!Stelle sicher, dass das Pyton-Script, nicht schon selbst die DB-Objekte erstellt (Z.B. Kommando db.create_all() darf nicht auch schon im Python-Code enthalten sein).

Durchführung:

C:\tmp\Python_Workspace1\task-app>flask db migrate

INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'todos'
Generating C:\tmp\Python_Workspace1\task-app\migrations\versions\70ee48c313e4_.py ...  done

C:\tmp\Python_Workspace1\task-app>

Ein Migrations-File wurde erstellt: task-app/migrations/versions/70ee48c313e4_.py:

"""empty message

Revision ID: 70ee48c313e4
Revises: 
Create Date: 2021-07-22 15:15:43.733091

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '70ee48c313e4'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('todos',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('description', sa.String(), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('todos')
    # ### end Alembic commands ###

Migration auf die DB spielen:

C:\tmp\Python_Workspace1\task-app>flask db upgrade
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 70ee48c313e4, empty message

Migration rückgängig machen wäre:

flask db downgrade

Prüfung, ob DB-Migrationen auf DB angekommen sind:

C:\tmp\Python_Workspace1\task-app>psql --username=postgres todoDB
Passwort für Benutzer postgres:
todoDB=# \dt
              Liste der Relationen
 Schema |      Name       |   Typ   | Eigent³mer
--------+-----------------+---------+------------
 public | alembic_version | Tabelle | postgres
 public | todos           | Tabelle | postgres
(2 Zeilen)


todoDB=#

Beachte die alembic_version Tabelle! Beinhaltet die Migrationsinfo. Prinzipiell nicht durch den Programmierer zu verändern.

Wenn nun Änderungen am DB-Modell innerhalb des Python-Scripts gemacht werden (z.B. wird hier das boolean Feld ‚completed‘ neu hinzu gefügt),

class Todo(db.Model):
    __tablename__ = 'todos'
    id = db.Column(db.Integer, primary_key=True)
    description = db.Column(db.String(), nullable=False)
    completed = db.Column(db.Boolean(), nullable=False, default=False)

…, dann wird mit flask db migrate ein neues Update-Script erstellt und mit flask db update dieser Update auf die DB gespielt.

Beheben der Problematik des Hinzufügens von NOT-NULL Feldern:
Das Migrations-Script folgendermassen anpassen:

from alembic import op

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('todos', sa.Column('completed', sa.Boolean(), nullable=True))
    op.execute('update todos set completed=False where completed is null')
    op.alter_column('todos', 'completed', nullable=False)
    # ### end Alembic commands ###

DB Manipulation with SQLAlchemy

Referenzen:
Docs for the SQLAlchemy Query API
SQLAlchemy Doku

Im folgenden eine Kleine Demo,
– wie bestehende SQLAlchemy-Definitionen eines bestehendes Python-Scripts in die interaktive Python-Konsole importier werden können und
– wie SQLAlchemy-Entitäten SQLAlchemy-artig von der abgefragt und hinzu gefügt werden.

AblaufCommandMeaning
1>pythonStart Python Console
2>from path/flask_postgres_sql_hello_app import db,PersonImportiert die in flask_postgres_sql_hello_app.py
(–> siehe) gemachten Definitionen von db und Person
3ffresults = Person.query.all()Holt alle Person Entitäten von der entsprechenden Tabelle
3ffresults = Person.query.first()
3ffPerson.query.filter_by(name=’Hans‘).all()

#Alternativ:
Person.query.filter(Person.name=’Hans‘).all()

#Alternativ:
db.session.query(Person).filter(Person.name=’Hans‘).all()
Holt Person Entitäten die dem Filter entsprechen von der entsprechenden Tabelle
User.query.filter(User.name.like(‚B%‘)).all()

ilike(‚b%‘)
Like-Query

Like (Case-insensitive)
3ffPerson.query.filter(Person.name==‚Hans‘).filter(Person.name==‚Peter‘).all()Multiple Filters (Method chaining)
3ffquery.count()
query.first()
query.get(9)
query.get(‚ID1‘)
query.limit(2)
query.delete()

query.order_by(db.desc(Person.name))
Wird für Grouping benutzt
Nur erstes Finding bringen
Nur 9tes Finding bringen
Primary-Key filter
Nur die ersten 2
Löschen des Query-Resultats

Order by
Person.query.join(‚vehicles‘).filter(Vehicle.marke==‚Opel‘).all()Join-Beispiel
3ffperson = Person(name=’Bob‘)
db.session.add(person)
db.sesssion.commit()
Erstellen eines Person-Objekts und speichern desselben.
3ffdb.session.add_all([Person(name=’Ada‘), Person(‚Jira‘)])
db.sesssion.commit()
Erstellen mehrerer Person-Objekt und speichern desselben
3ff
3ff
3ff

Erste Flask Python Webapp mit DB zugriff über sqlAlchemy

Referenzen

Overall Example

Python HTTP-Server (Flask) Applikation, die vom Browser aufgerufen mit ‚Hello <name>‘ antwortet, wobei der Name von der Personen-Tabelle der DB gelesen wird.
Flask wird dabei benutzt um den HTTP Server zur Verfügung zu stellen und darin die Web-App auf definierter URL anzubieten.
SQLAlchemy (in der Flask-Version) bietet die APIs um DB Entitäten als Klassen (z.B. ‚Person‘) formulieren zu können diese als Objekte auf die relationale DB zu mappen (ORM provider).

flask-postgres-sql-hello-app.py:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__) #Create a flask app with the name of this runnable python file (stored in the env var __name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://myDB:pw@localhost:5432/testDB'
db = SQLAlchemy(app)

class Person(db.Model):
  __tablename__ = 'persons'
  id = db.Column(db.Integer, primary_key=True)
  name = db.Column(db.String(), nullable=False)

#create all defined entities:
db.create_all()

#Insert an object to the DB:
person1 = Person(name = 'Marc')
db.session.add(person1)
db.session.commit()

person = Person.query.first()

#Handle web request on the web root:
@app.route('/')
def index():
    return 'Hello ' + person.name

# Wird dieser Code auskommentiert, dann kann diese HTTP-Server-App 
# durch 'python dieses-file-py' gestartet werden.
#if __name__ == '__main__':
#   app.run()

Define an Entity

class Person(db.Model):
  __tablename__ = 'persons'
  id = db.Column(db.Integer, primary_key=True)
  name = db.Column(db.String(), unique=False, nullable=False)
  ahv = db.Column(db.String(), unique=True, nullable=False)

How to run a Flask HTTP Server

Das obige HTTP-Server-Script flask-postgres-sql-hello-app.py wird gestartet mit:

> flask run

nachdem die Umgebungsvariablen FLASK_APP=pfad/flask-postgres-sql-hello-app.py gesetzt ist (Windows).
In Linux wird anscheinend so aufgerufen: >FLASK_APP=pfad/flask-postgres-sql-hello-app.py flask run

Debug-Mode: (Autorerun des HTTP-Server-Scripts)
Env. Variable setzen: FLASK_DEBUG=true

Alternative Start-Methode:
Sieh code auch oben:

# Wird dieser Code auskommentiert, dann kann diese HTTP-Server-App 
# durch 'python dieses-file-py' gestartet werden.
#if __name__ == '__main__':
#   app.run()

Soll die Server-App von aussen Aufrufbar sein:

>flask run --host=0.0.0.0

oder

if __name__ == '__main__':
   app.run(host="0.0.0.0")

Use Python Interactive Console to Import script and query script defined Entities

C:\tmp\Python_Workspace1\src>python

>>> from flask_postgres_sql_hello_app import db,Person
>>> Person.query.all()
[<Person ID: 5, name: Marc>
, <Person ID: 9, name: Marc>
, <Person ID: 10, name: Marc>

Funktioniert nur mit PY-Files deren Namen nach Konvention ist (kein ‚-‚).

Object Lifecycle in SQLAlchemy

The Lifecycle is: –object instantiated–> transient— query add executed–> pending –qery select/filter OR commit exec–> flushed –commit ex.–> committed

Flushed heisst: Im internen (in Memory) ORM-Object-Model-Cache sind die Änderungen bereits geschrieben.

!! ORM Selects (z.B. Person.query.all()) erhält bereits alle Objekte, die oberhalb im Code mit session.query.add(person) hinzugefügt/verändert/gelöscht wurden. –> ORM select-artige Queries löschen Flush aus!
Der SQL Select (direkt auf der DB) zeigt diese aber erst nach einem Commit!

Criteria API: Join mit Filter auf beiden joined Tables

Der Knackpunkt is dieser:

Mit

Root<TabelleA> root = query.from(TabelleA.class)

wird die Ausgangstabelle (root) bezeichnet.

Mit

Join<TabelleA, TablleB> join = root.join(TabelleA.bRef);

wir HealthCheck hinzu ge-joined.

Wichig Referenzen (z.B. der Filter auf das Feld feldXY) müssen nun von join aus gehen:

join.get(TablleB.feldXY)

Oder die ganze Where-Clause:

.where(criteriaBuilder.equal(root.get(TabelleA.id), "id000001"),
 criteriaBuilder.like( join.get(TablleB.feldXY), "%gesuchterInhalt%"))

 

Ganze Beispiel

CriteriaBuilder criteriaBuilder = ippDaoProvider.getEntityManager().getCriteriaBuilder();
CriteriaQuery<TabelleA> query = criteriaBuilder.createQuery(TabelleA.class);
Root<TabelleA> root = query.from(TabelleA.class);
Join<TabelleA, TablleB> join = root.join(TabelleA.bRef);

String healthCheckType = "BelowInvest";
query.select(root).where(criteriaBuilder.equal(root.get(TabelleA.id), "id000001"),
        criteriaBuilder.like( join.get(TablleB.feldXY), "%gesuchterInhalt%"));

List<TabelleA> aList =  ippDaoProvider.getHealthCheckExecutionDao().find(query);

 

 

Criteria API: Select where in / In list where clause

CriteriaBuilder cb = daoProvider.getEntityManager().getCriteriaBuilder();
CriteriaQuery<Employee2> criteriaQuery = cb.createQuery(Employee2.class);

Root<Employee2> employee2Root = criteriaQuery.from(Employee2.class);
criteriaQuery.select(employee2Root).where(employee2Root.get(Employee2._ATTR_gpn).in(gpns));
TypedQuery<Employee2> query = daoProvider.getEntityManager().createQuery(criteriaQuery);

List<Employee2> employees = query.getResultList();