Erfolgreicher execute(INSERT) aber kein Datensatz

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Odin
User
Beiträge: 20
Registriert: Samstag 5. September 2015, 10:11

Hallo zusammen,
schon habe ich das nächste Problem. Folgende Funktion soll einfach nur prüfen ob der Datensatz
schon Verhanden ist und die Verweiße stimmen. Wenn das passt soll sie den Datensatz einfügen.

Code: Alles auswählen

def addTag(cursor, name, uebertag, ordner):
    query = "SELECT ID FROM Tags WHERE Name = '%s'"
    cursor.execute(query, name)
    if not(cursor.with_rows):
        return -1
    clear(cursor)
    iduebertag = -1
    if(uebertag != "root"):
        cursor.execute(query, uebertag)
        if not(cursor.with_rows):
            return -2
        else:
            (iduebertag)=cursor.fetchone()
    clear(cursor)
    if(ordner=="true"):
        ordner = True
    else:
        ordner = False

    query = ("INSERT INTO Tags (Name, Uebertag, Ordner) "
        +"VALUES (\""+name+"\", "+str(iduebertag)+", "+str(ordner)+");")

    cursor.execute(query)
    clear(cursor)

    return 1

def clear(cursor):
    try:
        cursor.fetchall()
    except mysql.connector.Error as err:
        pass
Wenn ich das jetzt teste gibt die Funktion 1 zurück. SChau ich dann in die Datenbank wird es verwirrend.
Der Auto_Increment_Counter ist nach jedem Aufruf höher, aber es ist kein Datensatz verhanden.
Weiß jemand was da los ist?

Gruß
Odin
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Du schreibst ja leider nicht, welches RDBMS Du ansprichst, aber höchstwahrscheinlich fehlt Dir ein ``COMMIT``, um die (implizite?) Transaktion auch abzuschließen.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Odin
User
Beiträge: 20
Registriert: Samstag 5. September 2015, 10:11

Ich benutze MySQL.
Jetzt wo du commit erwähnst dämmert mir da was.
Das kommt davon wenn man an der Uni immer nur Theorie macht. -.-
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Der Code an sich bietet auch ziemlich viel gruseliges... aber den Review überlasse ich jetzt mal Leuten mit mehr Zeit und Muße :mrgreen:

Mein Tipp: Nutze SQLAlchemy und code nicht so low level ohne fundierte Ahnung von Python *und* von Datenbanken(-APIs). (``clear(cursor)`` geht ja mal gar nicht... :shock: )
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

@Odin: Lösch mal die vielen überflüssigen Klammern in Zeile 4,8,10,13 und 15. Der Query in Zeile 20 ist kaputt, dass da keine Fehlermeldung kommt, wundert mich. Wie man Parameter richtig übergibt hast Du in Zeile 2/3 doch schon gezeigt. Was der Sinn von clear ist, mußt Du auch noch erklären, außer der, dass er die Fehlersuche erschwert.
BlackJack

@Odin: Dieses `clear()` für den Cursor ist hochgradig suspekt. So etwas braucht man nicht. Weg damit.

Die Namenskonvention bei Python wäre `add_tag()` statt `addTag()`. Siehe Style Guide for Python Code.

Bei der ersten `query` gehören keine ' um den Platzhalter. Falls das tatsächlich funktionieren sollte ist das Zufall.

``not`` ist ein Operator und keine Funktion, das sollte man also auch nicht so schreiben als wäre es eine. Das gleiche gilt für ``if``. Die Klammern gehören da jeweils nicht hin und falls man Teilausdrücke klammern muss, dann gehört zwischen Schlüsselwort und Klammer ein Leerzeichen, eben damit das nicht wie ein Aufruf aussieht.

Das `with_rows`-Attribut gibt es allgemein auf Cursor-Objekten nicht. Das scheint eine Spezialität von dem Modul zu sein welches Du da verwendest. So etwas sollte man nicht machen. Einfach den Datensatz holen und auf die Ausnahme reagieren.

Wobei ich an der Stelle die Logik nicht ganz verstehe: Du testest ob es `name` als Tag gibt und wenn das der Fall ist, dann machst Du weiter. Sollte das nicht umgekehrt sein? Das weitergemacht wird falls es `name` als Tag noch *nicht* gibt?

Das mit der `iduebertag` (besser `id_uebertag` oder `uebertag_id`) und 'root' kann man verständlicher ausdrücken. Wobei ich mich gerade frage ob 'root' nicht als Datensatz in der Datenbank vorliegt oder warum man das hier gesondert behandeln muss, und das dann im Programm mit einem Wert der eine gültige ID in der Datenbank sein kann. Die werden ja wahrscheinlich von der Datenbank automatisch vergeben, und die könnte doch auch irgendwann einmal eine -1 vergeben. *Hoffst* Du einfach dass das niemals passiert und diese ID dann zwei Bedeutungen hat? Die die in der DB steht und die von der das Programm ausgeht. Ich würde ja 'root' einfach mit als Datensatz in der Datenbank aufnehmen.

Auf der linken Seite einer Zuweisung Klammern um einen einzelnen Namen zu setzen macht Null Sinn.

`ordner` sollte bitte schon als boole'scher Typ in die Funktion hinein kommen. So etwas wandelt man so früh wie möglich in passende Datentypen um und nicht irgendwo innerhalb von Funktionen die den Wert nicht selber in irgendeiner Weise von ”aussen” bekommen haben oder dediziert zum parsen gedacht sind.

Werte sollte man niemals selber in Zeichenketten mit SQL hineinformatieren. Das ist eine Sicherheitslücke und ein potentieller Performance-Verlust weil die Datenbank dann keine Ablaufpläne mit der SQL-Anfrage als Schlüssel cachen kann. Du machst das bei den ersten beiden Anfragen doch richtig, warum bei der dritten nicht mehr?

Zuguterletzt muss man dann nur noch diese komischen magischen Zahlen als Rückgabewerte loswerden. So etwas macht man in modernen Programmiersprachen mit Ausnahmen nicht mehr. Die Funktion läuft entweder erfolgreich durch oder löst eine Ausnahme aus. Mit dem Text bei der Ausnahme wird dann auch gleich deutlich wo das Problem liegt, statt kryptische Werte wie -1 oder -2 interpretieren zu müssen.

Ich lande dann bei so etwas (ungetestet):

Code: Alles auswählen

def add_tag(cursor, name, uebertag, ordner):
    query = 'SELECT ID FROM Tags WHERE Name = %s;'
    cursor.execute(query, name)
    try:
        cursor.fetchone()
    except mysql.connector.Error:
        try:
            cursor.execute(query, uebertag)
        except mysql.connector.Error:
            raise ValueError('parent tag {0!r} does not exist'.format(uebertag))
        else:
            uebertag_id = cursor.fetchone()

        cursor.execute(
            'INSERT INTO Tags (Name, Uebertag, Ordner) VALUES (%s, %s, %s);',
            (name, uebertag_id, ordner)
        )
    else:
        raise ValueError('tag {0!r} already exists'.format(name))
Wobei ich persönlich ja mittlerweile für alles was mit relationalen Datenbanken zu tun hat SQLAlchemy als Abstraktionsschicht verwende. Das ORM macht vieles einfacher. Selbst wenn man kein ORM verwenden möchte kann man auch SQL einfacher und von der konkreten Datenbank unabhängiger programmatisch erstellen ohne mit Zeichenkettenoperationen SQL zusammenbasteln zu müssen.

Die Tabelle `Tags` hätte ich `Tag` genannt, denn so eine Tabellendefinition ist ja so ähnlich wie eine Klassendefinition: die beschreibt die Eigenschaften von *einem* Exemplar oder im Falle der Datenbank von *einem* Datensatz.

Die `Ubertag`-Spalte würde ich `uebertag_id` nennen, damit man am Namen schon sieht das es sich höchstwahrscheinlich um einen Fremdschlüssel handelt.
BlackJack

Mal völlig ungetesteter Code wie das mit SQLAlchemy aussehen könnte:

Code: Alles auswählen

# ...

class Tag(Base):
    __tablename__ = 'Tags'

    id = Column('ID', INTEGER, primary_key=True)
    name = Column('Name', CHAR(100), unique=True)
    parent_tag_id = Column('Uebertag', ForeignKey('Tags.ID'))
    is_folder = Column('Ordner', BOOLEAN)

    parent = relationship('Tag', backref='children')

# ...

def add_tag(session, name, parent_name, is_folder):
    try:
        parent_tag = session.query(Tag).filter_by(name=parent_name).one()
        tag = Tag(name=name, parent=parent_tag, is_folder=is_folder)
        session.add(tag)
        session.commit()
    finally:
        session.rollback()
Durch die Klassendefinition ist das etwa gleich viel Code aber man wird ja mehr als eine Funktion haben die etwas mit der Datenbank und `Tag`-Objekten macht, und die sehen dadurch dann alle einfacher aus.

Einen Baum von Tags ausgeben geht mit einem übergebenen `Tag`-Exemplar, beispielsweise dem Wurzeltag, zum Beispiel so:

Code: Alles auswählen

def print_tag_tree(tag, offset=0):
    print(' ' * offset, tag.name)
    for child in tag.children:
        print_tag_tree(child, offset + 2)
Da sind die Datenbankanfragen enthalten! Das sieht manuell mit SQL als Zeichenketten sicher deutlich umständlicher aus.
Odin
User
Beiträge: 20
Registriert: Samstag 5. September 2015, 10:11

@Sirius3: Nein, da ich normalerweiße C und Java programmiere wäre es für mich eine riesige Umstellung. Außerdem macht es für mich den Code übersichtlicher. Und nein der Query ist nicht kaput es ist nur eine "statischere" Variante. Der Sinn von clear() ist das mir, wenn ich aus einem Ergebnis nur die Zahl der Zeilen entnehme aber nicht die Daten, keine exception geschmissen wird.

@BlackJack: Ich weiß das man eigendlich den Programmiersprachen eigenen Styleguide verwendet, aber da ich hauptsächlich Java und C verwende hätte ich damit die gleichen Probleme wie mit den Klammern.
Werden um einen String-Platzhalter automatisch die ' gesetzt?
Die Klammern beim not sind Überreste von meinem Versuch ! zum negieren zu verwenden (gewohnheit ...)
`with_rows`-Attribut vermutlich nicht, da das Ergebnis nichts mit der Anzahl der Ergebnisse korreliert.
Das mit der Logik stammt von meiner Verwirrung die durch das merkwürdige Verhalten von with_rows verursacht wurde.
Auf der linken Seite einer Zuweisung Klammern um einen einzelnen Namen zu setzen macht Null Sinn.
Ok
Ordner kommt direckt von einem Client und für die Übertragung muss ich es zu einem String machen.
warum bei der dritten nicht mehr?
Wieder altlasten. Ich hab bei der Parameter Übergabe nen Index verhauen weswegen das so aussah als ob die Platzhalter nicht richtig arbeiten.
Danke für den Code ich werden mir ein Beispiel daran nehmen.
Bei den Datenbank Sachen hast du recht.

Zu meiner Verteidigung, wenn ich die Funktion so weit funktionstüchtig bekommen hätte, hätte ich sie noch einmal gesäubert.
Ich werde mir eure Ratschläge zu Herzen nehmen, auch wenn ich nicht alle befolgen werde (die rein Optischen) und danke euch
das ihr mir so Gründlich helf.

@BlackJack: Ich mag aber meine SQL-String. Ich hab das erst 3 Jahre in der Schule und jetzt schon 2 Jahre an der Uni ins Hirn gehämmert bekommen,
die sind für mich fast Klartext.

Gruß
Odin
BlackJack

@Odin: SQL in Zeichenketten zusammenbasteln ist aber fehleranfälliger und oft auch nicht ganz unabhängig vom DBMS. Und wenn Du das ORM nicht nutzen willst (wobei sich da schon die Frage stellt warum nicht), kannst Du immer noch die SQL-Anfragen programmatisch und damit sicherer erzeugen.

Um Platzhalter wird im Idealfall gar nichts gesetzt sondern es werden die Platzhalter verwendet und SQL und Daten getrennt an die Datenbank übermittelt, also gar nicht erst in eine Zeichenkette hinein formatiert. Scheint bei MySQL beziehungsweise diesem Datenbankmodul nicht so gemacht zu werden, aber ”richtige” Datenbanken machen das so.

Und wie gesagt: Die `clear()`-Funktion macht keinen Sinn.
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

Odin hat geschrieben:Ich mag aber meine SQL-String. Ich hab das erst 3 Jahre in der Schule und jetzt schon 2 Jahre an der Uni ins Hirn gehämmert bekommen, die sind für mich fast Klartext.
Es ist wohl wieder Zeit hierfür: https://xkcd.com/327/
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Brandon Rhodes hat in einem seiner Vorträge gesagt: "If someone is good in another language, it's easy for him to produce ugly python."
Sicher ist das keine zwingende Voraussetzung, aber die Erkenntnis, dass auch Programmiererfahrung stets ausbaufähig bleibt.

@Odin: Den größten Nutzen von Python wirst Du haben, wenn Du die Ratschläge hier befolgst und idiomatisches Python lernst. Java wirst Du anschließend nicht mehr verwenden wollen.
Odin
User
Beiträge: 20
Registriert: Samstag 5. September 2015, 10:11

Zuerst das wichtigste:
@kbr: Ich habe Java nie verwenden wollen, ich will Java nicht verwenden, ich werden Java nie verwenden wollen.
Leider ist das die an der Uni und Schule verwendete Sprache. C, Assembler und VHDL sind die Sprachen die ich
gerne verwende (wenn ich den die Wahl habe).

Ich weiß das der Code nicht schön ist und man lebt um zu lernen.

@pillmuncher: Das kann man so programmieren, muss man aber nicht.

@BlackJack: So wie in Zeile 3 meinst du oder? Und die clear()-Funktion kommt raus hab ja jetzt nen besseren Ansatz
gezeigt bekommen.

Gruß
Odin

EDIT: Wenn ich den String und die Daten für einen query einzel übergebe werden die dann automatisch escaped?
BlackJack

@Odin: Bei der Nachfrage zur Zeile drei stehe ich irgendwelche auf dem Schlauch‽ Welche Zeile, wo, und auf welche Meinung von mir bezieht sich das‽ :-)

Wenn Du die SQL-Anweisung mit Platzhaltern versehen und die Werte als getrennte Argumente an `execute()` übergibst, dann werden die Werte im Idealfall gar nicht escaped weil wie schon gesagt im Idealfall die Werte dann gar nicht erst in SQL-Quelltext als Zeichenkette eingefügt werden. Für DB-Module oder Datenbanken die Clientseite aus allem erst SQL-Quelltext machen, werden die Daten dann natürlich escaped, deswegen soll man das ja auch nicht selber machen sondern dem Modul überlassen.
Odin
User
Beiträge: 20
Registriert: Samstag 5. September 2015, 10:11

Code: Alles auswählen

query = 'SELECT ID FROM Tags WHERE Name = %s;'
    cursor.execute(query, name)
Ist das die Methode die du mit Platzhalter meinst?
Da kann ich dann auch unbehandelte Strings reinschmeißen?

An der stelle schmeist es mir aber folgenden Error:

Code: Alles auswählen

mysql.connector.errors.ProgrammingError: 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '%s' at line 1
(Präzise an dem execute)
BlackJack

@Odin: Welchen Wert hat denn `name` an der Stelle?
Odin
User
Beiträge: 20
Registriert: Samstag 5. September 2015, 10:11

Die Variable name hatte in meinem Test den Wert "test". (Kreativität wird bei mir großgeschrieben :wink: )
BlackJack

@Odin: Das kann nicht funktionieren weil die Sequenz 'test' vier Elemente hat, die SQL-Abfrage aber nur einen Platzhalter. Da wäre dann aber auch eine andere Ausnahme gekommen. Also wäre ein komlettes Testbeispiel nötig um dem Problem auf die Spur zu kommen.
Odin
User
Beiträge: 20
Registriert: Samstag 5. September 2015, 10:11

Sorry ich komm leider nicht so viel zum arbeiten an meinem Projekt wie ich möchte.
weil die Sequenz 'test' vier Elemente hat
An den anderen Stellen werden doch über %s auch Strings unbekannter länge eingesetzt,
oder was meinst du mit Sequenz.

Für den Testfall habe ich folgende Werte verwendet:

Code: Alles auswählen

name="test"
uebertag=root
ordner=false
Brauchst du noch mehr?
BlackJack

@Odin: Das zweite Argument von `execute()` muss ein Sequenztyp sein, also irgendwas mit einer Länge und Indexzugriff (Liste, Tupel, …). Und jedes Element von dieser Sequenz wird dann für einen Platzhalter eingesetzt. Wenn Du also tatsächlich `name` als zweites Argument übergibst und `name` den Wert ``'test'`` hat, dann brauchst Du vier Platzhalter — für jeden Buchstaben einen. Wenn Du nur einen Platzhalter hast und da den *einen* Wert 'test' für übergeben willst, dann brauchst Du für den *einen* Platzhalter eine Sequenz mit diesem *einen* Wert, also zum Beispiel eine Liste mit dieser Zeichenkette als Element.
Odin
User
Beiträge: 20
Registriert: Samstag 5. September 2015, 10:11

Wenn ich name in Klammern setzte wird das dann zu einer einstelligen Sequence und löst das Problem?
Antworten