sqlite - 无法在循环 : use of moved value and cannot borrow as mutable more than once at a time 内构建 rusqlite 事务

标签 sqlite rust borrow-checker

为了使用 rusqlite 加快插入 SQLite 数据库的速度,我想在 for 循环中构建一个事务,并且只提交每 N 次迭代。

下面的代码可以编译,但它构建了一个事务并一次性提交:

use rusqlite::{Connection, Result, NO_PARAMS};

fn main() -> Result<()> {
    let mut conn = Connection::open_in_memory()?;

    conn.execute(
        "CREATE TABLE entry (
            id   INTEGER PRIMARY KEY,
            data INTEGER
        )",
        NO_PARAMS,
    )?;

    let tx = conn.transaction()?;
    for i in 0..20 {
        tx.execute("INSERT INTO entry (data) VALUES (?1)", &[i])?;
    }
    tx.commit()?;

    Ok(())
}

我的用例需要构建一个包含数百万次插入的事务,所以我想做的是累积事务,当它达到 transaction_size 时提交它并重新开始新交易。 非编译版本看起来像这样:

let transaction_size = 5;
let tx = conn.transaction()?;
for i in 0..20 {
    if (i % transaction_size) == (transaction_size - 1) {
        tx.commit()?;
        let tx = conn.transaction()?;
    }
    tx.execute("INSERT INTO entry (data) VALUES (?1)", &[i])?;
}

借用检查器不允许这样做有两个原因。

error[E0382]: use of moved value: `tx`
  --> src/main.rs:18:13
   |
15 |     let tx = conn.transaction()?;
   |         -- move occurs because `tx` has type `rusqlite::transaction::Transaction<'_>`, which does not implement the `Copy` trait
...
18 |             tx.commit()?;
   |             ^^ value moved here, in previous iteration of loop

error[E0499]: cannot borrow `conn` as mutable more than once at a time
  --> src/main.rs:19:22
   |
15 |     let tx = conn.transaction()?;
   |              ---- first mutable borrow occurs here
...
19 |             let tx = conn.transaction()?;
   |                      ^^^^ second mutable borrow occurs here
20 |         }
21 |         tx.execute("INSERT INTO entry (data) VALUES (?1)", &[i])?;
   |         -- first borrow later used here

第一个提示对我来说很有意义。第二个不是那么多,因为下面将编译(但我在每个事务中只插入一行):

for i in 0..20 {
    let tx = conn.transaction()?;
    tx.execute("INSERT INTO entry (data) VALUES (?1)", &[i])?;
    tx.commit()?;
}

我试过使用 let tx = if cond { tx.commit()?; conn.transaction()? } 在循环中,但你需要一个 else 子句来进行类型检查。

我不知道如何在让编译器满意的同时实现我的目标。也许有一些方法可以使用不安全的功能来做到这一点,但我对 Rust 还很陌生。

编辑

我忘了说我想把我的迭代器当作一次性使用。

使用来自@Sébastien Renauld 的将构建交易的逻辑分离到 do_batch 中的想法,我制作了这个版本,它将累积必须添加到具有可变向量的交易中的数据。然后,它以 transaction_size 大小的 block 构建并提交事务。

use rusqlite::{Connection, Result, Transaction, NO_PARAMS};
use std::vec::Vec;

fn do_batch<'a>(tx: &Transaction<'a>, transaction_accum: &Vec<i32>) -> Result<()> {
    for i in transaction_accum.iter() {
        tx.execute("INSERT INTO entry (data) values (?1)", &[i])?;
    }
    Ok(())
}

fn main() -> Result<()> {
    let mut conn = Connection::open_in_memory()?;

    conn.execute(
        "CREATE TABLE entry (
            id   INTEGER PRIMARY KEY,
            data INTEGER
        )",
        NO_PARAMS,
    )?;

    let transaction_size = 5;
    let mut transaction_accum: Vec<i32> = Vec::new();
    for i in 1..20 {
        transaction_accum.push(i);

        if (i % transaction_size) == (transaction_size - 1) {
            let tx = conn.transaction()?;
            do_batch(&tx, &transaction_accum)?;
            transaction_accum.clear();
            tx.commit()?;
        }
    }
    Ok(())
}

编辑 2

在@Sébastien Renauld 的另一个建议之后,我偶然发现了 itertools crate,它可以让你分块迭代器的输出,它提供了以下漂亮而干净的解决方案。我唯一担心的是,为了生成 block ,整个迭代器在调用 chunks 时是在幕后实现的。是这样吗?

use rusqlite::{Connection, Result, Transaction, NO_PARAMS};
use std::vec::Vec;
use itertools::Itertools;


fn do_batch<'a>(tx: &Transaction<'a>, transaction_accum: &Vec<i32>) -> Result<()> {
    for i in transaction_accum.iter() {
        tx.execute("INSERT INTO entry (data) values (?1)", &[i])?;
    }
    Ok(())
}

fn main() -> Result<()> {
    let mut conn = Connection::open_in_memory()?;

    conn.execute(
        "CREATE TABLE entry (
            id   INTEGER PRIMARY KEY,
            data INTEGER
        )",
        NO_PARAMS,
    )?;

    let transaction_size = 5;
    let my_iter = 1..20; // this is really a WalkDir from the walkdir crate
    for chunk in &my_iter.into_iter().chunks(transaction_size) {
        let tx = conn.transaction()?;
        do_batch(&tx, &chunk.collect())?;
        tx.commit()?;
    }
    Ok(())
}

最佳答案

这是一个 SQL 问题,而不是 Rust 问题,但我会解释你为什么会遇到这个问题,以及它如何出现在 Rust 中。

这一切都源于对事务数据库的基本误解,它适用于支持事务的每一个 RDBMS。事务的要点是打开服务器上可以看作是单独的平板的东西;然后,您对其进行状态更改,例如添加或删除行,然后您将单独的平板变成服务器的“真实”状态。根据您使用的数据库引擎,这将以不同的方式实现,但对于我们今天针对您的问题的目的,这个类比就可以了。

您不是这样做,而是打开您的交易,进行一次插入,然后立即用 commit() 交回石板.注意它的签名:

fn commit(self) -> Result<()>

正如我们所期望的,commit() 接受self,而不是&mut self。通过提交(或回滚),您告诉服务器您已完成此事务。

要解决此问题,您需要根据数据库决定如何处理它。批处理是一个好主意,您已经找到了,但是您需要确保能够承受一批失败并重复的后果。因此,我们将把事情分开一些。

首先,我们将构建我们的批处理生成器。我们将需要它,特别是如果我们打算重播一个批处理:

fn do_batch<'a>(tx: &mut Transaction<'a>) -> Result<(), rusqlite::Error> {
    for i in 0..20 {
        tx.execute("INSERT INTO entry (data) values (?1", &[i])?;
    }
    Ok(())
}

然后,我们围绕它构建结构:

fn do_tx(mut conn: Connection) -> Result<(), rusqlite::Error> {
    for i in 0..20 {
        // Open the TX
        let mut tx = conn.transaction()?;
        do_batch(&mut tx)?;
        // Do your error handling here. If the batch fails, you want to decide whether to retry or abort.
        tx.commit()?;
    }
    Ok(())
}

如果可能,分离关注点总是值得的,如果需要,传递事务总是值得的;这就是他们在那里的目的。让您的函数构建批处理,然后在某种总体结构中处理提交/回滚行为。


正如您在评论中提到的,您正在走一棵树。为此,我假设您已经展平了迭代器(即您的 N 维树由一维迭代器表示),并且该迭代器位于 tree_walker.

当前没有在迭代器上定义chunks() 方法,而这正是您需要的。为简洁起见,我们将使用 collect() 然后使用 Vec::chunks()。对于大多数工作负载,这应该不是问题,但如果您发现此分配的大小太大,您可以相对轻松地自己重新实现它。

use rusqlite::Error;
use rusqlite::{Connection, Transaction};

fn do_batch<'a>(tx: &Transaction<'a>, transaction_accum: &[i32]) -> Result<(), rusqlite::Error> {
    for i in transaction_accum.iter() {
        tx.execute("INSERT INTO entry (data) values (?1)", &[i])?;
    }
    Ok(())
}
fn commit(
    mut conn: Connection,
    tree_walker: impl Iterator<Item = i32>,
    batch_size: usize,
) -> Result<(), rusqlite::Error> {
    let collected: Vec<i32> = tree_walker.collect();
    collected
        .chunks(batch_size)
        .fold(Ok(()), |current, elements| {
            current.and_then(|_| {
                let tx = conn.transaction()?;
                do_batch(&tx, &elements)?;
                tx.commit()
            })
        })
}

关于sqlite - 无法在循环 : use of moved value and cannot borrow as mutable more than once at a time 内构建 rusqlite 事务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58088362/

相关文章:

iOS:从 SQLite 数据库填充 tableView

c# - 通过关系(外键)更新其他列值SQLite

rust - 如何初始化数组并保存对其项的引用?

rust - 安全地搬出借来的内容

sqlite - 在 ASP.NET Core 2.1 中使用 Entity Framework Core 的 UseSqlite 不起作用

sqlite - B 树的节点使用哪种数据结构?

rust - 在 Rust 中,如何强制一个被阻止读取文件的线程恢复?

gcc - 使用 Cargo 构建时找不到 `lcrypto` 或 `lssl`

vector - 如何将字符串的 HashSet 转换为向量?

compilation - 我不明白借贷是如何运作的