
SQL avec database/sql
Maîtrisez l'accès aux bases de données SQL en Go avec `database/sql` : connexion, requêtes, transactions, gestion des erreurs et bonnes pratiques pour des applications robustes.
Introduction à l'accès SQL en Go : Le package database/sql
Pour interagir avec des bases de données SQL en Go, le package standard database/sql est la pierre angulaire. database/sql fournit une interface générique et abstraite pour accéder à différents types de bases de données SQL (MySQL, PostgreSQL, SQLite, etc.) de manière uniforme, sans être lié à une implémentation de base de données spécifique. Il agit comme une couche d'abstraction qui permet à votre code Go d'interagir avec n'importe quelle base de données SQL compatible, à condition d'utiliser le driver SQL approprié.
Imaginez database/sql comme une interface commune que tous les drivers SQL Go doivent implémenter. Chaque driver SQL (par exemple, mysql, pq pour PostgreSQL, sqlite3) fournit l'implémentation spécifique de cette interface pour une base de données SQL particulière. Votre code Go interagit avec la base de données via l'interface database/sql, sans avoir à se soucier des détails d'implémentation spécifiques à chaque base de données. Cette approche favorise la portabilité, la flexibilité et la maintenabilité de votre code, en vous permettant de changer de base de données SQL (si nécessaire) en modifiant simplement le driver SQL, sans impacter le reste de votre code.
Ce chapitre vous propose un guide complet sur l'accès aux bases de données SQL en Go avec le package database/sql. Nous allons explorer en détail les étapes de connexion à une base de données SQL, l'exécution de requêtes SQL (SELECT, INSERT, UPDATE, DELETE), la gestion des transactions, la préparation de requêtes préparées pour la performance et la sécurité, la gestion des erreurs, et les bonnes pratiques pour une interaction efficace et robuste avec les bases de données SQL en Go. Que vous soyez débutant ou développeur backend expérimenté, ce guide vous fournira les connaissances et les compétences nécessaires pour maîtriser l'accès SQL avec database/sql et construire des applications Go performantes et persistantes.
Connexion à une base de données SQL : Driver et chaîne de connexion
La première étape pour interagir avec une base de données SQL en Go est d'établir une connexion à la base de données. La connexion est gérée par le package database/sql, en combinaison avec un driver SQL spécifique à la base de données que vous souhaitez utiliser (MySQL, PostgreSQL, SQLite, etc.).
Choisir et importer un driver SQL :
Vous devez choisir un driver SQL Go compatible avec la base de données que vous utilisez et l'importer dans votre code Go. Chaque base de données SQL a généralement son propre driver Go tiers. Voici quelques drivers SQL Go populaires :
- MySQL : Driver
"github.com/go-sql-driver/mysql"(ou"database/mysql", moins maintenu). Importation :import _ "github.com/go-sql-driver/mysql"(importation anonyme, seul l'effet de bord de l'enregistrement du driver est nécessaire). - PostgreSQL : Driver
"github.com/lib/pq". Importation :import _ "github.com/lib/pq". - SQLite : Driver
"github.com/mattn/go-sqlite3". Importation :import _ "github.com/mattn/go-sqlite3". - SQL Server : Driver
"github.com/microsoft/go-mssqldb". Importation :import _ "github.com/microsoft/go-mssqldb".
Ouverture de la connexion avec sql.Open :
Pour ouvrir une connexion à la base de données, vous utilisez la fonction sql.Open(driverName string, dataSourceName string) (*DB, error) du package database/sql.
driverName string: Le nom du driver SQL à utiliser. Ce nom doit correspondre au nom enregistré par le driver SQL lors de son importation (par exemple,"mysql","postgres","sqlite3","sqlserver"). Le nom du driver est spécifique à chaque driver SQL.dataSourceName string: La chaîne de connexion (data source name - DSN), qui contient les informations de connexion à la base de données (nom d'utilisateur, mot de passe, hôte, port, nom de la base de données, etc.). Le format de la chaîne de connexion est spécifique à chaque driver SQL et à chaque base de données. Consultez la documentation du driver SQL spécifique que vous utilisez pour connaître le format de la chaîne de connexion approprié.- Retourne
*sql.DB: En cas de succès,sql.Openretourne un pointeur vers un objetsql.DB, qui représente un pool de connexions à la base de données. Il est important de noter quesql.Openn'établit pas immédiatement une connexion à la base de données. Il configure et initialise un pool de connexions, mais la connexion réelle n'est établie que lors de la première requête ou lors d'un appel àdb.Ping(). - Retourne
error: En cas d'échec de l'ouverture du pool de connexions (par exemple, driverName inconnu, chaîne de connexion invalide),sql.Openretourne une erreurerrornon-nil.
Exemples de chaînes de connexion (DSN) pour différents drivers SQL :
- MySQL :
"utilisateur:motdepasse@tcp(hôte:port)/nom_base_de_données"(par exemple,"user:password@tcp(localhost:3306)/mydatabase") - PostgreSQL :
"user=utilisateur password=motdepasse host=hôte port=port dbname=nom_base_de_données sslmode=disable"(par exemple,"user=mydbuser password=mydbpassword host=localhost port=5432 dbname=mydb sslmode=disable") - SQLite (fichier) :
"./chemin/vers/base_de_donnees.db"(chemin vers le fichier de base de données SQLite) - SQLite (en mémoire) :
":memory:"(pour une base de données SQLite en mémoire, volatile)
Exemple d'ouverture de connexion à une base de données MySQL :
package main
import (
"database/sql"
"log"
"os"
_ "github.com/go-sql-driver/mysql" // Importation du driver MySQL (anonyme)
)
func main() {
// Chaîne de connexion MySQL (à adapter à votre configuration)
dataSourceName := "utilisateur:motdepasse@tcp(localhost:3306)/nom_base_de_données"
// Ouverture de la connexion à la base de données avec sql.Open
db, err := sql.Open("mysql", dataSourceName) // driverName = "mysql"
if err != nil {
log.Fatalf("Echec de l'ouverture de la connexion à la base de données: %v", err)
os.Exit(1)
}
defer db.Close() // Fermer la connexion à la sortie du programme principal (defer)
// ... (utilisation de la connexion 'db' pour exécuter des requêtes SQL) ...
log.Println("Connexion à la base de données réussie.")
}
Il est important de fermer explicitement la connexion à la base de données après utilisation avec defer db.Close() (comme illustré dans l'exemple) pour libérer les ressources système et éviter les fuites de connexions.
Exécution de requêtes SQL : Query, QueryRow, Exec
Une fois que vous avez établi une connexion à la base de données SQL avec sql.Open, vous pouvez exécuter des requêtes SQL pour interagir avec la base de données : sélectionner des données, insérer, mettre à jour, supprimer, etc. Le package database/sql fournit plusieurs méthodes sur l'objet *sql.DB (pool de connexions) pour exécuter différents types de requêtes SQL :
1. db.Query(query string, args ...interface{}) (*sql.Rows, error) : Requêtes SELECT (retournant plusieurs lignes)
La méthode db.Query est utilisée pour exécuter des requêtes SQL de type SELECT qui sont susceptibles de retourner plusieurs lignes de résultats. Elle prend en arguments la requête SQL (string) et des arguments optionnels pour les placeholders de la requête (args ...interface{}, pour les requêtes paramétrées). Elle retourne un *sql.Rows (itérateur sur les lignes de résultats) et une erreur error.
Itération sur les résultats avec rows.Next() et rows.Scan() :
Pour parcourir et traiter les lignes de résultats retournées par db.Query, vous devez utiliser un *sql.Rows et itérer sur les lignes avec les méthodes rows.Next() et rows.Scan() :
rows.Next() bool: Fait avancer l'itérateurrowsà la ligne suivante de résultats. Retournetrues'il y a encore une ligne à lire, etfalses'il n'y a plus de lignes (fin des résultats). Utilisezrows.Next()dans une boucleforpour itérer sur toutes les lignes de résultats.rows.Scan(dest ...interface{}) error: Copie les valeurs des colonnes de la ligne courante (pointée par l'itérateurrows) dans les variables de destination (dest ...interface{}) passées en arguments. Le nombre et le type des variables de destination doivent correspondre au nombre et au type des colonnes retournées par la requête SQL. Retourne une erreurerroren cas d'erreur lors de la copie des valeurs (par exemple, types incompatibles).
Exemple de requête SELECT avec db.Query et itération sur les résultats :
package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "utilisateur:motdepasse@tcp(localhost:3306)/nom_base_de_données")
// ... (gestion de l'erreur d'ouverture de connexion) ...
defer db.Close()
// Requête SQL SELECT (récupérer tous les utilisateurs)
requeteSQL := "SELECT id, nom, prenom, email FROM utilisateurs"
rows, err := db.Query(requeteSQL) // Exécution de la requête avec db.Query
if err != nil {
log.Fatal(err)
}
defer rows.Close() // Fermer rows après utilisation (important !)
fmt.Println("Liste des utilisateurs :")
for rows.Next() { // Itération sur les lignes de résultats avec rows.Next()
var id int
var nom string
var prenom string
var email string
err := rows.Scan(&id, &nom, &prenom, &email) // Copie des valeurs des colonnes dans les variables avec rows.Scan()
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID: %d, Nom: %s, Prénom: %s, Email: %s\n", id, nom, prenom, email)
}
// Vérification des erreurs d'itération (après la boucle rows.Next)
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
2. db.QueryRow(query string, args ...interface{}) (*sql.Row) : Requêtes SELECT (retournant une seule ligne)
La méthode db.QueryRow est utilisée pour exécuter des requêtes SQL de type SELECT qui sont censées retourner au plus une seule ligne de résultat (par exemple, une requête avec une clause WHERE basée sur une clé primaire ou un index unique). Elle prend les mêmes arguments que db.Query (requête SQL et arguments optionnels) et retourne un *sql.Row (représentant une seule ligne de résultat) et une erreur error.
Récupération des valeurs d'une seule ligne avec row.Scan() :
Pour récupérer les valeurs de la ligne de résultat retournée par db.QueryRow, vous utilisez directement la méthode row.Scan(dest ...interface{}) error sur l'objet *sql.Row. row.Scan fonctionne de la même manière que rows.Scan, mais elle opère sur une seule ligne et ne nécessite pas d'itération.
Exemple de requête SELECT avec db.QueryRow (récupérer un utilisateur par ID) :
package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "utilisateur:motdepasse@tcp(localhost:3306)/nom_base_de_donnees")
// ... (gestion de l'erreur d'ouverture de connexion) ...
defer db.Close()
utilisateurID := 123
// Requête SQL SELECT (récupérer un utilisateur par ID, requête paramétrée avec placeholder '?')
requeteSQL := "SELECT id, nom, prenom, email FROM utilisateurs WHERE id = ?"
row := db.QueryRow(requeteSQL, utilisateurID) // Exécution de la requête avec db.QueryRow, placeholder '?'
var id int
var nom string
var prenom string
var email string
err = row.Scan(&id, &nom, &prenom, &email) // Copie des valeurs de la ligne de résultat avec row.Scan()
if err != nil {
if err == sql.ErrNoRows {
fmt.Println("Utilisateur non trouvé.") // Gestion du cas où aucun utilisateur n'est trouvé (sql.ErrNoRows)
} else {
log.Fatal(err)
}
return
}
fmt.Printf("Utilisateur trouvé : ID: %d, Nom: %s, Prénom: %s, Email: %s\n", id, nom, prenom, email)
}
3. db.Exec(query string, args ...interface{}) (sql.Result, error) : Requêtes INSERT, UPDATE, DELETE (sans retour de lignes)
La méthode db.Exec est utilisée pour exécuter des requêtes SQL qui ne retournent pas de lignes de résultat (ou qui retournent une seule ligne que vous n'avez pas besoin de traiter explicitement), comme les requêtes INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE, etc. Elle prend les mêmes arguments que db.Query et db.QueryRow (requête SQL et arguments optionnels) et retourne un sql.Result (qui permet d'accéder aux métadonnées de l'opération, comme l'ID de la dernière ligne insérée ou le nombre de lignes affectées) et une erreur error.
Utilisation de sql.Result pour récupérer les métadonnées de l'opération :
L'interface sql.Result retournée par db.Exec permet d'accéder à des métadonnées sur l'opération exécutée :
result.LastInsertId() (int64, error): Retourne l'ID de la dernière ligne insérée (généré automatiquement par la base de données) pour les requêtesINSERTavec auto-incrémentation. Utile pour récupérer l'ID d'une nouvelle ressource créée.result.RowsAffected() (int64, error): Retourne le nombre de lignes affectées par la requête (INSERT,UPDATE,DELETE). Utile pour vérifier si l'opération a eu l'effet escompté.
Exemple de requête INSERT avec db.Exec (créer un nouvel utilisateur) :
package main
import (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "utilisateur:motdepasse@tcp(localhost:3306)/nom_base_de_donnees")
// ... (gestion de l'erreur d'ouverture de connexion) ...
defer db.Close()
nouveauUtilisateur := Utilisateur{Nom: "Martin", Prenom: "Julie", Email: "julie.martin@example.com"}
// Requête SQL INSERT (créer un nouvel utilisateur, requête paramétrée)
requeteSQL := "INSERT INTO utilisateurs(nom, prenom, email) VALUES(?, ?, ?)"
result, err := db.Exec(requeteSQL, nouveauUtilisateur.Nom, nouveauUtilisateur.Prenom, nouveauUtilisateur.Email) // Exécution de la requête avec db.Exec, placeholders '?'
if err != nil {
log.Fatal(err)
}
// Récupération de l'ID de la dernière ligne insérée avec result.LastInsertId()
id, err := result.LastInsertId()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Nouvel utilisateur créé avec ID: %d\n", id)
// Récupération du nombre de lignes affectées (devrait être 1 pour une insertion réussie) avec result.RowsAffected()
rowsAffected, err := result.RowsAffected()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Nombre de lignes affectées: %d\n", rowsAffected)
}
Ces trois méthodes (db.Query, db.QueryRow, db.Exec) du package database/sql couvrent la plupart des besoins pour exécuter différents types de requêtes SQL en Go et interagir avec les bases de données SQL de manière efficace et idiomatique.
Transactions SQL : Garantir la cohérence des données
Les transactions SQL sont un mécanisme essentiel pour garantir la cohérence et l'intégrité des données dans les bases de données relationnelles, en particulier lors d'opérations complexes qui impliquent plusieurs requêtes SQL qui doivent être exécutées de manière atomique (tout ou rien). Le package database/sql de Go supporte pleinement les transactions SQL, vous permettant de mettre en oeuvre des opérations transactionnelles robustes dans vos applications Go.
Principes des transactions SQL (ACID) :
Les transactions SQL respectent les propriétés ACID (Atomicity, Consistency, Isolation, Durability), qui garantissent la fiabilité et la cohérence des données :
- Atomicité (Atomicity) : Une transaction est traitée comme une unité de travail atomique : soit toutes les opérations de la transaction sont exécutées avec succès et les modifications sontCommitées (enregistrées de manière permanente dans la base de données), soit, en cas d'échec, aucune des opérations n'est exécutée et la transaction est Rollbackée (annulée), restaurant l'état de la base de données à son état initial avant le début de la transaction. L'atomicité garantit que les transactions sont "tout ou rien".
- Cohérence (Consistency) : Une transaction doit maintenir la cohérence de la base de données. Elle doit garantir que la base de données passe d'un état cohérent à un autre état cohérent après l'exécution de la transaction. La cohérence repose sur le respect des contraintes d'intégrité définies dans le schéma de la base de données (contraintes de clés primaires, clés étrangères, contraintes de validation, etc.).
- Isolation (Isolation) : Les transactions doivent être exécutées de manière isolée les unes des autres. L'exécution concurrente de plusieurs transactions ne doit pas interférer entre elles et doit produire le même résultat que si les transactions étaient exécutées de manière séquentielle. L'isolation garantit que chaque transaction a une vue consistante et isolée de la base de données, même en présence de transactions concurrentes. Différents niveaux d'isolation (Read Uncommitted, Read Committed, Repeatable Read, Serializable) définissent le degré d'isolation entre les transactions et les compromis entre la cohérence et la concurrence.
- Durabilité (Durability) : Une fois qu'une transaction est Commitée avec succès, les modifications apportées à la base de données sont persistantes et durables, même en cas de panne du système, de coupure de courant, ou d'autres types de défaillances. La durabilité est généralement assurée par les mécanismes de journalisation (transaction log) et de persistance sur disque de la base de données.
Gestion des transactions avec database/sql en Go :
Le package database/sql fournit les outils nécessaires pour gérer les transactions SQL en Go :
- Démarrer une transaction :
db.Begin() (*sql.Tx, error): La méthodedb.Begin()sur l'objet*sql.DBdémarre une nouvelle transaction SQL et retourne un objet*sql.Tx(représentant la transaction) et une erreurerror. Toutes les opérations SQL exécutées via l'objet*sql.Txferont partie de la même transaction. - Commit de la transaction :
tx.Commit() error: La méthodetx.Commit()sur l'objet*sql.TxCommit la transaction, c'est-à-dire enregistre de manière permanente toutes les modifications apportées à la base de données au cours de la transaction. Après untx.Commit()réussi, les modifications sont garanties d'être durables. - Rollback de la transaction :
tx.Rollback() error: La méthodetx.Rollback()sur l'objet*sql.Txeffectue un rollback (annulation) de la transaction, c'est-à-dire annule toutes les modifications apportées à la base de données au cours de la transaction et restaure l'état de la base de données à son état initial avant le début de la transaction. Le rollback est généralement effectué en cas d'erreur ou d'échec lors de l'exécution des opérations de la transaction. - Exécuter des requêtes dans une transaction : Pour exécuter des requêtes SQL dans le contexte d'une transaction, utilisez les méthodes
tx.Query,tx.QueryRowettx.Execsur l'objet*sql.Tx(et non les méthodesdb.Query,db.QueryRow,db.Execsur*sql.DB). Les méthodestx.*fonctionnent de la même manière que les méthodesdb.*correspondantes, mais elles exécutent les requêtes dans le contexte de la transactiontx.
Exemple de transaction SQL en Go (transfert d'argent entre deux comptes) :
package main
import (
"database/sql"
"fmt"
"log"
"os"
)
func transfererArgent(db *sql.DB, compteDebitID int, compteCreditID int, montant float64) error {
tx, err := db.Begin() // Démarrage d'une transaction avec db.Begin()
if err != nil {
return fmt.Errorf("Begin transaction failed: %w", err)
}
// Rollback de la transaction en cas de panic ou d'erreur non gérée (defer)
defer tx.Rollback()
// 1. Débiter le compte débit
_, err = tx.Exec("UPDATE comptes SET solde = solde - ? WHERE id = ?", montant, compteDebitID)
if err != nil {
return fmt.Errorf("Debit account failed: %w", err)
}
// 2. Créditer le compte crédit
_, err = tx.Exec("UPDATE comptes SET solde = solde + ? WHERE id = ?", montant, compteCreditID)
if err != nil {
return fmt.Errorf("Credit account failed: %w", err)
}
// Commit de la transaction si toutes les opérations réussissent
err = tx.Commit() // Commit de la transaction avec tx.Commit()
if err != nil {
return fmt.Errorf("Commit transaction failed: %w", err)
}
fmt.Println("Transfert d'argent effectué avec succès (transaction commitée).")
return nil // Succès de la transaction
}
func main() {
db, err := sql.Open("mysql", "utilisateur:motdepasse@tcp(localhost:3306)/nom_base_de_donnees")
// ... (gestion de l'erreur d'ouverture de connexion) ...
defer db.Close()
err = transfererArgent(db, 1, 2, 100.00)
if err != nil {
log.Fatalf("Erreur lors du transfert d'argent: %v", err)
os.Exit(1)
}
}
Cet exemple illustre l'utilisation des transactions SQL en Go pour réaliser un transfert d'argent entre deux comptes de manière atomique. Les opérations de débit et de crédit des comptes sont exécutées à l'intérieur d'une transaction (délimitée par tx.Begin() et tx.Commit() ou tx.Rollback()), garantissant que les deux opérations sont exécutées avec succès ou qu'aucune des deux n'est exécutée en cas d'erreur, préservant ainsi la cohérence des données bancaires.
Requêtes préparées : Performance et sécurité améliorées
Les requêtes préparées (prepared statements) sont une technique essentielle pour améliorer à la fois la performance et la sécurité des requêtes SQL, en particulier pour les requêtes qui sont exécutées plusieurs fois avec des paramètres différents. Le package database/sql de Go supporte pleinement les requêtes préparées, vous permettant de les utiliser facilement dans vos applications Go.
Avantages des requêtes préparées :
- Performance améliorée (pour les requêtes répétées) : Lorsqu'une requête préparée est exécutée pour la première fois, la base de données compile et optimise le plan d'exécution de la requête. Pour les exécutions suivantes de la même requête préparée (avec des paramètres différents), la base de données peut réutiliser le plan d'exécution précompilé, évitant ainsi la surcharge de la compilation et de l'optimisation à chaque exécution. Les requêtes préparées sont donc plus rapides et plus efficaces pour les requêtes répétées, en particulier pour les requêtes complexes ou les requêtes exécutées un grand nombre de fois.
- Sécurité renforcée (prévention des injections SQL) : Les requêtes préparées permettent de se prémunir contre les attaques par injection SQL, une vulnérabilité de sécurité courante dans les applications web qui manipulent des bases de données. Avec les requêtes préparées, les paramètres de la requête sont passés séparément de la requête SQL elle-même, et la base de données se charge d'échapper et de valider correctement les paramètres avant de les utiliser dans la requête. Cela empêche les attaquants d'injecter du code SQL malveillant dans les paramètres de la requête et de compromettre la sécurité de la base de données.
Utilisation des requêtes préparées avec database/sql en Go :
Le workflow d'utilisation des requêtes préparées avec database/sql comprend généralement les étapes suivantes :
- Préparer la requête :
db.Prepare(query string) (*sql.Stmt, error): Utilisez la méthodedb.Preparesur l'objet*sql.DBpour préparer une requête SQL.db.Prepareprend en argument la requête SQL (string) avec des placeholders (?pour MySQL, SQLite, PostgreSQL,@p1,@p2, etc. pour SQL Server) pour les paramètres, et retourne un*sql.Stmt(prepared statement) et une erreurerror. La préparation de la requête est une opération potentiellement coûteuse (compilation et optimisation), et il est donc recommandé de préparer les requêtes une seule fois et de les réutiliser pour de multiples exécutions avec des paramètres différents. - Exécuter la requête préparée :
stmt.Query(args ...interface{}) (*sql.Rows, error),stmt.QueryRow(args ...interface{}) (*sql.Row),stmt.Exec(args ...interface{}) (sql.Result, error): Utilisez les méthodesstmt.Query,stmt.QueryRowetstmt.Execsur l'objet*sql.Stmtpour exécuter la requête préparée avec des arguments pour les placeholders. Ces méthodes fonctionnent de la même manière que les méthodesdb.Query,db.QueryRowetdb.Execcorrespondantes, mais elles opèrent sur la requête préparée (*sql.Stmt) et prennent en arguments uniquement les valeurs des paramètres (args ...interface{}), la requête SQL elle-même ayant déjà été spécifiée lors de la préparation. - Fermer la requête préparée :
stmt.Close() error: Lorsque vous avez terminé d'utiliser une requête préparée, fermez-la explicitement avec la méthodestmt.Close()pour libérer les ressources associées à la requête préparée côté serveur (par exemple, les ressources de compilation et d'optimisation). Il est important de fermer les requêtes préparées pour éviter les fuites de ressources, en particulier si vous préparez un grand nombre de requêtes différentes. Utilisezdefer stmt.Close()pour garantir la fermeture de la requête préparée, même en cas d'erreur.
Exemple d'utilisation de requêtes préparées (requête SELECT répétée avec différents IDs) :
package main
import (
"database/sql"
"fmt"
"log"
"os"
)
func main() {
db, err := sql.Open("mysql", "utilisateur:motdepasse@tcp(localhost:3306)/nom_base_de_donnees")
// ... (gestion de l'erreur d'ouverture de connexion) ...
defer db.Close()
// Préparation de la requête SQL SELECT (une seule fois, en dehors de la boucle)
requetePreparee, err := db.Prepare("SELECT id, nom, prenom, email FROM utilisateurs WHERE id = ?") // Préparation de la requête avec db.Prepare, placeholder '?'
if err != nil {
log.Fatal(err)
}
defer requetePreparee.Close() // Fermeture de la requête préparée à la sortie du programme principal (defer)
// Exécution répétée de la requête préparée avec différents IDs (boucle)
utilisateurIDs := []int{1, 5, 10, 123}
for _, utilisateurID := range utilisateurIDs {
row := requetePreparee.QueryRow(utilisateurID) // Exécution de la requête préparée avec stmt.QueryRow, en passant uniquement la valeur du paramètre
var id int
var nom string
var prenom string
var email string
err = row.Scan(&id, &nom, &prenom, &email)
if err != nil {
if err == sql.ErrNoRows {
fmt.Printf("Utilisateur avec ID %d non trouvé.\n", utilisateurID)
} else {
log.Fatal(err)
}
continue // Passer à l'ID suivant en cas d'erreur
}
fmt.Printf("Utilisateur trouvé (requête préparée) : ID: %d, Nom: %s, Prénom: %s, Email: %s\n", id, nom, prenom, email)
}
}
Cet exemple illustre l'utilisation de requêtes préparées pour exécuter une requête SELECT de manière répétée avec différents IDs d'utilisateurs. La requête SQL est préparée une seule fois en dehors de la boucle (db.Prepare), et elle est ensuite exécutée plusieurs fois à l'intérieur de la boucle avec différents paramètres (requetePreparee.QueryRow(utilisateurID)), améliorant ainsi la performance et la sécurité des requêtes répétées.
Bonnes pratiques pour l'accès aux bases de données SQL avec database/sql
Pour interagir avec des bases de données SQL de manière efficace, robuste et sécurisée en Go avec database/sql, voici quelques bonnes pratiques à suivre :
- Toujours vérifier les erreurs retournées par les fonctions
database/sql: Vérifiez systématiquement les erreurs retournées par toutes les fonctions du packagedatabase/sql(sql.Open,db.Query,db.Exec,tx.Commit,tx.Rollback,rows.Next,rows.Scan, etc.) et traitez-les de manière appropriée. Une gestion rigoureuse des erreurs est essentielle pour la robustesse des applications qui accèdent aux bases de données. - Fermer explicitement les connexions (
db.Close()) et les Rows (rows.Close()) : Fermez explicitement les connexions à la base de données (db.Close()) et les objets*sql.Rows(rows.Close()) après utilisation pour libérer les ressources système (connexions, sockets, mémoire, curseurs) et éviter les fuites de ressources. Utilisezdeferpour garantir la fermeture, même en cas d'erreur ou de panic. - Utiliser des requêtes préparées (
db.Prepare,stmt.Exec,stmt.Query,stmt.QueryRow) pour les requêtes répétées : Privilégiez l'utilisation de requêtes préparées pour les requêtes SQL qui sont exécutées plusieurs fois avec des paramètres différents. Les requêtes préparées améliorent la performance et la sécurité (prévention des injections SQL). - Gérer les transactions SQL (
db.Begin,tx.Commit,tx.Rollback) pour les opérations atomiques : Utilisez les transactions SQL (sql.Tx) pour garantir l'atomicité et la cohérence des opérations complexes qui impliquent plusieurs requêtes SQL qui doivent être exécutées comme une unité de travail (tout ou rien). Committez explicitement les transactions avectx.Commit()en cas de succès, et effectuez un rollback avectx.Rollback()en cas d'erreur. - Valider et échapper les données utilisateur avant de les inclure dans les requêtes SQL : Validez et échappez toujours les données utilisateur (provenant des requêtes HTTP, des entrées utilisateur, etc.) avant de les inclure dans les requêtes SQL, pour vous protéger contre les attaques par injection SQL. Utilisez les placeholders (
?ou@p1, etc.) des requêtes préparées pour passer les paramètres de manière sûre, et laissez la base de données se charger de l'échappement et de la validation des paramètres. - Configurer correctement le pool de connexions
sql.DB(timeouts, max connections, etc.) : Configurez les options du pool de connexionssql.DB(timeouts de connexion, timeouts d'inactivité, nombre maximum de connexions, etc.) pour optimiser la performance et la robustesse de votre application en fonction de la charge de travail et des caractéristiques de votre base de données. Utilisez les méthodesdb.SetConnMaxLifetime,db.SetMaxIdleConns,db.SetMaxOpenConnspour configurer le pool de connexions. - Utiliser un driver SQL adapté à votre base de données et bien maintenu : Choisissez un driver SQL Go qui soit compatible avec votre base de données SQL spécifique (MySQL, PostgreSQL, SQLite, SQL Server, etc.) et qui soit activement maintenu et mis à jour par sa communauté. Consultez la documentation et les recommandations de la communauté Go pour choisir le driver SQL le plus approprié à vos besoins.
- Tester rigoureusement l'accès à la base de données (tests unitaires, tests d'intégration) : Testez rigoureusement votre code d'accès à la base de données avec des tests unitaires (pour tester les fonctions d'accès aux données isolément) et des tests d'intégration (pour tester l'interaction avec la base de données réelle). Testez les cas nominaux (succès) et les cas d'erreur (erreurs de connexion, erreurs de requête, contraintes d'intégrité violées, etc.) pour garantir la robustesse et la fiabilité de votre code d'accès aux données.
En appliquant ces bonnes pratiques, vous maîtriserez l'accès aux bases de données SQL avec database/sql en Go et construirez des applications robustes, performantes, sécurisées et faciles à maintenir, en interaction efficace et fiable avec vos bases de données SQL.