WDO MySQL - Prepared Statements¶
I prepared statement sono il modo corretto di eseguire query con valori esterni (da form, query string, JSON, ecc.). Proteggono dall'SQL injection e, negli INSERT massivi, riutilizzano il piano di query lato server.
Prepare o Query?¶
Qualche valore proviene dall'esterno (form, URL, JSON, sessione)?
├── SÌ → Prepare. Senza eccezioni.
└── NO → La query si ripete molte volte con valori diversi?
├── SÌ → Prepare (evita il re-parsing sul server)
└── NO → Query. SQL costante senza variabili esterne.
Regola semplice: se scriveresti Escape(), scrivi Prepare().
Esempio base¶
LOCAL oConn := WDO_Get( "mysql" )
LOCAL oStmt, aRows
IF oConn == NIL ; RETURN USendError( 503, "DB unavailable" ) ; ENDIF
oStmt := oConn:Prepare( "SELECT id, name FROM users WHERE age > ? AND city = ?" )
oStmt:BindParam( 1, 18, "i" ) // indice 1-based, tipo "i" = intero
oStmt:BindParam( 2, "Girona", "s" ) // tipo "s" = string
oStmt:Execute()
aRows := oStmt:FetchAll( .T. )
oStmt:Free() // OBBLIGATORIO prima di Close
oConn:Close()
USendJson( aRows )
Segnaposto¶
Posizionale ?¶
L'N-esimo ? dell'SQL corrisponde a BindParam(N, ...):
oStmt := oConn:Prepare( "INSERT INTO log (level, msg) VALUES (?, ?)" )
oStmt:BindParam( 1, "error", "s" )
oStmt:BindParam( 2, cMessaggio, "s" )
oStmt:Execute()
oStmt:Free()
Nominale :name¶
oStmt := oConn:Prepare( ;
"SELECT id FROM users WHERE city = :city AND active = :active" )
oStmt:BindParam( ":city", "Girona", "s" )
oStmt:BindParam( ":active", 1, "i" )
oStmt:Execute()
Lo stesso :name può apparire più volte nell'SQL - basta un solo BindParam.
BindParams - scorciatoia posizionale¶
oStmt := oConn:Prepare( "INSERT INTO events (name, ts) VALUES (?, ?)" )
oStmt:BindParams( { cName, hb_DateTime() } ) // inferisce i tipi automaticamente
oStmt:Execute()
oStmt:Free()
Tipi¶
cType |
Uso | Esempio |
|---|---|---|
"i" |
Intero, booleano (0/1) | 18, .T. → 1 |
"n" |
Numerico decimale | 12.50 |
"s" |
String | "Girona" |
"d" |
Date → 'YYYY-MM-DD' |
Date() |
"t" |
Timestamp → 'YYYY-MM-DD HH:MM:SS' |
hb_DateTime() |
"b" |
Blob binario → x'HEX' |
dati binari |
| (omesso) | Inferito da ValType |
consigliato solo per tipi chiari |
Casi in cui l'inferenza può fallire
CToD("")haValType == "D"ma dovrebbe essereNULL→ passaNILesplicito.- Stringhe vuote →
''(stringa vuota SQL), nonNULL. Se vuoi NULL, passaNIL.
Pattern comuni¶
Paginazione¶
oStmt := oConn:Prepare( ;
"SELECT id, name FROM users ORDER BY id LIMIT ? OFFSET ?" )
oStmt:BindParam( 1, nLimit, "i" )
oStmt:BindParam( 2, nOffset, "i" )
oStmt:Execute()
aRows := oStmt:FetchAll( .T. )
oStmt:Free()
LIKE con wildcard¶
I % vanno nel valore, non nell'SQL:
oStmt := oConn:Prepare( "SELECT id FROM users WHERE name LIKE ?" )
oStmt:BindParam( 1, "%" + cRicerca + "%", "s" )
oStmt:Execute()
IN-list dinamica¶
MySQL non accetta IN (?) con un array - genera N segnaposto:
LOCAL aIds := { 1, 5, 42 }
LOCAL cSql, i
cSql := "SELECT id, name FROM users WHERE id IN (" + ;
Left( Replicate( "?,", Len( aIds ) ), Len( aIds ) * 2 - 1 ) + ")"
oStmt := oConn:Prepare( cSql )
FOR i := 1 TO Len( aIds )
oStmt:BindParam( i, aIds[ i ], "i" )
NEXT
oStmt:Execute()
INSERT massivo - riutilizzare lo stesso Prepare¶
oStmt := oConn:Prepare( "INSERT INTO log (level, msg) VALUES (?, ?)" )
FOR EACH hRow IN aLogRows
oStmt:BindParam( 1, hRow[ "level" ], "s" )
oStmt:BindParam( 2, hRow[ "msg" ], "s" )
IF ! oStmt:Execute()
le( "insert failed: " + oStmt:cError )
EXIT
ENDIF
NEXT
oStmt:Free()
UPDATE con campi opzionali¶
LOCAL aSet := {}, aVal := {}
IF ! Empty( cName )
AAdd( aSet, "name = ?" ) ; AAdd( aVal, { cName, "s" } )
ENDIF
IF nAge > 0
AAdd( aSet, "age = ?" ) ; AAdd( aVal, { nAge, "i" } )
ENDIF
IF Empty( aSet ) ; RETURN ; ENDIF
oStmt := oConn:Prepare( "UPDATE users SET " + ;
hb_ArrayToList( aSet, ", " ) + " WHERE id = ?" )
LOCAL i
FOR i := 1 TO Len( aVal )
oStmt:BindParam( i, aVal[ i ][ 1 ], aVal[ i ][ 2 ] )
NEXT
oStmt:BindParam( Len( aVal ) + 1, nId, "i" )
oStmt:Execute()
oStmt:Free()
Integrazione con il pool - Free() obbligatorio¶
Free() prima di Close()
Se non chiami Free(), il prepared statement lato server rimane attivo nella connessione (SHOW PREPARED STATEMENTS lo vedrà accumularsi). La prossima volta che il pool consegna quella connessione, lo slot ha statement orfani.
Pattern difensivo:
LOCAL oConn, oStmt
oConn := WDO_Get( "mysql" )
oStmt := NIL
TRY
oStmt := oConn:Prepare( "SELECT id FROM users WHERE age > ?" )
oStmt:BindParam( 1, 18, "i" )
oStmt:Execute()
// ...
FINALLY
IF oStmt != NIL ; oStmt:Free() ; ENDIF
oConn:Close()
END
Introspezione dopo Execute¶
oStmt:FCount() // numero di colonne del resultset
oStmt:DbStruct() // { { "id", "N", ... }, { "name", "C", ... }, ... }
oStmt:Row_Count() // righe interessate (INSERT/UPDATE/DELETE) o restituite (SELECT)
oConn:Last_Insert_Id() // ultimo AUTO_INCREMENT generato
oStmt:lError // .T. se c'è un errore
oStmt:cError // descrizione dell'errore
Antipattern da evitare¶
| Antipattern | Problema | Correzione |
|---|---|---|
Prepare dentro il loop |
N Prepare + N DEALLOCATE | Un Prepare fuori, N Execute dentro |
oStmt:Free() dimenticato |
Statement accumulati nel server | - in FINALLY |
LIKE '%?%' |
Il ? è dentro un literal, non è un segnaposto |
LIKE ? e wildcard nel valore |
oStmt in STATIC tra richieste |
Punta alla connessione sbagliata | Prepare dentro lo scope Get/Close |
Riutilizzare oStmt dopo Close() |
oConn non appartiene più a questo thread |
Scope di oStmt = scope del Get/Close |
Alt A - protocollo binario nativo¶
Per BLOB grandi (> 1 MB), dati binari con byte NUL o INSERT massivo ad alta precisione decimale, esiste WDO_MySqlStmtBin (Alt A), che usa il protocollo binario nativo di MySQL (mysql_stmt_*). Consulta docs/mysql/prepared_statements_bin.md per maggiori dettagli.