Toute application fonctionnant avec des requêtes SQL peut bénéficier de l'utilisation d'un générateur de requêtes pour améliorer la lisibilité, la maintenabilité et la sécurité du code. En fait, il existe de nombreuses bibliothèques différentes qui font exactement cela dans Golang. Chez Vaunt, nous avons essayé de nombreuses options différentes avant de finalement décider d'en créer une nous-mêmes. En fin de compte, nous voulions quelque chose de sécurisé et fournissant un remplacement de variable pour empêcher l'injection SQL tout en restant lisible et capable d'avoir des instructions conditionnelles. Nous avons donc créé une nouvelle bibliothèque appelée tqla, publiée et annoncée à la fin de l'année dernière. Vous pouvez en savoir plus à ce sujet dans cet article.
Avant de créer tqla, nous utilisions principalement Squirrel pour notre logique de création de requêtes SQL, et nous le recommandons vivement. Nous utilisons toujours Squirrel dans certains domaines, mais avons progressivement commencé à remplacer et à implémenter une nouvelle logique de création de requêtes avec tqla. Nous avons trouvé de nombreux cas dans lesquels tqla a amélioré notre capacité à maintenir notre code et à résoudre les problèmes que nous avons rencontrés lors de l'utilisation d'autres générateurs d'instructions.
Chez Vaunt, nous avons récemment subi une migration de base de données de CockroachDB vers TiDB. Bien que CockroachDB soit performant et fiable, nous avons finalement été confrontés à la décision d'ajouter à notre pile technologique pour prendre en charge une base de données OLAP. Le besoin était de soutenir notre charge de travail analytique sur notre produit open source d’informations sur la communauté. Pour réduire notre empreinte technologique, nous avons décidé d'aller de l'avant avec TiDB et de profiter de l'architecture HTAP de la base de données.
CockroachDB est largement compatible avec PostgreSQL et nous avons utilisé la syntaxe PostgreSQL pour plusieurs de nos requêtes SQL. Pour passer à TiDB, nous avons dû modifier quelques-unes de nos tables et mettre à jour les requêtes pour utiliser la syntaxe MySQL. À quelques endroits au cours de la migration, nous avons constaté que nous n'utilisions pas correctement les instructions de création de requêtes conditionnelles et que nous ne disposions pas des tests appropriés pour détecter que les instructions n'étaient pas générées correctement.
Dans le fichier README de Squirrel, vous trouverez un exemple de la manière dont vous pouvez utiliser la création de requêtes conditionnelles pour mettre à jour des instructions avec des filtres facultatifs :
if len(q) > 0 { users = users.Where("name LIKE ?", fmt.Sprint("%", q, "%")) }
Voici un exemple réel, mais simplifié, de la façon dont nous avons mis à jour l'une de nos requêtes pour joindre des tables de manière conditionnelle et ajouter un filtre facultatif :
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) }
Pouvez-vous repérer le problème avec le code ? Si ce n’est pas le cas, ne vous inquiétez pas : c’est quelque chose qui a également échappé à nos propres révisions de code jusqu’à ce que nous exécutions nos tests.
Le problème ici est que nous avons oublié de mettre à jour le générateur d'instructions avec le résultat des fonctions du générateur. Par exemple, le filtre de condition du fournisseur devrait plutôt lire :
if len(provider) > 0 { statementBuilder = statementBuilder.Where(`provider = ?`, provider) }
Il s'agit d'une erreur relativement simple à commettre et qui peut facilement être détectée avec suffisamment de cas de test, mais comme il ne s'agit pas d'un code techniquement invalide, cela peut prendre un peu de temps pour réaliser immédiatement ce qui se passe.
Un autre problème de lisibilité avec cette configuration est que la jointure conditionnelle est séparée de l'instruction select initiale. Nous pourrions réorganiser le constructeur pour placer chaque élément là où il devrait aller, mais cela nécessiterait plusieurs vérifications d'instructions conditionnelles en double et souffrirait toujours de certains problèmes de lisibilité.
La démonstration ci-dessus utilisant Squirrel a depuis été réécrite, et l'équivalent en tqla ressemble à ceci :
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 }
Comme vous pouvez le constater, la syntaxe du modèle pour tqla rend l'incorporation de clauses conditionnelles très simple. Tqla remplace automatiquement les variables que nous définissons par nos espaces réservés spécifiés et fournit les arguments que nous pouvons utiliser avec notre pilote SQL pour exécuter l'instruction.
Semblable à Squirrel, cette approche de création d'instructions est facile à tester, car nous pouvons créer différents ensembles d'objets de données à transmettre au générateur de modèles et valider la sortie.
Vous pouvez voir que nous sommes facilement en mesure d'ajouter des éléments conditionnels de la requête là où ils s'intégreraient le mieux. Par exemple, nous avons ici un JOIN conditionnel directement après l'instruction FROM et bien que nous ayons encore plusieurs vérifications de conditions, cela ne complique pas trop le modèle.
Une autre fonctionnalité intéressante de tqla qui contribue à améliorer la maintenabilité de nos constructeurs SQL est la possibilité de définir des fonctions personnalisées que nous pourrions utiliser dans les modèles pour abstraire une certaine logique de transformation.
Voici un exemple de la façon dont nous avons utilisé une fonction pour convertir la valeur time.Time du Golang en un sql.NullTime pour nous permettre de faire une insertion avec nos objets de données sans avoir besoin de la convertir au préalable :
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 }
Avec cette fonction définie dans notre tqla funcs map, nous pouvons désormais l'utiliser librement dans nos modèles de requête en lui fournissant un paramètre de l'objet de données qui est un champ time.Time. Nous pouvons même appeler cette fonction plusieurs fois dans le même modèle avec des champs différents.
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!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!