Consultar sin hacerte daño
Parámetros en vez de concatenación —sí, también en una base local—, sentencias preparadas, transacciones y el mapeo de filas a tipos de Rust.
Parámetros, siempre
// mal: la cadena se pega dentro de la consultalet sql = format!("SELECT * FROM proyecto WHERE nombre = '{}'", nombre);// bien: el valor viaja aparte y nunca se interpreta como consultalet mut st = conn.prepare("SELECT id, nombre FROM proyecto WHERE nombre = ?1")?;let filas = st.query_map([&nombre], |f| Ok(Proyecto { id: f.get(0)?, nombre: f.get(1)?,}))?;Y la objeción de «pero es una base local, no hay atacante» merece respuesta porque es la que hace que la gente lo deje pasar:
La primera fila es la que convence a todo el mundo: no hace falta un atacante, basta un apellido con apóstrofo. La segunda es por qué además es un problema de seguridad.
Sentencias preparadas
Preparar una consulta la compila una vez y luego se ejecuta muchas. En un bucle, la diferencia se nota:
// preparar fuera del bucle, ejecutar dentrolet mut st = conn.prepare("INSERT INTO evento (proyecto_id, tipo, en) VALUES (?1, ?2, ?3)")?;for e in eventos { st.execute((e.proyecto_id, &e.tipo, e.en))?;}Preparar dentro del bucle es el error de rendimiento más común con SQLite, y es invisible hasta que hay volumen.
Transacciones
Y aquí está el número que sorprende: insertar mil filas sin transacción puede tardar cien veces más que con ella.
La razón es que, sin transacción explícita, cada sentencia es su propia transacción y el motor tiene que asegurarse de que llega al disco. Mil veces. Con una transacción envolvente, es una sola vez al final.
let tx = conn.transaction()?;{ let mut st = tx.prepare("INSERT INTO evento (...) VALUES (?1, ?2, ?3)")?; for e in eventos { st.execute((...))?; }}tx.commit()?;Dos usos, los dos importantes:
- Por velocidad, en cualquier bucle de escritura.
- Y por corrección, que es la razón del nivel 40.2: si algo falla a mitad, no se aplica nada. Es la atomicidad entre partes que los ficheros no daban.
Y un aviso de la fase IV: una transacción larga mantiene el cerrojo tomado. Si vas a insertar cien mil filas, trocéalo en transacciones de unos miles y suelta entre medias — nivel 41.2.
Mapear filas
El código repetitivo que conviene centralizar desde el principio:
impl Proyecto { // una sola definición de cómo se lee un proyecto de una fila fn de_fila(f: &rusqlite::Row) -> rusqlite::Result<Self> { Ok(Self { id: f.get("id")?, nombre: f.get("nombre")?, creado_en: f.get("creado_en")?, }) }}Leer por nombre de columna y no por índice es lo que evita el fallo silencioso cuando alguien añade una columna en medio de la consulta. Cuesta lo mismo y no se rompe.