Rusty Full Stack

El blog para los amantes de Rust, Ionic y Vuejs

Jaime Blandón
Jaime Blandón Desarrollador de software desde 2009, entusiasta de Rust, Vuejs y Ionic!. Fundador de este blog, espero que las publicaciones te sean de utilidad y si tienes comentarios para mejorar, son bienvenidos.

Bases De Datos Con Rust: Sqlite

Esta la primera de una serie de publicaciones que estaré realizando periódicamente sobre el manejo de base de datos con Rust, en esta ocasión abriremos el telón con una base de datos bastante sencilla de utilizar, Sqlite.

¿Qué son y para qué sirven las bases de datos?

La respuesta a esta pregunta es bastante amplia y sería casi imposible cubrirlo en una única publicación, pero sobre simplificando la respuesta, las bases de datos son herramientas que nos permiten tanto almacenar como manipular datos que queremos sean persistentes.

Imagínate que cada vez que quieras ingresar a tu red social favorita, sea necesario que crees una cuenta nueva porque no hay alguna forma de "recordar" que ya habías creado una cuenta anteriormente o que tengas que tus transacciones bancarias no puedan hacerse en línea porque no existiera un mecanismo para conocer el historial de todas tus transacciones 😱. La solución a estos y muchos más casos es utilizar bases datos.

Las bases de datos permiten a tus programas "recordar", acceder y transformar los datos de interés, aún si el programa se cierra o incluso si el servidor o computador donde se encuentra instalado es apagado.

¿Qué tipos de bases de datos existen y cómo saber cuál es la que más me conviene?

Existen muchos tipos de bases de datos y al igual que la pregunta anterior, sería imposible cubrir todas las opciones en una única publicación; para darte algunos ejemplos, existen bases de datos SQL, No-Sql, caches, etc.

Para hacer un poco más complejo el panorama, cada tipo de base de datos se puede derivar en uno ó más motores o sistemas de manejo de datos, por ejemplo, en el caso de las Sql, puedes encontrar MySql, Postgresdb, Oracle, Sqlite y muchos más 😵‍💫

Suena confuso y complicado no? pues, en realidad es al contrario, una vez decides el motor de base de datos que utilizar, generalmente, son bastante sencillos de utilizar.

Desafortunadamente no hay una fórmula mágica para poder decirte qué motor de base de datos es la más adecuada para ti, todo depende de tu caso de uso o necesidades. Lo mejor es investigar, tomar algún curso y documentarse sobre algún motor que te llame la atención.

El propósito de esta serie sobre base de datos con Rust es el mostrarte como poder conectarse y manipular los datos con los distintos motores de base de datos disponibles, sin embargo, intentaré mostrarte algunas características de los motores que vayamos aprendiendo a utilizar 🤓

¿Qué es Sqlite?

Según la propia definición que podemos encontrar en su sitio web sqlite.org, sqlite es:

"SQLite es una biblioteca en lenguaje C que implementa un motor de base de datos SQL pequeño, rápido, autónomo, de alta confiabilidad y con todas las funciones. SQLite es el motor de base de datos más utilizado en el mundo. SQLite está integrado en todos los teléfonos móviles y en la mayoría de las computadoras y viene incluido dentro de innumerables otras aplicaciones que la gente usa todos los días."

Si has utilizando anteriormente algún motor de base de datos como por ejemplo MySql, Mongodb, sqlserver, etc, sabrás que es necesario instalar una instancia o programa que corre en un servidor o computador mediante el cual un cliente o consumidor de los datos pueden abrir una conexión a esa instancia y poder utilizar dichos datos. También existen algunas instancias pre instaladas en la nube como AWS DynamoDB o Atlas, pero en esencia maneja el mismo patrón en el cual un cliente se conecta a dicha instancia.

En el diagrama anterior podemos ver como es una conexión a un motor de base de datos, mediante el cual uno o más dispositivos se conectan y utilizan los datos en el servidor, para ello es necesario que los dispositivos se conecten a una red que tenga acceso a dicha base de datos.

El esquema anterior permite, en el mejor de los escenarios, si el dispositivo 1 y 2 acceden a un mismo registro, entonces deberían de ver los mismos datos.

Pero ¿qué tal si queremos utilizar nuestra aplicación en modo off-line, es decir, sin acceso a una red ya sea porque nuestra aplicación no necesita conectarse a una red o porque queremos hacer un feature que nos permita seguir utilizando la app aún sin conexión aunque después se sincronicen los datos? Sqlite le da respuesta a estas y más preguntas porque es una base de datos que permite tener una instancia (librería sería lo más correcto) dentro de nuestro dispositivo y utilizar los datos como si nos estamos conectando a un servidor remoto, pero en realidad es el almacenamiento local de nuestro dispositivo 🤩. Más o menos el esquema de sqlite sería el siguiente.

En el diagrama anterior, cada dispositivo tiene una instancia de Sqlite instalado, por lo que no es necesario utilizar una red y conectarse a un servidor central, es decir, que todos los datos viven en el dispositivo.

Según el diagrama anterior los datos que tiene almacenados el dispositivo 1, pueden ser totalmente diferentes a los que tiene el dispositivo 2. Esto, como lo comentaba al inicio de la publicación no necesariamente es malo, todo depende de nuestro caso de uso, por ejemplo si nuestra app funciona en modo stand-alone, es decir, que no requiere enviar datos a un servidor central, o también si queremos ofrecer a nuestros usuarios un "feature" en el cual, si nos quedamos sin conexión, la app sigue funcionando sin problema y que cuando se restablezca la conexión, entonces sincronice con el servidor central.

No es parte del alcance de esta publicación realizar una tarea de sincronización de datos de un esquema off-line a un servidor central cuando recupere conexión, pero si te gustaría ver un ejemplo utilizando Rust déjalo en la caja de comentarios al final de este artículo o en nuestra página de contacto! 😎

¿Cómo utilizar Sqlite con Rust?

Usualmente Sqlite no require instalación, ya debería de ser parte de tu sistema operativo, pero por si algo raro ha pasado y no tienes la librería instalada, te dejo el enlace de descarga, pero primero intenta seguir el ejemplo sin instalar nada porque es posible que ya lo tengas:

Enlace de descarga (solamente casos especiales).

Algo que no te había comentado es que Sqlite se puede usar como una base de datos relacional, si es la primera vez que escuchas este término, las bases de datos relacionales almacenan datos en algo llamado "Tablas", las cuales pueden imaginarse como una cuadrícula en el que cada columna se llama "campo" y cada fila se llama registro.

⚠️ ADVERTENCIA IMPORTANTE ⚠️: El ejemplo siguiente no constituye la mejor forma de diseñar una base de datos, ni debe tomarse como algo escrito en piedra. No es el propósito de este artículo explicar sobre reglas de normalización ni otros conceptos importantes en el diseño de base de datos. El ejemplo anterior es únicamente la forma más básica que se me ha ocurrido de explicar el concepto de base de datos relacionales, si no tienes experiencia diseñando base de datos, te aconsejo muchísimo tomar un curso especializado sobre su diseño 😇.

El siguiente ejemplo es de una "Tabla" con todos los estudiantes de una universidad:

En el ejemplo anterior nuestra tabla tiene 3 "campos" los cuales son código, nombre y carrera.

La parte de relacional, es cuando podemos asociar cada registro de una tabla "maestra" a un "detalle", esta relación o referencia, se realiza utilizando algún campo que sea único en nuestra tabla maestra, como por ejemplo el código de estudiante que debería de ser único para cada estudiante. En nuestro ejemplo anterior podemos crear un "detalle" de todas las materias cursadas por un estudiante y su nota final, la "Tabla" de notas podría verse de la siguiente manera:

En el ejemplo anterior podemos ver algunas cosas curiosas, como por ejemplo que esta tabla tiene una columna ID la cual es una secuencia de números, otra cosa interesante es que si ves las filas 1 y 2 verás que el código de estudiante se repite, recuerda que en la tabla de estudiante el código 1234 pertenece a un estudiante de nombre "Rusty" 😉, esto quiere decir que cada uno de esos registros está "relacionado" en base a una llave foránea (de la tabla maestra estudiante) basada en el código de estudiante.

Las primeras dos filas las podemos interpretad como:

FILA 1:

El estudiante "Rusty" obtuvo un 100 de nota final en la materia Programación I

FILA 2:

El estudiante "Rusty" obtuve un 90 de nota final en la materia Base de Datos.

Tu puedes inferir la interpretación de las siguientes columnas en base a los ejemplos anteriores 😊

La relación entra la tabla de estudiantes y la tabla de notas se puede visualizar de la siguiente manera (PK = primary key o llave única, FK = Foreign Key o llave foránea)

⚠️ RECORDATORIO DE ADVERTENCIA IMPORTANTE ⚠️: El ejemplo anterior no constituye la mejor forma de diseñar una base de datos, ni debe tomarse como algo escrito en piedra. No es el propósito de este artículo explicar sobre reglas de normalización ni otros conceptos importantes en el diseño de base de datos. El ejemplo anterior es únicamente la forma más básica que se me ha ocurrido de explicar el concepto de base de datos relacionales, si no tienes experiencia diseñando base de datos, te aconsejo muchísimo tomar un curso especializado sobre su diseño 😇.

Como te lo comentaba al inicio de la publicación, el alcance de esta publicación es enseñarte a conectarte y utilizar las bases de datos con Rust, iniciemos!

Hay varias formas de trabajar con una base de datos en Rust, por ejemplo, un método bastante popular es utilizar Object-Relational Mapping (ORM por sus siglas en inglés), algunas de los crates más populares son diesel y seaORM. Ambos crates facilitan mucho el trabajo con base de datos, si te gustaría que subiera un artículo sobre su uso déjalo en los comentarios al final de esta publicación 😎

En esta ocasión me gustaría mostrarte cómo utilizar las bases de datos con Rust de forma más básica.

Vamos a crear un ejemplo simple en el cual crearemos una base de datos con Sqlite, en dicha base de datos crearemos una tabla en la cual almacenaremos los datos de personas y luego vamos a consultarlas para verificar que estemos guardando los datos correctamente.

Empecemos creando un proyecto nuevo al que llamaremos sqlite_con_rust, en una terminal ejecuta el siguiente comando:

cargo new sqlite_con_rust

cd sqlite_con_rust

(Puedes ver el código completo desde github dando click acá)

Ahora vamos a agregar dos crates a nuestro proyecto, el primero de ellos será chrono para poder manejar formatos de fechas de forma sencilla con Rust, el otro crate será rusqlite, el cual nos permitirá interactuar con Sqlite, a este último create le activaremos el feature "bundle".

Recuerda que puedes agregar los crates utilizando cargo edit o desde tu archivo Cargo.toml el cual debería de ser como el siguiente:

        
        
            
// Cargo.toml
[package]
name = "sqlite_con_rust"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
chrono = "0.4.26"
rusqlite = { version = "0.29.0", features = ["bundled"] }

        
        
    

Ahora con nuestros crates disponibles vamos a crear nuestra base de datos local utilizando sqlite, lo primero que vamos a hacer es agregar el siguiente código el cual abre una conexión a sqlite indicando que queremos utilizar la librería y al mismo tiempo que queremos crear una base de datos a la que llamaremos "my-db.db3", modifiquemos nuestro archivo src/main.rs con el siguiente código:

        
        
            
// src/main.rs
use rusqlite::{Connection, Result};
fn main() -> Result<()> {
    let conexion = Connection::open("my-db.db3").expect("error conectando a sqlite");
    Ok(())
}


        
        
    

En la primera línea estamos indicando que queremos utilizar rusqlite sobre todo Connection y Result, este último porque la conexión puede ser exitosa o no.

En la línea:

        
        
            

let conexion = Connection::open("my-db.db3").expect("error conectando a sqlite");
        
        
    

Estamos indicando que queremos "abrir una conexión" con sqlite y crear una base de datos de nombre "my-db.db3" tu puedes llamarle como mejor te parezca, la extensión .db3 nos indica que se trata de una base de datos sqlite; si la conexión falla entonces mostrara un mensaje "error conectando a sqlite".

Si ejecutamos el código actual ejecutando en nuestra terminal el comando:

cargo run

Veremos el siguiente resultado en pantalla:

Por el momento ignoraremos el warning, si ahora miras los archivos en tu carpeta del proyecto verás que un archivo llamado "my-db.db3" debería de existir:

Aunque nuestra base de datos ha sido creada localmente, todavía se encuentra vacía. Como pudiste leer anteriormente, las bases de datos necesitan tener unas estructuras llamadas "Tablas" para poder guardar datos, para nuestro ejemplo crearemos una llama PERSONAS en la cual vamos a indicar que queremos crear las columnas id, nombre y la fecha de creación del registro.

Para interactuar con Sqlite (sin utilizar ORM) es necesario conocer de un lenguaje de consulta llamado SQL, si no estás familiarizado con SQL, te recomiendo tomar algún curso o leer su documentación.

Modifiquemos un poco nuestro código en src/main.rs para crear nuestra tabla.

        
        
            
// src/main.rs
use rusqlite::{Connection, Result};
fn main() -> Result<()> {
    let conexion = Connection::open("my-db.db3").expect("error conectando a sqlite");

    conexion.execute(
            "CREATE TABLE IF NOT EXISTS PERSONA(
            id INTEGER PRIMARY KEY,
            nombre TEXT NOT NULL,
            fecha TEXT NULL,
            data BLOB
        )",
            (),
        )
        .expect("Error Creando La Tabla PERSONA");

    Ok(())
}


        
        
    

En el código anterior vemos que estamos pidiéndole a nuestra conexion, que ejecute (execute) un script SQL. El script únicamente le está diciendo a Sqlite:

"crea una tabla, en caso no exista, y usa el nombre PERSONA, también agrega un campo de nombre id el cual será un INTEGER y nuestra primary key, también un campo nombre de tipo texto y que no puede estar vacío, también crea otro campo de nombre fecha que será de tipo text, pero puede estar vacío y finalmente crea un campo de nombre data, que puede contener cualquier tipo de información y que se almacenará como un Blob y que también puede estar vacío".

Por el momento solamente estamos creando la tabla, pero lo que queremos es guardar algo de información en ella, así que ahora vamos a crear el mecanismo para poder guardar los datos.

Para guardar datos bastaría con ejecutar el SQL indicado INSERT, sin embargo, una práctica que a mi me gusta ( y que los ORM facilitan mucho) es crear una estructura que representa los datos que deseo guardar o consultar de una tabla.

Ahora agregamos en nuestro código una nuestra estructura Persona, que representa los datos, en nuestro archivo src/main.rs coloca el siguiente código:

        
        
            
// src/main.rs
use chrono::{DateTime, Utc};
use rusqlite::{Connection, Result};

struct Persona {
    id: i32,
    nombre: String,
    data: Option<Vec<u8>>,
    fecha: Option<DateTime<Utc>>,
}

fn main() -> Result<()> {
    let conexion = Connection::open("my-db.db3").expect("error conectando a sqlite");

    conexion
        .execute(
            "CREATE TABLE IF NOT EXISTS PERSONA(
            id INTEGER PRIMARY KEY,
            nombre TEXT NOT NULL,
            fecha TEXT NULL,
            data BLOB
        )",
            (),
        )
        .expect("Error Creando La Tabla PERSONA");

    Ok(())
}

        
        
    

Algo importante a resaltar de nuestra estructura y la tabla es que en nuestra tabla hemos definido el campo fecha como TEXT, pero nuestra estructura lo ha definido como:

fecha: Option<DateTime<Utc>>

Sqlite tiene la limitante que no maneja un tipo de datos "Date" o "DateTime" como otros motores como Postgresql, MySql, etc, las fechas se manejan como tipo Text, pero si nosotros luego de extraer los datos, queremos manipular su fecha de creación, sería importante obtenerla como lo que queremos representar en Rust el cual sería un DateTime. En nuestro caso utilizamos Utc como formato deseado porque luego es mucho más flexible de transformar de diferentes zonas, sobre todo si nuestra aplicación es utilizada en distintos husos horarios.

También otra cosa llamar la atención es el campo data, el cual en nuestra tabla la hemos definido como Blob, pero en nuestra estructura lo hemos colocado como:

data: Option<Vec<u8>>

Esto es debido a que los tipos Blob, usualmente se representan como bytes, y eso en Rust, se expresa como un vector de u8.

Ahora vamos a crear una función en la cual le pasaremos la referencia de un objeto Persona y la referencia de nuestra conexión, la función será la responsable de guardar una "persona" en nuestra base de datos, también será la encargada de colocar la fecha actual en Utc (si quieres aprender sobre manejo de fechas con Rust déjalo en los comentarios al final de la publicación 😎)

Modifiquemos nuestro archivo src/main.rs para que se vea de la siguiente forma:

        
        
            
// src/main.rs
use chrono::{DateTime, Utc};
use rusqlite::{Connection, Result};

struct Persona {
    id: i32,
    nombre: String,
    data: Option<Vec<u8>>,
    fecha: Option<DateTime<Utc>>,
}

fn guardar_persona(persona: &Persona, conexion: &Connection) -> Result<()> {
    conexion
        .execute(
            "INSERT INTO PERSONA (nombre, data, fecha) VALUES (?1, ?2, ?3)",
            (&persona.nombre, &persona.data, Utc::now().to_rfc3339()),
        )
        .expect("Error haciendo el registro de una persona");

    Ok(())
}

fn main() -> Result<()> {
    let conexion = Connection::open("my-db.db3").expect("error conectando a sqlite");

    conexion
        .execute(
            "CREATE TABLE IF NOT EXISTS PERSONA(
            id INTEGER PRIMARY KEY,
            nombre TEXT NOT NULL,
            fecha TEXT NULL,
            data BLOB
        )",
            (),
        )
        .expect("Error Creando La Tabla PERSONA");

    Ok(())
}

        
        
    

En nuestra función para guardar personas es importante resaltar la forma en que se ejecuta el SQL INSERT:

        
        
            

"INSERT INTO PERSONA (nombre, data, fecha) VALUES (?1, ?2, ?3)",
(&persona.nombre, &persona.data, Utc::now().to_rfc3339()),
        
        
    

si lo notas, el id no se está utilizando y esto es porque Sqlite puede crearlo por ti a menos que indiques lo contrario, en nuestro caso lo creará como un número secuencial.

También "VALUES (?1, ?2, ?3)" indica que se pasarán los valores a colocar de forma ordenada según se define en "PERSONA (nombre, data, fecha)", es decir primero se debe pasar el nombre, luego data y por último fecha.

Los parámetros son pasados en el orden esperado en (nota que la fecha se pasa como la hora en UTC actual y luego se transforma a string:

(&persona.nombre, &persona.data, Utc::now().to_rfc3339())

Ahora estamos en una buena posición para guardar nuestros registros en una base de datos, creemos algunos de ellos llamando a nuestra nueva función, para ello modifiquemos nuestro archivo src/main.rs colocando el siguiente código: 

        
        
            
// src/main.rs
use chrono::{DateTime, Utc};
use rusqlite::{Connection, Result};

struct Persona {
    id: i32,
    nombre: String,
    data: Option<Vec<u8>>,
    fecha: Option<DateTime<Utc>>,
}

fn guardar_persona(persona: &Persona, conexion: &Connection) -> Result<()> {
    conexion
        .execute(
            "INSERT INTO PERSONA (nombre, data, fecha) VALUES (?1, ?2, ?3)",
            (&persona.nombre, &persona.data, Utc::now().to_rfc3339()),
        )
        .expect("Error haciendo el registro de una persona");

    Ok(())
}

fn main() -> Result<()> {
    let conexion = Connection::open("my-db.db3").expect("error conectando a sqlite");

    conexion
        .execute(
            "CREATE TABLE IF NOT EXISTS PERSONA(
            id INTEGER PRIMARY KEY,
            nombre TEXT NOT NULL,
            fecha TEXT NULL,
            data BLOB
        )",
            (),
        )
        .expect("Error Creando La Tabla PERSONA");

    guardar_persona(
        &Persona {
            id: 0, // sera colocado automaticamente
            nombre: "Rusty".to_string(),
            data: None,  // Este valor definimos que puede ser vacio (None)
            fecha: None, // Sera colocado automaticamente
        },
        &conexion,
    )
    .expect("error guardando la persona");

    guardar_persona(
        &Persona {
            id: 0, // sera colocado automaticamente
            nombre: "Full Stack".to_string(),
            data: Some(vec![1, 2, 3, 5]), // Este valor definimos que puede ser vacio (None)
            fecha: None,                  // Sera colocado automaticamente
        },
        &conexion,
    )
    .expect("error guardando la persona");

    Ok(())
}

        
        
    

Ok, hasta este momento tenemos una forma de guardar datos, pero si ejecutamos nuestro programa sin una forma de visualizarlos no podríamos saber si nuestro programa está funcionando correctamente, agreguemos una forma de visualizar los datos, para ello vamos a agregar un bloque de código para hacer un query o consultar nuestros datos, para ello podemos modificar nuestro archivo src/main.rs con el siguiente código:

        
        
            
// src/main.rs
use chrono::{DateTime, Utc};
use rusqlite::{Connection, Result};

struct Persona {
    id: i32,
    nombre: String,
    data: Option<Vec<u8>>,
    fecha: Option<DateTime<Utc>>,
}

fn guardar_persona(persona: &Persona, conexion: &Connection) -> Result<()> {
    conexion
        .execute(
            "INSERT INTO PERSONA (nombre, data, fecha) VALUES (?1, ?2, ?3)",
            (&persona.nombre, &persona.data, Utc::now().to_rfc3339()),
        )
        .expect("Error haciendo el registro de una persona");

    Ok(())
}

fn main() -> Result<()> {
    let conexion = Connection::open("my-db.db3").expect("error conectando a sqlite");

    conexion
        .execute(
            "CREATE TABLE IF NOT EXISTS PERSONA(
            id INTEGER PRIMARY KEY,
            nombre TEXT NOT NULL,
            fecha TEXT NULL,
            data BLOB
        )",
            (),
        )
        .expect("Error Creando La Tabla PERSONA");

    guardar_persona(
        &Persona {
            id: 0, // sera colocado automaticamente
            nombre: "Rusty".to_string(),
            data: None,  // Este valor definimos que puede ser vacio (None)
            fecha: None, // Sera colocado automaticamente
        },
        &conexion,
    )
    .expect("error guardando la persona");

    guardar_persona(
        &Persona {
            id: 0, // sera colocado automaticamente
            nombre: "Full Stack".to_string(),
            data: Some(vec![1, 2, 3, 5]), // Este valor definimos que puede ser vacio (None)
            fecha: None,                  // Sera colocado automaticamente
        },
        &conexion,
    )
    .expect("error guardando la persona");

    // Leyendo los datos
    let mut statement = conexion
        .prepare("SELECT id, nombre, data, fecha FROM PERSONA")
        .expect("No es posible crear el statement");

    let iterador_persona = statement
        .query_map([], |registro| {
            // la fecha del registro de sqlite es text, habra que pasarla a DateTime<Utc>
            // fecha es el cuarto campo del SELECT indice 3
            let fecha_del_registro: String = registro.get(3)?;
            Ok(Persona {
                id: registro.get(0)?,     // id es el primer campo del SELECT indice 0
                nombre: registro.get(1)?, // nombre es el segundo campo del SELECT indice 1
                data: registro.get(2)?,   // data es el tercer campo del SELECT indice 2
                fecha: Some(fecha_del_registro.parse::<DateTime<Utc>>().unwrap()),
            })
        })
        .expect("No fue posible obtener las personas");

    for item_persona in iterador_persona {
        let persona: Persona = item_persona.unwrap();
        println!("--------------------------------------------------------");
        println!("ID: {}", persona.id);
        println!("Nombre: {}", persona.nombre);
        // data puede ser vacio, para evitar un error le pondremos un valor por defecto
        // si viene como None
        println!("Data:{:?}", persona.data.unwrap_or(vec![]));
        println!(
            "Fecha del registro: {}",
            persona.fecha.unwrap().to_rfc3339()
        );
        println!("--------------------------------------------------------");
    }

    Ok(())
}

        
        
    

En el código anterior agregamos dos bloques importantes, el primero de ellos es crear un statement, el cual nos permite ejecutar una consulta a nuestros datos, el statement nos permitirá hacer un mapeo de los registros retornados de la base de datos y colocarlos en un iterador de personas (si quieres saber más sobre iteradores puedes dar click acá)

        
        
            
// Leyendo los datos
    let mut statement = conexion
        .prepare("SELECT id, nombre, data, fecha FROM PERSONA")
        .expect("No es posible crear el statement");

    let iterador_persona = statement
        .query_map([], |registro| {
            // la fecha del registro de sqlite es text, habra que pasarla a DateTime<Utc>
            // fecha es el cuarto campo del SELECT indice 3
            let fecha_del_registro: String = registro.get(3)?;
            Ok(Persona {
                id: registro.get(0)?,     // id es el primer campo del SELECT indice 0
                nombre: registro.get(1)?, // nombre es el segundo campo del SELECT indice 1
                data: registro.get(2)?,   // data es el tercer campo del SELECT indice 2
                fecha: Some(fecha_del_registro.parse::<DateTime<Utc>>().unwrap()),
            })
        })
        .expect("No fue posible obtener las personas");
        
        
    

La segunda parte es donde propiamente recorremos el iterador y mostramos en pantalla los resultados:

        
        
            

for item_persona in iterador_persona {
        let persona: Persona = item_persona.unwrap();
        println!("--------------------------------------------------------");
        println!("ID: {}", persona.id);
        println!("Nombre: {}", persona.nombre);
        // data puede ser vacio, para evitar un error le pondremos un valor por defecto
        // si viene como None
        println!("Data:{:?}", persona.data.unwrap_or(vec![]));
        println!(
            "Fecha del registro: {}",
            persona.fecha.unwrap().to_rfc3339()
        );
        println!("--------------------------------------------------------");
    }

        
        
    

Si ahora ejecutamos nuestro programa colocando el siguiente comando en una terminal:

cargo run

deberíamos de ver el siguiente output (recuerda que la fecha variará dependiendo de cuando ejecutes el programa):

Genial, estamos guardando datos y leyéndolos desde Sqlite!! 🥳

Ahora verifiquemos que nuestros datos son realmente persistentes aún si nuestro programa ha finalizado de ejecutarse, comentemos en nuestro archivo src/main.rs las líneas de código que registran las personas, recuerda NO BORRAR tu archivo my-db.db3, ya que borrarlo es el equivalente a perder tu base de datos.

        
        
            
// src/main.rs
use chrono::{DateTime, Utc};
use rusqlite::{Connection, Result};

struct Persona {
    id: i32,
    nombre: String,
    data: Option<Vec<u8>>,
    fecha: Option<DateTime<Utc>>,
}

fn guardar_persona(persona: &Persona, conexion: &Connection) -> Result<()> {
    conexion
        .execute(
            "INSERT INTO PERSONA (nombre, data, fecha) VALUES (?1, ?2, ?3)",
            (&persona.nombre, &persona.data, Utc::now().to_rfc3339()),
        )
        .expect("Error haciendo el registro de una persona");

    Ok(())
}

fn main() -> Result<()> {
    let conexion = Connection::open("my-db.db3").expect("error conectando a sqlite");

    conexion
        .execute(
            "CREATE TABLE IF NOT EXISTS PERSONA(
            id INTEGER PRIMARY KEY,
            nombre TEXT NOT NULL,
            fecha TEXT NULL,
            data BLOB
        )",
            (),
        )
        .expect("Error Creando La Tabla PERSONA");

    // guardar_persona(
    //     &Persona {
    //         id: 0, // sera colocado automaticamente
    //         nombre: "Rusty".to_string(),
    //         data: None,  // Este valor definimos que puede ser vacio (None)
    //         fecha: None, // Sera colocado automaticamente
    //     },
    //     &conexion,
    // )
    // .expect("error guardando la persona");

    // guardar_persona(
    //     &Persona {
    //         id: 0, // sera colocado automaticamente
    //         nombre: "Full Stack".to_string(),
    //         data: Some(vec![1, 2, 3, 5]), // Este valor definimos que puede ser vacio (None)
    //         fecha: None,                  // Sera colocado automaticamente
    //     },
    //     &conexion,
    // )
    // .expect("error guardando la persona");

    // Leyendo los datos
    let mut statement = conexion
        .prepare("SELECT id, nombre, data, fecha FROM PERSONA")
        .expect("No es posible crear el statement");

    let iterador_persona = statement
        .query_map([], |registro| {
            // la fecha del registro de sqlite es text, habra que pasarla a DateTime<Utc>
            // fecha es el cuarto campo del SELECT indice 3
            let fecha_del_registro: String = registro.get(3)?;
            Ok(Persona {
                id: registro.get(0)?,     // id es el primer campo del SELECT indice 0
                nombre: registro.get(1)?, // nombre es el segundo campo del SELECT indice 1
                data: registro.get(2)?,   // data es el tercer campo del SELECT indice 2
                fecha: Some(fecha_del_registro.parse::<DateTime<Utc>>().unwrap()),
            })
        })
        .expect("No fue posible obtener las personas");

    for item_persona in iterador_persona {
        let persona: Persona = item_persona.unwrap();
        println!("--------------------------------------------------------");
        println!("ID: {}", persona.id);
        println!("Nombre: {}", persona.nombre);
        // data puede ser vacio, para evitar un error le pondremos un valor por defecto
        // si viene como None
        println!("Data:{:?}", persona.data.unwrap_or(vec![]));
        println!(
            "Fecha del registro: {}",
            persona.fecha.unwrap().to_rfc3339()
        );
        println!("--------------------------------------------------------");
    }

    Ok(())
}

        
        
    

En el código anterior hemos comentado las operaciones de registro, es decir, no estamos guardando nada, solamente leyendo lo que nuestro Sqlite ya tiene registrado.

Si ahora ejecutamos en nuestra terminal:

cargo run

Veremos algunos warnings que podemos ignorar (por los comentarios hechos en el código) pero el resultado son los mismos datos que habíamos guardado anteriormente, es decir, nuestros datos están siendo persistentes 😉

Ahora Agreguemos dos registros más, una persona de nombre Maria y la otra de nombre Juan, podemos quitar los comentarios a la parte de guardado de registros y colocar ahí los nuevos datos en nuestro archivo src/main.rs:

        
        
            
// src/main.rs
use chrono::{DateTime, Utc};
use rusqlite::{Connection, Result};

struct Persona {
    id: i32,
    nombre: String,
    data: Option<Vec<u8>>,
    fecha: Option<DateTime<Utc>>,
}

fn guardar_persona(persona: &Persona, conexion: &Connection) -> Result<()> {
    conexion
        .execute(
            "INSERT INTO PERSONA (nombre, data, fecha) VALUES (?1, ?2, ?3)",
            (&persona.nombre, &persona.data, Utc::now().to_rfc3339()),
        )
        .expect("Error haciendo el registro de una persona");

    Ok(())
}

fn main() -> Result<()> {
    let conexion = Connection::open("my-db.db3").expect("error conectando a sqlite");

    conexion
        .execute(
            "CREATE TABLE IF NOT EXISTS PERSONA(
            id INTEGER PRIMARY KEY,
            nombre TEXT NOT NULL,
            fecha TEXT NULL,
            data BLOB
        )",
            (),
        )
        .expect("Error Creando La Tabla PERSONA");

    guardar_persona(
        &Persona {
            id: 0, // sera colocado automaticamente
            nombre: "Maria".to_string(),
            data: Some(vec![3, 4, 1, 2]), // Este valor definimos que puede ser vacio (None)
            fecha: None,                  // Sera colocado automaticamente
        },
        &conexion,
    )
    .expect("error guardando la persona");

    guardar_persona(
        &Persona {
            id: 0, // sera colocado automaticamente
            nombre: "Juan".to_string(),
            data: Some(vec![10, 11]), // Este valor definimos que puede ser vacio (None)
            fecha: None,              // Sera colocado automaticamente
        },
        &conexion,
    )
    .expect("error guardando la persona");

    // Leyendo los datos
    let mut statement = conexion
        .prepare("SELECT id, nombre, data, fecha FROM PERSONA")
        .expect("No es posible crear el statement");

    let iterador_persona = statement
        .query_map([], |registro| {
            // la fecha del registro de sqlite es text, habra que pasarla a DateTime<Utc>
            // fecha es el cuarto campo del SELECT indice 3
            let fecha_del_registro: String = registro.get(3)?;
            Ok(Persona {
                id: registro.get(0)?,     // id es el primer campo del SELECT indice 0
                nombre: registro.get(1)?, // nombre es el segundo campo del SELECT indice 1
                data: registro.get(2)?,   // data es el tercer campo del SELECT indice 2
                fecha: Some(fecha_del_registro.parse::<DateTime<Utc>>().unwrap()),
            })
        })
        .expect("No fue posible obtener las personas");

    for item_persona in iterador_persona {
        let persona: Persona = item_persona.unwrap();
        println!("--------------------------------------------------------");
        println!("ID: {}", persona.id);
        println!("Nombre: {}", persona.nombre);
        // data puede ser vacio, para evitar un error le pondremos un valor por defecto
        // si viene como None
        println!("Data:{:?}", persona.data.unwrap_or(vec![]));
        println!(
            "Fecha del registro: {}",
            persona.fecha.unwrap().to_rfc3339()
        );
        println!("--------------------------------------------------------");
    }

    Ok(())
}

        
        
    

Si corremos de nuevo nuestro programa con:

cargo run

Ahora veremos que en nuestro Sqlite hemos agregado los nuevos datos y que también los anteriores se han mantenido persistentes 😃

Ahora hemos aprendido a conectarnos a Sqlite con Rust!

(Puedes ver el código completo desde github dando click acá)

El tema de las bases de datos es muy amplio, si no tienes experiencia utilizándolas, te animo mucho a aprender más sobre ellas, verás todos los beneficios como los acá mencionados y mucho más 😊

En nuestro ejemplo hemos realizado todo a nivel del programa sin ninguna interacción de usuario, sería interesando hacer el ejemplo más dinámico agregando alguna interfaz de escritorio, web o app móvil, deja en la caja de comentarios al final de este artículo si te interesaría le creemos una interfaz gráfica a nuestro ejemplo con más operaciones como editar o borrar, además de otros motores de bases de datos te gustaría aprender a utilizar con Rust 😏

Si esta publicación te ha sido de utilidad, compártela con tus amigos y en las redes sociales. No te olvides de seguirme en X (twitter) ni de suscribirte al canal de youtube.

Si esta información te ha sido de utilidad y te gustaría apoyar a la creación de más artículos y contenidos puedes convertirte en mecenas desde patreon te estaré sumamente agradecido 🦀

println!("hasta la próxima!");


 Utilizamos cookies propias y de terceros para mejorar tu experiencia, mostrar publicidad y análisis de navegación, puedes encontrar el detalle en nuestra Política de Cookies