Jede Anwendung, die mit SQL-Abfragen arbeitet, kann von der Verwendung eines Abfrage-Builders profitieren, um die Lesbarkeit, Wartbarkeit und Sicherheit des Codes zu verbessern. Tatsächlich gibt es in Golang viele verschiedene Bibliotheken, die genau das tun. Hier bei Vaunt haben wir viele verschiedene Optionen ausprobiert, bevor wir uns schließlich entschieden haben, selbst eine zu erstellen. Letztendlich wollten wir etwas, das sicher ist und Variablenersatz bietet, um SQL-Injection zu verhindern, aber dennoch lesbar ist und bedingte Anweisungen verwenden kann. Deshalb haben wir eine neue Bibliothek namens tqla erstellt, die Ende letzten Jahres veröffentlicht und angekündigt wurde. Mehr darüber können Sie in diesem Artikel lesen.
Bevor wir tqla erstellt haben, haben wir hauptsächlich Squirrel für unsere SQL-Abfrageerstellungslogik verwendet – und wir können es wärmstens empfehlen. In einigen Bereichen verwenden wir immer noch Squirrel, haben jedoch nach und nach damit begonnen, neue Abfrageerstellungslogik durch TQLA zu ersetzen und zu implementieren. Wir haben viele Fälle gefunden, in denen tqla unsere Fähigkeit verbessert hat, unseren Code zu verwalten und Probleme zu beheben, die bei der Verwendung anderer Anweisungsersteller aufgetreten sind.
Bei Vaunt haben wir kürzlich eine Datenbankmigration von CockroachDB zu TiDB durchgeführt. Obwohl CockroachDB leistungsstark und zuverlässig war, standen wir letztendlich vor der Entscheidung, unseren Techstack um die Unterstützung einer OLAP-Datenbank zu erweitern. Der Bedarf hierfür bestand darin, unsere analytische Arbeitsbelastung für unser Open-Source-Community-Insight-Produkt zu unterstützen. Um unseren technologischen Fußabdruck klein zu halten, haben wir uns entschieden, mit TiDB weiterzumachen und die Vorteile der HTAP-Architektur der Datenbank zu nutzen.
CockroachDB ist weitgehend mit PostgreSQL kompatibel und wir haben für viele unserer SQL-Abfragen die PostgreSQL-Syntax verwendet. Um zu TiDB zu wechseln, mussten wir einige unserer Tabellen ändern und Abfragen aktualisieren, um die MySQL-Syntax zu verwenden. An einigen Stellen haben wir während der Migration festgestellt, dass wir Anweisungen zum Erstellen bedingter Abfragen falsch verwendet haben und nicht über die richtigen Tests verfügt haben, um festzustellen, ob die Anweisungen falsch generiert wurden.
In der README-Datei von Squirrel gibt es ein Beispiel dafür, wie Sie mithilfe der bedingten Abfrageerstellung Anweisungen mit optionalen Filtern aktualisieren können:
if len(q) > 0 { users = users.Where("name LIKE ?", fmt.Sprint("%", q, "%")) }
Hier ist ein echtes, aber vereinfachtes Beispiel dafür, wie wir eine unserer Abfragen aktualisiert haben, um Tabellen bedingt zu verknüpfen und einen optionalen Filter hinzuzufügen:
psql := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Question) statementBuilder := psql.Select(`i.id`). From("vaunt.installations i"). Where(`entity_name = ?`, name) if len(provider) > 0 { statementBuilder.Where(`provider = ?`, provider) } if len(repo) > 0 { statementBuilder.Join(`repositories as r on JSON_CONTAINS(i.repositories, CONCAT('["', r.id, '"]'))`) statementBuilder.Where(`r.name = ?`, repo) }
Können Sie das Problem mit dem Code erkennen? Wenn nicht, machen Sie sich keine Sorgen – es ist etwas, das auch bei unseren eigenen Codeüberprüfungen durchgefallen ist, bis wir unsere Tests durchgeführt haben.
Das Problem hierbei ist, dass wir vergessen haben, den Anweisungs-Builder mit dem Ergebnis der Builder-Funktionen zu aktualisieren. Beispielsweise sollte der Anbieterbedingungsfilter stattdessen lauten:
if len(provider) > 0 { statementBuilder = statementBuilder.Where(`provider = ?`, provider) }
Dies ist ein relativ einfacher Fehler und kann mit ausreichend Testfällen leicht erkannt werden. Da es sich jedoch technisch gesehen nicht um ungültigen Code handelt, kann es eine Weile dauern, bis sofort erkannt wird, was passiert.
Ein weiteres Lesbarkeitsproblem bei diesem Setup besteht darin, dass der bedingte Join von der anfänglichen Select-Anweisung getrennt ist. Wir könnten den Builder neu organisieren, um jedes Teil dort zu platzieren, wo es hingehört, aber es würde mehrere doppelte Prüfungen bedingter Anweisungen erfordern und immer noch unter einigen Lesbarkeitsproblemen leiden.
Die obige Demonstration mit Squirrel wurde inzwischen umgeschrieben und das Äquivalent in tqla sieht so aus:
t, err := tqla.New(tqla.WithPlaceHolder(tqla.Question)) if err != nil { return nil, err } query, args, err := t.Compile(` SELECT i.id FROM vaunt.installations as i {{ if .Repo }} JOIN vaunt.repositories as r on JSON_CONTAINS(i.repositories, CONCAT('["', r.id, '"]'), '$') {{ end }} WHERE entity_name = {{ .Name}} {{ if .Provider }} AND i.provider = {{ .Provider }} {{ end }} {{ if .Repo }} AND r.name = {{ .Repo }} {{ end }} `, data) if err != nil { return nil, err }
Wie Sie sehen, macht die Vorlagensyntax für tqla das Einbinden von Bedingungsklauseln sehr einfach. Tqla ersetzt automatisch die von uns festgelegten Variablen durch unsere angegebenen Platzhalter und stellt die Argumente bereit, die wir mit unserem SQL-Treiber zum Ausführen der Anweisung verwenden können.
Ähnlich wie bei Squirrel ist dieser Ansatz zur Anweisungserstellung einfach zu testen, da wir verschiedene Sätze von Datenobjekten erstellen können, um sie an den Vorlagenersteller zu übergeben und die Ausgabe zu validieren.
Sie sehen, dass wir problemlos bedingte Teile der Abfrage dort hinzufügen können, wo sie am besten hineinpassen. Hier haben wir beispielsweise einen bedingten JOIN direkt nach der FROM-Anweisung – und obwohl wir immer noch mehrere Bedingungsprüfungen haben, verkompliziert dies die Vorlage nicht übermäßig.
Eine weitere nette TQLA-Funktion, die zur Verbesserung der Wartbarkeit unserer SQL-Builder beiträgt, ist die Möglichkeit, benutzerdefinierte Funktionen zu definieren, die wir in den Vorlagen verwenden können, um einige Transformationslogiken zu abstrahieren.
Hier ist ein Beispiel dafür, wie wir eine Funktion verwendet haben, um den time.Time-Wert von Golang in einen sql.NullTime-Wert umzuwandeln, damit wir eine Einfügung mit unseren Datenobjekten durchführen können, ohne ihn vorher konvertieren zu müssen:
funcs := template.FuncMap{ "time": func(t time.Time) sql.NullTime { if t.IsZero() { return sql.NullTime{Valid: false} } return sql.NullTime{Time: t, Valid: true} }, } t, err := tqla.New(tqla.WithPlaceHolder(tqla.Question), tqla.WithFuncMap(funcs)) if err != nil { return err }
Da diese Funktion in unserer TQLA-Funktionszuordnung definiert ist, können wir sie jetzt frei in unseren Abfragevorlagen verwenden, indem wir ihr einen Parameter aus dem Datenobjekt bereitstellen, das ein time.Time-Feld ist. Wir können diese Funktion sogar mehrmals in derselben Vorlage mit unterschiedlichen Feldern aufrufen.
Hier ist ein vereinfachtes Beispiel:
statement, args, err := t.Compile(` INSERT INTO events (name, created_at, merged_at, closed_at) VALUES ( {{ .Name }}, {{ time .CreatedAt }}, {{ time .MergedAt }}, {{ time .ClosedAt }} )`, eventData)
Zusammenfassend glauben wir, dass die Verwendung von tqla dazu beitragen kann, die Wartbarkeit der Abfrageerstellungslogik zu verbessern und gleichzeitig ein leistungsstarkes Dienstprogramm für die Erstellung dynamischer Abfragen bietet. Die Einfachheit der Vorlagenstruktur ermöglicht eine saubere Codelesbarkeit und kann das Debuggen potenzieller Fehler beschleunigen.
Wir haben tqla als Open Source zur gemeinsamen Nutzung dieser Bibliothek erstellt, in der Hoffnung, dass sie eine gute Option für andere Benutzer darstellt, die eine einfache, wartbare und sichere Möglichkeit zum Erstellen von SQL-Abfragen in vielen verschiedenen Arten von Anwendungen suchen.
Wenn Sie interessiert sind, schauen Sie sich bitte das Repository an und geben Sie ihm einen Stern, wenn es Ihnen in irgendeiner Weise hilft. Fühlen Sie sich frei, Funktionswünsche oder Fehlerberichte zu stellen!
Wir sind immer offen für Feedback und Beiträge.
Um über zukünftige Entwicklungen auf dem Laufenden zu bleiben, folgen Sie uns auf X oder treten Sie unserem Discord bei!
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3