Estoy tratando de diseñar una estructura para transportar una conexión de Postgres, una transacción y un montón de declaraciones preparadas, y luego ejecutar las declaraciones preparadas repetidamente. Pero me encuentro con problemas de por vida. Esto es lo que tengo:

extern crate postgres;

use postgres::{Connection, TlsMode};
use postgres::transaction::Transaction;
use postgres::stmt::Statement;

pub struct Db<'a> {
    conn: Connection,
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

fn make_db(url: &str) -> Db {
    let conn = Connection::connect(url, TlsMode::None).unwrap();
    let tx = conn.transaction().unwrap();
    let insert_user = tx.prepare("INSERT INTO users VALUES ($1)").unwrap();
    Db {
        conn: conn,
        tx: tx,
        insert_user: insert_user,
    }
}

pub fn main() {
    let db = make_db("postgres://paul@localhost/t");
    for u in &["foo", "bar"] {
        db.insert_user.execute(&[&u]);
    }
    db.tx.commit().unwrap();
}

Aquí está el error que obtengo (en Rust 1.15.0 estable):

error: `conn` does not live long enough
  --> src/main.rs:15:14
   |
15 |     let tx = conn.transaction().unwrap();
   |              ^^^^ does not live long enough
...
22 | }
   | - borrowed value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the body at 13:28...
  --> src/main.rs:13:29
   |
13 | fn make_db(url: &str) -> Db {
   |                             ^

He leído el libro Rust (he perdido la cuenta cuántas veces), pero no estoy seguro de cómo avanzar aquí. ¿Alguna sugerencia?

EDITAR: Pensando en esto un poco más, todavía no entiendo por qué, en principio, no puedo decirle a Rust, "conn vive tanto tiempo como Db". El problema es mover conn, pero ¿qué pasa si no lo muevo? Entiendo por qué en C no puede devolver un puntero a la memoria asignada a la pila, por ejemplo:

#include <stdio.h>

int *build_array() {
  int ar[] = {1,2,3};
  return ar;
}

int main() {
  int *ar = build_array();
  printf("%d\n", ar[1]);
}

Y entiendo cómo eso es similar a Rust devolviendo un &str o devolviendo un segmento vec.

Pero en Rust, puedes hacer esto:

#[derive(Debug)]
struct S {
    ar: Vec<i32>,
}

fn build_array() -> S {
    let v = vec![1, 2, 3];
    S { ar: v }
}

fn main() {
    let s = build_array();
    println!("{:?}", s);
}

Y entiendo que Rust es lo suficientemente inteligente como para que regresar S no requiera un movimiento; esencialmente va directo al marco de la pila de la persona que llama.

Así que no entiendo por qué no puede poner también Db (incluido conn) en el marco de la pila de la persona que llama. Entonces no se requerirán movimientos, y tx nunca tendrá una dirección no válida. Siento que Rust debería ser capaz de resolver eso. Intenté agregar una pista de por vida, como esta:

pub struct Db<'a> {
    conn: Connection<'a>,
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

Pero eso da un error de "parámetro de vida útil inesperado". Puedo aceptar que Rust no puede seguir la lógica, pero tengo curiosidad por saber si hay una razón por la que, en principio, no podría.

Parece que poner conn en el montón debería resolver mis problemas, pero tampoco puedo hacer que esto funcione:

pub struct Db<'a> {
    conn: Box<Connection>,
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

Incluso con un let conn = Box::new(Connection::connect(...));, Rust todavía me dice "conn no vive lo suficiente". ¿Hay alguna forma de hacer que esto funcione con Box, o es un callejón sin salida?

EDIT 2: intenté hacer esto también con macros, para evitar cualquier marco de pila adicional:

extern crate postgres;

use postgres::{Connection, TlsMode};
use postgres::transaction::Transaction;
use postgres::stmt::Statement;

pub struct Db<'a> {
    conn: Connection,
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

macro_rules! make_db {
      ( $x:expr ) => {
        {
          let conn = Connection::connect($x, TlsMode::None).unwrap();
          let tx = conn.transaction().unwrap();
          let insert_user = tx.prepare("INSERT INTO users VALUES ($1)").unwrap();
          Db {
            conn: conn,
            tx: tx,
            insert_user: insert_user,
          }
        }
      }
    }


pub fn main() {
    let db = make_db!("postgres://paul@localhost/t");
    for u in &["foo", "bar"] {
        db.insert_user.execute(&[&u]);
    }
    db.tx.commit().unwrap();
}

Pero eso todavía me dice que conn no vive lo suficiente. Parece que moverlo a la estructura realmente no debería requerir ningún cambio real de RAM, pero Rust aún no me deja hacerlo.

2
Paul A Jungwirth 10 feb. 2017 a las 03:52

2 respuestas

La mejor respuesta

Usando la otra respuesta, armé un código de trabajo que me permite agrupar la transacción y todas las declaraciones preparadas, y pasarlas alrededor juntos:

extern crate postgres;

use postgres::{Connection, TlsMode};
use postgres::transaction::Transaction;
use postgres::stmt::Statement;

pub struct Db<'a> {
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

fn make_db(conn: &Connection) -> Db {
    let tx = conn.transaction().unwrap();
    let insert_user = tx.prepare("INSERT INTO users VALUES ($1)").unwrap();
    Db {
        tx: tx,
        insert_user: insert_user,
    }
}

pub fn main() {
    let conn = Connection::connect("postgres://paul@localhost/t", TlsMode::None).unwrap();
    let db = make_db(&conn);
    for u in &["foo", "bar"] {
        db.insert_user.execute(&[&u]);
    }
    db.tx.commit().unwrap();
}

Según tengo entendido, Rust quiere garantizar que conn viva tanto tiempo como db, por lo que al mantener conn fuera del "constructor", la estructura léxica asegura que no se obtendrá eliminado demasiado pronto

Mi estructura aún no encapsula conn, lo que me parece demasiado malo, pero al menos me permite mantener todo lo demás unido.

1
Community 23 may. 2017 a las 11:47

Comenzando con esta función:

fn make_db(url: &str) -> Db {
    unimplemented!()
}

Debido a elisión vitalicia, esto es equivalente a:

fn make_db<'a>(url: &'a str) -> Db<'a> {
    unimplemented!()
}

Es decir, la vida útil de todas las referencias dentro de la estructura Db struct debe vivir tanto tiempo como el corte de la cadena pasó. Eso solo tiene sentido si la estructura se aferra al corte de la cadena.


Para "resolver" eso, podemos intentar separar las vidas:

fn make_db<'a, 'b>(url: &'a str) -> Db<'b> {
    unimplemented!()
}

Ahora esto tiene aún menos sentido porque ahora solo estamos inventando toda una vida. ¿De dónde viene ese 'b? ¿Qué sucede si la persona que llama de make_db decide que la vida útil concreta del parámetro de vida útil genérico 'b debe ser 'static? Esto se explica con más detalle en ¿Por qué no puedo almacenar un valor y una referencia a ese valor en la misma estructura?, buscar para "algo está realmente mal con nuestra función de creación".

También vemos la parte de la pregunta con "A veces, ni siquiera estoy tomando una referencia del valor" en la otra pregunta, que dice en la respuesta:

la instancia Child contiene una referencia a la Parent que la creó,

Si revisamos la definición para Connection::transaction :

fn transaction<'a>(&'a self) -> Result<Transaction<'a>>

O la definición si no crees en los documentos:

pub struct Transaction<'conn> {
    conn: &'conn Connection,
    depth: u32,
    savepoint_name: Option<String>,
    commit: Cell<bool>,
    finished: bool,
}

Sí, un Transaction mantiene una referencia a su padre Connection. Ahora que vemos que Transaction tiene una referencia a Connection, podemos volver a la otra pregunta para ver cómo resolver el problema: separe las estructuras para que la anidación refleje las vidas.

Esta era una forma muy larga de decir: no, no se puede crear una estructura única que contenga una base de datos y una transacción de esa base de datos debido a la implementación de la caja de postgres. Presumiblemente, la caja se implementa de esta manera para obtener el máximo rendimiento.


No veo por qué [regresar Db<'b>] tiene menos sentido. Normalmente, cuando una función devuelve una cosa, la cosa vive mientras esté asignada a algo. ¿Por qué -> Db no puede funcionar de la misma manera?

El punto de referencia completo es que no posee el valor referido. Devuelve Db y la persona que llama de make_db sería dueña de eso, pero ¿a qué pertenece la cosa a la que Db se refiere ? ¿De dónde vino? No puede devolver una referencia a algo local ya que eso violaría todas las reglas de seguridad de Rust. Si desea transferir la propiedad, simplemente haga eso.

Véase también

3
Shepmaster 10 jul. 2017 a las 12:34