375 lines
11 KiB
Go
375 lines
11 KiB
Go
//! Gestionnaire de requêtes préparées pour optimiser les performances
|
|
//!
|
|
//! Ce module implémente un cache de requêtes préparées pour améliorer
|
|
//! les performances et la sécurité des requêtes SQL fréquentes.
|
|
|
|
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// PreparedStatement représente une requête préparée avec son nom
|
|
type PreparedStatement struct {
|
|
Name string
|
|
Query string
|
|
Stmt *sql.Stmt
|
|
}
|
|
|
|
// PreparedStatementManager gère le cache des requêtes préparées
|
|
type PreparedStatementManager struct {
|
|
db *sql.DB
|
|
statements map[string]*PreparedStatement
|
|
mutex sync.RWMutex
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewPreparedStatementManager crée un nouveau gestionnaire de requêtes préparées
|
|
func NewPreparedStatementManager(db *sql.DB, logger *zap.Logger) *PreparedStatementManager {
|
|
return &PreparedStatementManager{
|
|
db: db,
|
|
statements: make(map[string]*PreparedStatement),
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Prepare prépare une requête SQL et la met en cache
|
|
func (psm *PreparedStatementManager) Prepare(ctx context.Context, name, query string) error {
|
|
psm.mutex.Lock()
|
|
defer psm.mutex.Unlock()
|
|
|
|
// Vérifier si la requête est déjà préparée
|
|
if _, exists := psm.statements[name]; exists {
|
|
psm.logger.Debug("Statement already prepared", zap.String("name", name))
|
|
return nil
|
|
}
|
|
|
|
// Préparer la requête
|
|
stmt, err := psm.db.PrepareContext(ctx, query)
|
|
if err != nil {
|
|
psm.logger.Error("Failed to prepare statement",
|
|
zap.String("name", name),
|
|
zap.String("query", query),
|
|
zap.Error(err))
|
|
return fmt.Errorf("failed to prepare statement %s: %w", name, err)
|
|
}
|
|
|
|
// Mettre en cache
|
|
psm.statements[name] = &PreparedStatement{
|
|
Name: name,
|
|
Query: query,
|
|
Stmt: stmt,
|
|
}
|
|
|
|
psm.logger.Debug("Statement prepared successfully",
|
|
zap.String("name", name))
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetStatement récupère une requête préparée depuis le cache
|
|
func (psm *PreparedStatementManager) GetStatement(name string) (*sql.Stmt, error) {
|
|
psm.mutex.RLock()
|
|
defer psm.mutex.RUnlock()
|
|
|
|
stmt, exists := psm.statements[name]
|
|
if !exists {
|
|
return nil, fmt.Errorf("statement %s not found", name)
|
|
}
|
|
|
|
return stmt.Stmt, nil
|
|
}
|
|
|
|
// Execute exécute une requête préparée avec des arguments
|
|
func (psm *PreparedStatementManager) Execute(ctx context.Context, name string, args ...interface{}) (sql.Result, error) {
|
|
stmt, err := psm.GetStatement(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return stmt.ExecContext(ctx, args...)
|
|
}
|
|
|
|
// Query exécute une requête préparée et retourne des lignes
|
|
func (psm *PreparedStatementManager) Query(ctx context.Context, name string, args ...interface{}) (*sql.Rows, error) {
|
|
stmt, err := psm.GetStatement(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return stmt.QueryContext(ctx, args...)
|
|
}
|
|
|
|
// QueryRow exécute une requête préparée et retourne une ligne
|
|
func (psm *PreparedStatementManager) QueryRow(ctx context.Context, name string, args ...interface{}) *sql.Row {
|
|
stmt, err := psm.GetStatement(name)
|
|
if err != nil {
|
|
// Retourner une erreur dans le Row
|
|
return &sql.Row{}
|
|
}
|
|
|
|
return stmt.QueryRowContext(ctx, args...)
|
|
}
|
|
|
|
// Initialize prépare toutes les requêtes fréquemment utilisées
|
|
func (psm *PreparedStatementManager) Initialize(ctx context.Context) error {
|
|
psm.logger.Info("Initializing prepared statements...")
|
|
|
|
// Requêtes utilisateur
|
|
statements := map[string]string{
|
|
"get_user_by_id": `
|
|
SELECT id, username, email, password_hash, created_at, updated_at, deleted_at
|
|
FROM users WHERE id = $1 AND deleted_at IS NULL`,
|
|
|
|
"get_user_by_email": `
|
|
SELECT id, username, email, password_hash, created_at, updated_at, deleted_at
|
|
FROM users WHERE email = $1 AND deleted_at IS NULL`,
|
|
|
|
"get_user_by_username": `
|
|
SELECT id, username, email, password_hash, created_at, updated_at, deleted_at
|
|
FROM users WHERE username = $1 AND deleted_at IS NULL`,
|
|
|
|
"create_user": `
|
|
INSERT INTO users (username, email, password_hash, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5) RETURNING id`,
|
|
|
|
"update_user": `
|
|
UPDATE users SET username = $2, email = $3, updated_at = $4
|
|
WHERE id = $1 AND deleted_at IS NULL`,
|
|
|
|
"delete_user": `
|
|
UPDATE users SET deleted_at = $2 WHERE id = $1`,
|
|
|
|
// Requêtes de session
|
|
"get_session_by_token": `
|
|
SELECT id, user_id, token, created_at, expires_at, ip_address, user_agent, is_valid
|
|
FROM sessions WHERE token = $1 AND expires_at > $2 AND is_valid = true`,
|
|
|
|
"create_session": `
|
|
INSERT INTO sessions (user_id, token, created_at, expires_at, ip_address, user_agent)
|
|
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
|
|
|
"revoke_session": `
|
|
UPDATE sessions SET is_valid = false, revoked_at = $2 WHERE token = $1`,
|
|
|
|
"revoke_user_sessions": `
|
|
UPDATE sessions SET is_valid = false, revoked_at = $2
|
|
WHERE user_id = $1 AND is_valid = true`,
|
|
|
|
"cleanup_expired_sessions": `
|
|
DELETE FROM sessions WHERE expires_at < $1`,
|
|
|
|
// Requêtes de messages
|
|
"get_messages_by_room": `
|
|
SELECT m.id, m.room_id, m.user_id, m.content, m.type, m.parent_id,
|
|
m.is_edited, m.is_deleted, m.created_at, m.updated_at,
|
|
u.username, u.email
|
|
FROM messages m
|
|
JOIN users u ON m.user_id = u.id
|
|
WHERE m.room_id = $1 AND m.created_at < $2
|
|
ORDER BY m.created_at DESC LIMIT $3`,
|
|
|
|
"create_message": `
|
|
INSERT INTO messages (room_id, user_id, content, type, parent_id, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
|
|
|
"update_message": `
|
|
UPDATE messages SET content = $2, is_edited = true, updated_at = $3
|
|
WHERE id = $1 AND user_id = $4`,
|
|
|
|
"delete_message": `
|
|
UPDATE messages SET is_deleted = true, updated_at = $2 WHERE id = $1`,
|
|
|
|
// Requêtes de tracks
|
|
"get_track_by_id": `
|
|
SELECT id, user_id, title, artist, duration, file_path, file_size,
|
|
mime_type, status, created_at, updated_at
|
|
FROM tracks WHERE id = $1 AND status = 'active'`,
|
|
|
|
"get_user_tracks": `
|
|
SELECT id, user_id, title, artist, duration, file_path, file_size,
|
|
mime_type, status, created_at, updated_at
|
|
FROM tracks WHERE user_id = $1 AND created_at < $2 AND status = 'active'
|
|
ORDER BY created_at DESC LIMIT $3`,
|
|
|
|
"create_track": `
|
|
INSERT INTO tracks (user_id, title, artist, duration, file_path, file_size, mime_type, status, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`,
|
|
|
|
"update_track": `
|
|
UPDATE tracks SET title = $2, artist = $3, updated_at = $4
|
|
WHERE id = $1 AND user_id = $5`,
|
|
|
|
"delete_track": `
|
|
UPDATE tracks SET status = 'deleted', updated_at = $2 WHERE id = $1`,
|
|
|
|
// Requêtes de rooms
|
|
"get_room_by_id": `
|
|
SELECT id, name, description, type, is_private, created_by, created_at, updated_at
|
|
FROM rooms WHERE id = $1`,
|
|
|
|
"get_user_rooms": `
|
|
SELECT r.id, r.name, r.description, r.type, r.is_private, r.created_by, r.created_at, r.updated_at
|
|
FROM rooms r
|
|
JOIN room_users ru ON r.id = ru.room_id
|
|
WHERE ru.user_id = $1 AND r.created_at < $2
|
|
ORDER BY r.created_at DESC LIMIT $3`,
|
|
|
|
"create_room": `
|
|
INSERT INTO rooms (name, description, type, is_private, created_by, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
|
|
|
|
"add_user_to_room": `
|
|
INSERT INTO room_users (room_id, user_id, created_at)
|
|
VALUES ($1, $2, $3) ON CONFLICT (room_id, user_id) DO NOTHING`,
|
|
|
|
"remove_user_from_room": `
|
|
DELETE FROM room_users WHERE room_id = $1 AND user_id = $2`,
|
|
|
|
// Requêtes d'audit
|
|
"create_audit_log": `
|
|
INSERT INTO audit_logs (user_id, action, entity_type, entity_id, ip_address, user_agent, details, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
|
|
|
|
"get_audit_logs": `
|
|
SELECT id, user_id, action, entity_type, entity_id, ip_address, user_agent, details, created_at
|
|
FROM audit_logs WHERE user_id = $1 AND created_at < $2
|
|
ORDER BY created_at DESC LIMIT $3`,
|
|
|
|
// Requêtes de recherche
|
|
"search_tracks": `
|
|
SELECT id, user_id, title, artist, duration, file_path, file_size,
|
|
mime_type, status, created_at, updated_at,
|
|
ts_rank(to_tsvector('english', title || ' ' || artist), plainto_tsquery('english', $1)) as rank
|
|
FROM tracks WHERE status = 'active' AND to_tsvector('english', title || ' ' || artist) @@ plainto_tsquery('english', $1)
|
|
ORDER BY rank DESC, created_at DESC LIMIT $2`,
|
|
|
|
"search_messages": `
|
|
SELECT m.id, m.room_id, m.user_id, m.content, m.type, m.created_at,
|
|
u.username, u.email,
|
|
ts_rank(to_tsvector('english', m.content), plainto_tsquery('english', $1)) as rank
|
|
FROM messages m
|
|
JOIN users u ON m.user_id = u.id
|
|
WHERE m.room_id = $2 AND to_tsvector('english', m.content) @@ plainto_tsquery('english', $1)
|
|
ORDER BY rank DESC, m.created_at DESC LIMIT $3`,
|
|
}
|
|
|
|
// Préparer toutes les requêtes
|
|
for name, query := range statements {
|
|
if err := psm.Prepare(ctx, name, query); err != nil {
|
|
psm.logger.Error("Failed to prepare statement",
|
|
zap.String("name", name),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
|
|
psm.logger.Info("All prepared statements initialized successfully",
|
|
zap.Int("count", len(statements)))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close ferme toutes les requêtes préparées
|
|
func (psm *PreparedStatementManager) Close() error {
|
|
psm.mutex.Lock()
|
|
defer psm.mutex.Unlock()
|
|
|
|
var lastErr error
|
|
for name, stmt := range psm.statements {
|
|
if err := stmt.Stmt.Close(); err != nil {
|
|
psm.logger.Error("Failed to close statement",
|
|
zap.String("name", name),
|
|
zap.Error(err))
|
|
lastErr = err
|
|
}
|
|
}
|
|
|
|
// Vider le cache
|
|
psm.statements = make(map[string]*PreparedStatement)
|
|
|
|
psm.logger.Info("All prepared statements closed")
|
|
return lastErr
|
|
}
|
|
|
|
// GetStats retourne les statistiques des requêtes préparées
|
|
func (psm *PreparedStatementManager) GetStats() map[string]interface{} {
|
|
psm.mutex.RLock()
|
|
defer psm.mutex.RUnlock()
|
|
|
|
stats := map[string]interface{}{
|
|
"total_statements": len(psm.statements),
|
|
"statements": make([]string, 0, len(psm.statements)),
|
|
}
|
|
|
|
for name := range psm.statements {
|
|
stats["statements"] = append(stats["statements"].([]string), name)
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
// RefreshStatement rafraîchit une requête préparée (utile après reconnexion DB)
|
|
func (psm *PreparedStatementManager) RefreshStatement(ctx context.Context, name string) error {
|
|
psm.mutex.Lock()
|
|
defer psm.mutex.Unlock()
|
|
|
|
stmt, exists := psm.statements[name]
|
|
if !exists {
|
|
return fmt.Errorf("statement %s not found", name)
|
|
}
|
|
|
|
// Fermer l'ancienne requête
|
|
if err := stmt.Stmt.Close(); err != nil {
|
|
psm.logger.Warn("Failed to close old statement",
|
|
zap.String("name", name),
|
|
zap.Error(err))
|
|
}
|
|
|
|
// Préparer la nouvelle requête
|
|
newStmt, err := psm.db.PrepareContext(ctx, stmt.Query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to refresh statement %s: %w", name, err)
|
|
}
|
|
|
|
stmt.Stmt = newStmt
|
|
psm.logger.Debug("Statement refreshed", zap.String("name", name))
|
|
|
|
return nil
|
|
}
|
|
|
|
// RefreshAllStatements rafraîchit toutes les requêtes préparées
|
|
func (psm *PreparedStatementManager) RefreshAllStatements(ctx context.Context) error {
|
|
psm.mutex.Lock()
|
|
defer psm.mutex.Unlock()
|
|
|
|
var lastErr error
|
|
for name, stmt := range psm.statements {
|
|
// Fermer l'ancienne requête
|
|
if err := stmt.Stmt.Close(); err != nil {
|
|
psm.logger.Warn("Failed to close old statement",
|
|
zap.String("name", name),
|
|
zap.Error(err))
|
|
}
|
|
|
|
// Préparer la nouvelle requête
|
|
newStmt, err := psm.db.PrepareContext(ctx, stmt.Query)
|
|
if err != nil {
|
|
psm.logger.Error("Failed to refresh statement",
|
|
zap.String("name", name),
|
|
zap.Error(err))
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
stmt.Stmt = newStmt
|
|
}
|
|
|
|
psm.logger.Info("All statements refreshed")
|
|
return lastErr
|
|
}
|