SELECT Funktion

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
BlackJack

@Bindl: Ich würde ja gerne noch was zu der Funktion im ersten Beitrag sagen. Als erstes: Die Namen sind so alle schlecht gewählt, bis auf `row`, wobei das an zwei Stellen der passende Name ist, und an zwei anderen Stellen der falsche Name ist. Denn der Name wird manchmal an *eine* Datenbankzeile gebunden, und manchmal an *mehrere* Datenbankzeilen.

`selection()` beschreibt keine Tätigkeit und damit auch nicht was die Funktion macht.

`all` ist der Name einer (nützlichen) eingebauten Funktion. Den sollte man nicht an etwas anderes binden. Das führt zu Irritationen beim Leser. Das Argument ist ein Flag, da sollte man also nicht `None` und *irgendwas anderes* übergeben, sondern `True` und `False`.

Schliessen von Ressourcen würde ich mit ``with`` und `contextlib.closing()` erledigen (sofern das Objekt selbst kein Kontextmanager ist). Wenn man das mit dem Cursor macht, dann wird man den `row`-Namen los, weil man einfach direkt ``return`` verwenden kann.

Die beiden Zweige von dem ``if``/``else`` enden mit genau dem gleichen Code. Der sollte da also nicht zweimal stehen, sondern einmal hinter dem ``if``/``else``.

Mit vernünftigeren Namen und ``with`` könnte das dann so aussehen:

Code: Alles auswählen

def select(
    result_column_name,
    table_name,
    column_name_1,
    column_name_2,
    value_1,
    value_2,
    fetch_all,
):
    with closing(g.connection.cursor()) as cursor:
        if value_2 is None:
            cursor.execute(
                'SELECT %s FROM %s WHERE %s = %s',
                (result_column_name, table_name, column_name_1, value_1,)
            )
        else:
            cursor.execute(
                'SELECT %s FROM %s WHERE %s = %s AND %s = %s',
                (
                    result_column_name,
                    table_name,
                    column_name_1,
                    value_1,
                    column_name_2,
                    value_2,
                )
            )
        return cursor.fetchall() if fetch_all else cursor.fetchone()
So wirklich wesentlich unterscheiden sich die beiden Zweige in der Funktion ja nicht und nummerierte Namen sind in der Regel auch keine gute Wahl. Sie deuten oft darauf hin, dass man keine einzelnen Namen und Werte, sondern eigentlich eine Datenstruktur verwenden sollte. Oft eine Liste. Und auch das wäre hier sinnvoll: Eine Liste mit Paaren von Spaltennamen und Werten aus denen dann die Anfrage aufgebaut wird (ungetestet):

Code: Alles auswählen

def select(result_column_name, table_name, names_and_values, fetch_all):
    sql = 'SELECT %s FROM %s'

    query_values = list()
    for item in names_and_values:
        query_values.extend(item)

    if query_values:
        sql += ' WHERE ' + ' AND '.join(['%s = %s'] * len(names_and_values))

    with closing(g.connection.cursor()) as cursor:
        cursor.execute(sql, query_values)
        return cursor.fetchall() if fetch_all else cursor.fetchone()
Ein Aufruf würde dann so aussehen:

Code: Alles auswählen

    row = select(
        'user_hashed_password',
        'user',
        [('user_name', request.form['logname'])],
        False
    )
Und man kann dann nicht nur ein oder zwei Kriterien übegerben, sondern 0 bis beliebig viele.

Achtung: Die Funktion hat natürlich immer noch das Problem mit den SQL-Namen und -Werten von der Ausgangsfunktion!

Was immer noch unpraktisch ist (neben der Tatsache das die Funktion nicht funktioniert ;-)) ist die unterschiedliche Struktur des Rückgabewerts in Abhängigkeit des letzten Arguments. Für solche APIs werden Dich andere Programmierer irgendwann anfangen zu hassen. Und da man selbst der ”andere Programmierer” ist, wenn man den Code eine Weile nicht angefasst hat, kann man das auch nicht mit „ist ja nur für mich“ abtun. Man könnte leicht zwei Funktionen schreiben und den gemeinsamen Teil in eine nicht-öffentliche Funktion auslagern:

Code: Alles auswählen

def _select(result_column_name, table_name, names_and_values, method_name):
    sql = 'SELECT %s FROM %s'

    query_values = list()
    for item in names_and_values:
        query_values.extend(item)

    if query_values:
        sql += ' WHERE ' + ' AND '.join(['%s = %s'] * len(names_and_values))

    with closing(g.connection.cursor()) as cursor:
        cursor.execute(sql, query_values)
        return getattr(cursor, method_name)()


def select_one(result_column_name, table_name, names_and_values):
    return _select(result_column_name, table_name, names_and_values, 'fetchone')


def select_all(result_column_name, table_name, names_and_values):
    return _select(result_column_name, table_name, names_and_values, 'fetchall')
Um noch mal auf SQLAlchemy zurück zu kommen: Das Datei hochladen in einem anderen Thema von Dir könnte man statt so:

Code: Alles auswählen

            cursor = g.connection.cursor()
            cursor.execute(
                'INSERT INTO file (file_file, file_name, file_author,'
                ' file_upload, file_type, data_size)'
                ' VALUES (%s, %s, %s, NOW(), %s, %s)',
                (
                    data,
                    filename,
                    session['username'],
                    os.path.splitext(filename)[1],
                    len(data),
                )
            )
            g.connection.commit()
            cursor.close()
dann so schreiben:

Code: Alles auswählen

            g.session.add(
                File(
                    content=data,
                    name=filename,
                    author=session['username'],
                    type=os.path.splitext(filename)[1],
                    size=len(data),
                )
            )
            g.session.commit()
Alle PDF-Dateien eines Benutzer abzufragen, ginge beispielsweise so:

Code: Alles auswählen

    pdf_files = (
        g.session.query(File)
            .filter(File.user_name == User.name, File.type == 'pdf')
            .order_by(File.name)
            .all()
    )
An alle Dateien eines Benutzers kommt man, wenn man die Fremdschlüsselbeziehungen definiert hat, über das Benutzerobjekt ganz einfach so:

Code: Alles auswählen

    files = user.files
Und man kann den Datenmodell-Objekten natürlich auch Methoden verpassen, damit man die Abfragen nicht überall im Code verteilt. Beispielsweise eine Methode die von einem Benutzer alle Dateien eines bestimmten Typs abfragen kann:

Code: Alles auswählen

class User(Base):
    # ...

    def get_files(self, file_type=None):
        query = self.files_query  # <- Bei Fremdschlüsselbeziehung definiert.
        if file_type is not None:
            query = query.filter(File.type == file_type)
        return query.all()
Damit kann man dann woanders im Code einfach das hier schreiben wenn man ein Benutzer-Objekt hat:

Code: Alles auswählen

    pdf_files = user.get_files('pdf')
Antworten