Inserción de Datos en Bases de Datos con Diesel

Clase 6 de 21Curso de Backend con Rust: Bases de Datos, Controladores y Templates

Resumen

Es momento de crear el primer registro en la base de datos con Diesel y Rust. Pero antes, tomémonos un momento y hagamos un esfuerzo de comprender algunos conceptos algo complicados del lenguaje.

Modularización, alcance y visibilidad en Rust

Rust utiliza a su manera conceptos que muchos otros lenguajes de programación también poseen, como la visibilidad de una función (public/private), el alcance (scope) y, al tratarse de un lenguaje de bajo nivel, el manejo de memoria por parte del desarrollador.

Pub y Mod

Este también proporciona un poderoso sistema de Módulos para jerarquizar, dividir el código en unidades lógicas y manejar la visibilidad entre ellos. Dentro de un módulo, puedes tener otros módulos, estructuras, funciones, etc.

Por defecto, todo lo que declares dentro de un módulo es privado y solo puede utilizarse dentro de este. Pero puedes permitir la utilización por fuera del mismo, volviendo pública la función, la estructura, o todo el módulo. Declara un nuevo módulo con la palabra reservada mod, y hazlo público con pub.

Super y Self

Dentro de un módulo, para acceder a estructuras o funciones de otro, vamos a utilizar las palabras reservadas super y self. En pocas palabras, con self accedes a los ítems dentro del módulo actual donde te encuentras y con super accedes a módulos exteriores. Veamos un ejemplo:

// Función disponible en todo el scope de main.rs
fn my_function() {
    println!("Llamado número 4");
}

// Módulo al cual se puede hacer referencia con 'super' desde otro módulo
mod another_module {
    pub fn my_function() {
        println!("Llamado número 5");
    }
}

// Módulo principal de la app
mod my_module {

    // Función disponible en todo el scope de my_module
    fn my_function() {
        println!("Llamado número 2");
    }
    
    // Submódulo de my_module, lo utilizamos con 'self' dentro de my_module
    mod another_module {
        pub fn my_function() {
            println!("Llamado número 3");
        }
    }
    
    pub fn start_calls() {

        println!("Llamado número 1");
        
        // Llamamos a my_function, el 'self' aquí es opcional pero evita ambigüedades y problemas si otra función tiene el mismo nombre
        self::my_function();
        
        // Llamamos a my_function perteneciente a another_module y DENTRO de my_module
        self::another_module::my_function();
        
        // Llamamos a my_function disponible en todo el scope con 'super'
        super::my_function();
        
        // Llamamos my_function perteneciente a another_module y FUERA de my_module con 'super'
        super::another_module::my_function();

        // Otra forma de utilizar una función de un módulo externo
        {
            // Utilizamos la palabra reservada 'crate' para invocar librerías externas y darles un nombre con 'as'
            use crate::another_module::my_function as new_name_function;
            new_name_function();
        }
    }
}

fn main() {
    my_module::start_calls();
}

Aquí puedes observar un ejemplo bastante amplio de utilización de super y self. No dejes de ponerlo en funcionamiento en tu intérprete de Rust en tu computador para observar los llamados a las funciones entre módulo y dentro del mismo.

Pasaje por valor y referencia

Comencemos hablando del pasaje por valor y pasaje por referencia de una variable. El pasaje por valor, como su nombre lo indica, compartes solo el valor de una variable a una función o como asignación a otra variable, estás creando una copia de la misma. El pasaje por referencia, lo que compartes, no es solo el valor, también la dirección de memoria.

Al pasar una variable por valor, la variable original seguirá existiendo y no puedes modificar su valor. Al hacerlo por referencia, puedes alterar su valor e impactará en todos los lugares donde esa variable se utilice.

Por defecto, las variables se comparten por valor. Si quieres hacerlo por referencia, emplea el & delante del nombre de la variable.

Lifetime

Habiendo entendido los tipos de pasajes de variables, y también habiendo hablado de los scopes, ambos son conceptos similares, pero no lo mismo. Llega el concepto de Lifetime que tiene un poquito de ambos.

La “vida útil” de una variable se termina. Las variables pueden destruirse liberando espacio en memoria. Las variables se almacenan dentro de un scope y, cuando este finaliza, las variables declaradas dentro desaparecen.

Considera el siguiente ejemplo (Spoiler: No compilará):

fn main() {
  // this code sample does *not* compile
  {
      let x;
      {                           // Creamos nuevo scope
          let y = 42;
          x = &y;                 // Asignamos por referencia 'y' a 'x'
      }                           // Cerramos scope, 'y' es destruido

      println!("El valor de 'x' es {}", x);
  }
}

Como la variable y es declarada dentro de un scope, la misma se destruirá al cerrarse este y como estamos haciendo un pasaje por referencia hacia x, esta variable no puede llevarse la referencia hacia fuera del scope. Puedes solucionarlo rápidamente haciendo un pasaje por valor de y.

Cuando realizas un pasaje por referencia, a veces debes indicarle explícitamente al compilador de Rust que dos variables están relacionadas y que su tiempo de vida tiene que estar alineado para evitar problemas en la compilación. De lo contrario, verás un error en tu código advirtiéndote que algo estás codificando mal

¿Recuerdas que Rust posee un “Modo Seguro” para evitar problemas en la codificación? Nos referimos a conceptos como el Lifetime que lo convierten en eso.

Para indicar el tiempo de vida útil de una variable se utiliza el apóstrofe '. Veamos un ejemplo sencillo de utilización:

fn main() {
    let mut num: i32 = 4;
    add_one(&mut num);
    println!("{}", num)
}

fn add_one<'a>(x: &'a mut i32) {
    *x += 1;
}

Aquí pasamos por referencia una variable y le sumamos uno. Observa que la función add_one() no necesita retornar nada, ya que el pasaje se realiza por referencia.

Conclusiones

Cuando hablamos de que Rust es un lenguaje de bajo nivel, nos referimos a estas cosas. Seguro te provoca incomodidad la expresión: fn add_one<'a>(x: &'a mut i32) {}. Es natural porque la sintaxis es muy bajo nivel, muchas palabras reservadas y caracteres especiales que por detrás están haciendo algo. No es muy intuitivo comprender ese código al instante.

Espero que, hasta aquí, al menos comprendas la teoría de lo que está sucediendo y con la práctica, comprender en profundidad la utilización de todos estos conceptos que Rust implementa.

Fuente:
Modules
Lifetimes

Cómo insertar registros en la BBDD

Luego de esta extensa explicación para comprender mejor la sintaxis de Rust, continuando con lo que nos compete. Insertar registros en una base de datos con Diesel y Rust.

Paso 1: Crea el module para insertar registros

Comienza creando en module.rs una nueva estructura que también utilizaremos para insertar posts en la BBDD.

// Importamos el esquema de la BBDD
use super::schema::posts;

// Macro para indicar que la estructura servirá que insert en la BBDD
#[derive(Insertable)]
#[table_name="posts"]
pub struct NewPost<'a> {
    pub title: &'a str,
    pub body: &'a str,
    pub slug: &'a str,
}

Observa que no hemos agregado el id a la estructura NewPost, ya que este se autogenera luego del insert.

Paso 2: Inserta el registro en la BBDD

// ...

fn main() {

    // Indicamos que vamos a utilizar el esquema de Posts y el modelo
    use self::models::{Post, NewPost};
    use self::schema::posts;
    use self::schema::posts::dsl::*;

    // Lectura de variables de entorno
    dotenv().ok();
    let db_url = env::var("DATABASE_URL").expect("La variable de entorno DATABASE_URL no existe.");

    // Conexión con la BBDD
    let conn = PgConnection::establish(&db_url).expect("No se ha podido establecer la conexión con la base de datos.");

    // Insertamos el primer registro en la BBDD
    let new_post = NewPost {
        title: "Mi primer post",
        body: "Lorem ipsum...",
        slug: "primer-post",
    };
    diesel::insert_into(posts::table).values(new_post).get_result::<Post>(&conn).expect("Falló el insert en la BBDD");

    // Realizamos la consulta equivalente a: SELECT * FROM posts;
    let posts_result = posts.load::<Post>(&conn).expect("Error en la consulta SQL.");

    // Iteramos los resultados de la consulta
    for post in posts_result {
        println!("{}", post.title);
    }

}

Utilizando la estructura NewPost previamente creada, creamos el objeto con las propiedades que le corresponde y realizamos el insert en la BBDD. Si todo ha ido bien, el ciclo for mostrará los resultados insertados uno a uno.

Hasta aquí, has visto lo básico de cómo realizar un insert en una base de datos SQL con Rust y Diesel. Continuemos ya que aún hay mucho por explorar sobre conexiones a una BBDD con este lenguaje.


Contribución creada por: Kevin Fiorentino.