Saltar al contenido

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

Rust
// 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:

Rust
// 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.

Rust
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:

Rust
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.