Verbindung zur PostgreSQL Datenbank

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Hallo Leute,

ich habe gerade angefangen ein Backend mit FastAPI und PostgreSQL zu programmieren. Ich habe dafür schon eine PostgreSQL Datenbank angelegt und unter anderem die Tabelle 'users' angelegt.
Außer den Datentypen stimmen die Spalten dieser Tabelle genau mit den Attributen der Klasse 'User' überein. (Nicht wundern, dass ich die Klasse nicht benutze, diese soll für zukünftige INSERT befehle die Datentypvalidierung übernehmen).

Okay soviel zum Hintergrund. Nun meine eigentlichen Fragen:
A) Soll ich mich wie in "get_all_users" immer wieder mit meiner Datenbank mit 'psycopg2.connect(**config)' verbinden oder reicht es dies einmal ganz am Anfang des Programms zu machen? Denn ansonsten müsste ja mein Programm sich bei jeder API-Anfrage erneut mit der Datenbank verbinden... Ich habe keine Ahnung ob das so vorgesehen ist.. Vielen Dank schonmal für eure Ratschläge ;)

B) Um das Python-Modul 'psycopg2' verwenden zu können habe ich mich vor allem auf Code von dieser Webseite gestützt:
https://www.postgresqltutorial.com/postgresql-python/

Sind die Beispiele von dieser Webseite gut oder sollte ich doch lieber auf die offizielle Dokumentation von psycopg hören?
(https://www.psycopg.org/docs/)


Hier mein Code:
database.ini

Code: Alles auswählen

[postgresql]
host=localhost
database=jubeltachDB
user=postgres
password=geheim
(password wurde hier natürlich durch "geheim" ersetzt)


main.py:

Code: Alles auswählen

from configparser import ConfigParser

from fastapi import FastAPI
from fastapi.param_functions import Depends
from pydantic import BaseModel
import psycopg2


class User(BaseModel):
    user_id: int
    name: str
    password: str
    accept_invitation: bool
    vote_qty: int


app = FastAPI()


def config(filename='database.ini', section='postgresql'):
    parser = ConfigParser()
    parser.read(filename)

    db = {}
    if parser.has_section(section):
        params = parser.items(section)
        for param in params:
            db[param[0]] = param[1]
    else:
        raise Exception(f'Section {section} not found in the {filename} file')

    return db


@app.get("/users/")
async def get_all_users(config: dict = Depends(config)):
    conn = psycopg2.connect(**config)
    cur = conn.cursor()
    cur.execute('SELECT * FROM users ORDER BY user_id;')
    users = cur.fetchall()
    cur.close()
    conn.close()

    return users
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

Benutze eine Bibliothek wie SQLAlchemy, das hat das Sessionhandling bereits eingebaut.
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

SQLAlchemy wäre eigentlich meine erste Wahl gewesen. Ich wollte es nur nicht nehmen, weil ich SQL Befehle üben möchte. Ich stelle gerade fest das ich mit SQAlchemy Core auch SQL Befehle nutzen kann. Ich muss anscheinend den ORM garnicht benutzen wenn ich das hier richtig verstehe: https://docs.sqlalchemy.org/en/14/intro.html
Benutzeravatar
__blackjack__
User
Beiträge: 13103
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wobei man da auch unterhalb des ORM nicht direkt SQL als Zeichenketten zusammenbastelt, sondern die entsprechenden SQLAlchemy-Objekte verwendet.

Code: Alles auswählen

    cur.execute("SELECT * FROM user ORDER BY id")
    users = cur.fetchall()

    # =>

    users = engine.execute(
        user_table.select().order_by(user_table.c.id)
    ).fetchall()
Mit einer entsprechenden Definition von `user_table` und `engine`.

Ohne eine konkrete Engine an die Metadaten gebunden zu haben, führt das zu folgendem SQL:

Code: Alles auswählen

In [231]: print(user_table.select().order_by(user_table.c.id))                  
SELECT "user".id, "user".name, "user".password, "user".accepted_invitation, "user".vote_quantity 
FROM "user" ORDER BY "user".id
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

Domroon hat geschrieben: Mittwoch 15. Dezember 2021, 14:03 SQLAlchemy wäre eigentlich meine erste Wahl gewesen. Ich wollte es nur nicht nehmen, weil ich SQL Befehle üben möchte.
Wenn es darum geht SQL zu üben, würde ich pgAdmin nutzen. Da kannst du in einer grafischen Benutzeroberfläche gut überblicken was deine Query bewirkt hat.

Oder:
Du kannst die Query aus geben lassen die SQLAlchemy ORM tatsächlich verwendet hat um deine gewünschte Transaction im code auszuführen.
create_engine('sqlite:///:memory:', echo=True)

Siehe:
https://docs.sqlalchemy.org/en/14/orm/tutorial.html

Ich finde das ist lehrreicher als die Query von Hand zu programmieren und dabei womöglich immer noch Fehler zu machen.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Fehler kann man auch machen wenn man SQL nicht direkt verbindet und so schwer ist SQL auch nicht. SQL soll ja auch so leicht sein dass man nicht Software Entwickler sein muss um es zu nutzen und dass funktioniert auch in der Praxis.

Es ist auch definitiv sinnvoll SQL wirklich zu können den spätestens wenn es zu Problemen kommt, sollte man verstehen was tatsächlich passiert und dafür muss man SQL und Postgres/MySQL/... wirklich kennen und nicht nur die Abstraktionsschichten ein paar Ebenen höher.

Einfaches Connection Pooling hat psycopg2 übrigens auch: https://www.psycopg.org/docs/pool.html.

Im "Ernstfall" sollte man natürlich sqlalchemy (oder django) nutzen, es kann aber auch da Sinn machen mal am ORM vorbei zu gehen z.B. um COPY zu nutzen oder Funktionen die recordsets produzieren (jsonb_to_recordset z.b.). Das muss man bei SQLAlchemy zwar selten weil es sehr mächtig ist und sich auch noch recht komplexe Queries halbwegs leicht mit SQLAlchemy ausdrücken lassen, dass ist aber nicht bei jedem ORM so. Django z.B. hat massive Probleme bei nicht-trivialen Queries und da muss man schon recht häufig SQL nutzen.

Grundsätzlich solltest du aber wissen dass es unsinnig ist fastapi und psycopg2 zusammen zu nutzen. Wenn du unbedingt fastapi nutzen möchtest, solltest du asyncpg oder aiopg nutzen. Solltest du sqlalchemy mit fastapi nutzen wollen, dann solltest du auch alle bisherigen Beispiele in diesem Thread ignorieren und dich mit diesem Teil der SQLAlchemy Dokumentation beschäftigen. Hängst du nicht sonderlich an fastapi, würde ich statt dessen flask nehmen.
einfachTobi
User
Beiträge: 491
Registriert: Mittwoch 13. November 2019, 08:38

FastAPI selbst hat dazu eine übersichtliche und gute Anleitung, welche auch auf SQLAlchemy zurückgreift: https://fastapi.tiangolo.com/tutorial/sql-databases/
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Erstmal vielen dank für eure Tipps. Bevor die alle kamen habe ich folgenden Code zusammengestrickt. Nun habe ich das Gefühl es mir zu einfach gemacht zu haben :lol:
Wie es es aussieht komme ich wohl um asynchrone Programmierung nicht drum herum. Ich dachte ich kann es noch ein wenig aufschieben, da ich AsyncIO so garnicht verstehen will :roll:
(Ich weiß der f-String ist fürchterlich, da will ich mir auf jedenfall was anderes einfallen lassen)

Code: Alles auswählen

from configparser import ConfigParser
from typing import List

import psycopg2
from pydantic.fields import Field
from sqlalchemy import create_engine
from fastapi import FastAPI
from pydantic import BaseModel


def config(filename='database.ini', section='postgresql'):
    parser = ConfigParser()
    parser.read(filename)

    db = {}
    if parser.has_section(section):
        params = parser.items(section)
        for param in params:
            db[param[0]] = param[1]
    else:
        raise Exception(f'Not found section "{section}" in the file "{filename}"')

    return db


db_inf = config()
engine = create_engine(f"postgresql+psycopg2://{db_inf['user']}:{db_inf['password']}@{db_inf['host']}/{db_inf['database']}")
conn = engine.connect()
app = FastAPI()


class User(BaseModel):
    user_id: int
    name: str
    password: str
    accept_invitation: bool = Field(None)
    vote_qty: int


@app.get("/users/", response_model=List[User], response_model_exclude={"password"})
def show_all_users():
    sql = 'SELECT * FROM users ORDER BY user_id;'
    users = conn.execute(sql).fetchall()
    return users
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

Nun habe ich das Gefühl es mir zu einfach gemacht zu haben :lol:
Naja, du hast einfach das async - Keyword entfernt. Damit verzichtest du auf das Feature welches FastAPi eben auszeichnet
Benutzeravatar
__blackjack__
User
Beiträge: 13103
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Domroon: `psycopg2` wird importiert aber nicht verwendet.

Warum testest Du denn ob es einen Abschnitt in der INI gibt? Wenn man einfach darauf zugreift, wird doch auch ohne testen bereits eine (spezifischere) Ausnahme ausgelöst.

Dann ist das Erstellen des Wörterbuchs unnötig umständlich. `dict()` kann man etwas iterierbares übergeben das Schlüssel/Wert-Paare liefert.

`db_inf` könnte man ja `config` nennen, wenn man den Namen nicht schon für die Funktion verwendet hätte.

Ich würde den Abschnitt in der INI nicht mehr "postgresql" nennen wenn man in der URL für SQLAlchemy auch eine andere Datenbank auswählen kann. Und das schöne an diesen URLs ist auch, dass man *die* als *einen* Wert in die Konfiguration packen kann.

Die Verbindung würde ich wenn dann `connection` nennen, aber wenn Du es Dir schon einfach machst, warum dann überhaupt selbst eine Verbindung erstellen und nicht einfach direkt die `Engine` verwenden und sich *das* Objekt um Verbindungen kümmern lassen?

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
from configparser import ConfigParser
from typing import List

from fastapi import FastAPI
from pydantic import BaseModel
from pydantic.fields import Field
from sqlalchemy import create_engine


def get_config(filename="database.ini", section="database"):
    parser = ConfigParser()
    parser.read(filename)
    return dict(parser.items(section))


engine = create_engine(get_config()["url"])
app = FastAPI()


class User(BaseModel):
    user_id: int
    name: str
    password: str
    accept_invitation: bool = Field(None)
    vote_qty: int


@app.get(
    "/users/", response_model=List[User], response_model_exclude={"password"}
)
def show_all_users():
    return engine.execute("SELECT * FROM users ORDER BY user_id").fetchall()
Aber für SQL als Zeichenketten würde ich nicht SQLAlchemy verwenden. Das fängt ja schon damit an, dass man den "*" da nicht schreiben würde, sondern da alle Spalten auflistet die man haben möchte, und da spart einem SQLAlchemy dann schon auf Dauer Arbeit, dass man das nur an einer Stelle bei der Tabellendefinition angeben muss. Sofern man das nicht dynamisch von der Datenbank macht, was ich persönlich bisher aber nur bei schon bestehenden DBs gemacht habe und in der Regel auch nur als schnelle Massnahme um die `Table`-Objekte später im Laufe der Zeit zu definieren.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

einfachTobi hat geschrieben: Mittwoch 15. Dezember 2021, 21:53 FastAPI selbst hat dazu eine übersichtliche und gute Anleitung, welche auch auf SQLAlchemy zurückgreift: https://fastapi.tiangolo.com/tutorial/sql-databases/
Welche allerdings etwas absurd ist. Wieso sollte man FastAPI nutzen welches den klaren Fokus auf async hat und dann synchrone Aufrufe zur Datenbank machen? Kann man natürlich ist dann aber nicht sonderlich sinnvoll und erst recht nicht "fast".
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

DasIch hat geschrieben: Donnerstag 16. Dezember 2021, 17:05 Welche allerdings etwas absurd ist. Wieso sollte man FastAPI nutzen welches den klaren Fokus auf async hat und dann synchrone Aufrufe zur Datenbank machen? Kann man natürlich ist dann aber nicht sonderlich sinnvoll
Die Dokumentation "Advanced User Guide" enthält ein Tutorial
https://fastapi.tiangolo.com/advanced/a ... -databases
mit dem man über
https://github.com/encode/databases
asynchrone Aufrufe machen kann.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Das hat Sinn gemacht als SQLAlchemy noch keine Unterstützung für asynchrone Aufrufe hatte. Ich würde mir aber gut überlegen ob es noch langfristig Sinn macht encode/databases zu nutzen.
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

DasIch hat geschrieben: Donnerstag 16. Dezember 2021, 17:26 Das hat Sinn gemacht als SQLAlchemy noch keine Unterstützung für asynchrone Aufrufe hatte. Ich würde mir aber gut überlegen ob es noch langfristig Sinn macht encode/databases zu nutzen.
Die Freiheit steht ja jedem offen. Ich habe die Dokumentation nicht geschrieben, aber ich denke es soll nur *eine* Möglichkeit gezeigt werden. Nicht, dass man unbedingt diese nutzen sollte.
Benutzeravatar
__blackjack__
User
Beiträge: 13103
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@DasIch: Also macht es auch immer noch Sinn, denn SQLALchemy (stable) hat ja noch keine Unterstützung für asynchrone Aufrufe. Da steht was von Beta und da ist ein Bild von einem Drachen der Feuer nach einem weglaufenden Ritter spuckt. Ich weiss nicht ob ich das ausser zum testen schon benutzen wollen würde. 😀
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Stimmt ist aber ein grundsätzliches Problem mit async in Python. Das gesamte Ökosystem ist noch recht jung und wenn man sich Projekte ein bisschen anschaut merkt man schnell dass es alles noch ein bisschen wackelig ist. Hinter encode/databases steht z.B. nur ein Autor/Contributor. Ein bisschen Risikobereitschaft muss man da schon mitbringen. Wenn man die nicht hat und sich nicht zutraut solche Projekte im Zweifel zu übernehmen oder so ein Beta Label schon abschreckt, sollte man doch eher bei etablierten Projekten wie Django oder Flask bleiben oder eine andere Programmiersprache nutzen (Javascript/Typescript oder Go z.B.) wenn es unbedingt asynchron sein soll.
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

DasIch hat geschrieben: Donnerstag 16. Dezember 2021, 20:24 Stimmt ist aber ein grundsätzliches Problem mit async in Python. Das gesamte Ökosystem ist noch recht jung und wenn man sich Projekte ein bisschen anschaut merkt man schnell dass es alles noch ein bisschen wackelig ist.
Das wundert mich, ehrlich gesagt, etwas. Asyncio ist schon seit 3.4, oder so, in Python. Und davor gab es auch schon Frameworks wie Tornado, was mittlerweile über 10 Jahre alt ist. Asynchrone Programmierung ist in Python ist nichts Neues, hat sich über die letzten Jahre aber stark verbessert und ist durch die vereinfachte Syntax noch zugänglicher geworden. Dass es ein "grundsätzliches Problem" mit async in Python gibt, kann ich nicht nachvollziehen. Was meinst du mit "wackelig"? Ist das dein Eindruck, oder ist das was konkretes? Würde mich mal interessieren.
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

Na die Integration in andere Projekte ist schon wackelig und eben gerne auch mal die Arbeit motivierter einzelner, statt der betroffenen Projekte. Zb PyQt muss man extra qasync installieren, das verspricht, anders als seine Vorgänger, maintained zu werden: https://pypi.org/project/qasync/ -hat aber auch wenig Aktivität: https://github.com/CabbageDevelopment/qasync/pulls
rogerb
User
Beiträge: 878
Registriert: Dienstag 26. November 2019, 23:24

PyQt ist ja in erster Linie für Desktop Applikationen gedacht. Es wundert mich jetzt nicht so sehr, dass es da eher schleppend mit der async-Unterstützung läuft.
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

Gerade Desktop Apps leiden darunter, dass ihrGUI Code zwingend in einem thread laufen MUSS. Wenn man also Bibliotheken verwenden kann, die quasi-parallel darin laufen, ohne das man sich die diversen Probleme mit Threads einfängt, ist das ein Vorteil. Wenn es dann nicht geht, obwohl das seit 10 Jahren der Standard ist, dann ist das eben ein Beleg dafür, dass das nicht so weit ist, wie man sich das wünschen würde.
Antworten