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 enthalten 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 bedingte Abfrageerstellungsanweisungen nicht ordnungsgemäß verwendeten und nicht über die richtigen Tests verfügten, um festzustellen, ob die Anweisungen falsch generiert wurden.
In der README-Datei von Squirrel finden Sie 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 reales, 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 das 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 können, 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 zum Erstellen von Anweisungen 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.
Here is a simplified example:
statement, args, err := t.Compile(` INSERT INTO events (name, created_at, merged_at, closed_at) VALUES ( {{ .Name }}, {{ time .CreatedAt }}, {{ time .MergedAt }}, {{ time .ClosedAt }} )`, eventData)
In conclusion, we believe that using tqla can help improve the maintainability of query building logic while offering some powerful utility for creating dynamic queries. The simplicity of the template structure allows for clean code readability and can make it faster to debug any potential errors.
We made tqla open source to share this library in hopes that it provides a good option for other users wanting a simple, maintainable, and secure way to build sql queries in many different types of applications.
If you are interested, please check out the repository and give it a star if it helps you in any way. Feel free to make any feature requests or bug reports!
We are always open to receiving feedback and contributions.
To stay in the loop on future development, follow us on X or join our Discord!
Das obige ist der detaillierte Inhalt vonWartbare SQL-Abfrageerstellung mit Golang. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!