RUST异步数据库工具库sqlx快速入门教程

文章目录

数据最终还是要落到某个地方的,很多时候都是关系型数据库,所以数据库的学习是必不可少的, 本教程主要记录sqlx的一些常用代码片段,便于快速掌握sqlx的增删改查操作(CRUD)。

值得注意的是,sqlx不是ORM, 所以必须自己写sql, 如果不喜欢自己写sqlx可以选择rust的其他orm, 比如ormx或者diesel

版本说明

Cargo.toml文件里的依赖配置如下:

[dependencies]
tokio = {version = "1", features=["full"] }
sqlx = { version = "0.7", features = [ "runtime-tokio", "postgres"] }

注意: 这里使用的sqlite, 如果需要使用其他数据库, 比如mysql, postgresql需要开启必要的特性。

安装sqlx-cli

sqlx-cli是一个sqlx配套的命令行工具,可以管理数据库。

cargo install sqlx-cli

本文使用的版本是: 0.7.2

通过查看是否可以执行sqlx命令检查是否安装成功。

数据库管理

我们只可以直接通过sqlx-cli创建和删除数据库, 在操作数据库之前你需要设置环境变量DATABASE_URL, 比如DATABASE_URL=postgres://用户名:密码@数据库地址/数据库名

如果是linux环境,可以通过以下命令设置环境变量(windows的git bash也行)

export DATABASE_URL=postgres://用户名:密码@数据库地址/数据库名

将配置放在.env文件似乎也可以, 但是有时候不生效, 不知道为啥。

数据库管理

当前DATABASE_URL变量设置完毕, 就可以通过sqlx命令行工具管理数据库了。

创建

我们可以通过以下命令创建

sqlx database create

删除

sqlx database drop

处理创建和删除还有其他命令resetsetup, 这个大家可以通过sqlx help database查看具体的使用说明

数据表管理

当数据库创建完成,就可以管理数据表了,假设我们想要管理一张todo的表,创建的sql如下。

CREATE TABLE IF NOT EXISTS todos
(
    id          BIGSERIAL PRIMARY KEY,
    description TEXT    NOT NULL,
    done        BOOLEAN NOT NULL DEFAULT FALSE
);

migration

由于开发过程中,数据表的改动时常发生,所以我们需要一套版本控制的流程,大多数的ORM都将这个操作叫做migration(不知道翻译成啥比较好),sqlx也不例外, 有了这套工具我们就应该每次创建一个新的版本,然后有sqlx来管理数据表之间的不同,以及记录创建表的sql代码的各个版本。

执行以下命令创建第一个版本

sqlx migrate add todo

它会在当前目录创建一个migrations的目录, 然后创建一个{时间}_todo.sql的文件, 时间用于确定各个sql的先后顺序,然后我们可以将上面的sql语句复制到{时间}_todo.sql文件。

创建数据表

这条命令会创建最新的sql语句,数据表的修改需要自己维护sql

sqlx migrate run

注意: 每次修改sql都需要执行sqlx migrate add todo来创建一个新的sql

代码管理数据库和数据表

sqlx的数据库和数据表除了使用sqlx-cli命令行操作,也可以使用代码管理。

use sqlx::{migrate::MigrateDatabase, Postgres, postgres::PgPoolOptions};

const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";


#[tokio::main]
async fn main() {
    // 判断数据库是否存在,不存在则创建
    if !Postgres::database_exists(DB_URL).await.unwrap_or(false) {
        println!("创建数据库 {}", DB_URL);
        match Postgres::create_database(DB_URL).await {
            Ok(_) => println!("创建数据库成功"),
            Err(err) => panic!("创建数据库失败: {}", err)
        }
    } else {
        println!("创建库已存在, 无需创建");
    }

    // 创建连接池
    let db = PgPoolOptions::new()
        // 设置最大连接数
        .max_connections(20)
        .connect(DB_URL)
        .await.unwrap();

    // 执行创建表的sql
    let result = sqlx::query(r#"
        CREATE TABLE IF NOT EXISTS todos
        (
            id          BIGSERIAL PRIMARY KEY,
            description TEXT    NOT NULL,
            done        BOOLEAN NOT NULL DEFAULT FALSE
        );"#)
        .execute(&db)
        .await
        .unwrap();

    println!("创建数据库的结果: {:?}", result);
}

数据管理

当数据库和数据表都有了,自然就可以增删改查数据了。不过,在此之前应该了解如何连接数据库。

连接数据库

use sqlx::postgres::PgPoolOptions;

const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";


#[tokio::main]
async fn main() {
    // 创建连接池
    let db = PgPoolOptions::new()
        // 设置最大连接数
        .max_connections(20)
        .connect(DB_URL)
        .await.unwrap();
}

增删改查

有了连接就可以对数据增删改查了。

use sqlx::postgres::PgPoolOptions;

const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";


#[tokio::main]
async fn main() {
    // 创建连接池
    let db = PgPoolOptions::new()
        // 设置最大连接数
        .max_connections(20)
        .connect(DB_URL)
        .await.unwrap();

    // 插入数据
    let result = sqlx::query!(
        r#"
    INSERT into todos (description)
        values($1)
        RETURNING id
        "#, 
        "hello world")
        .fetch_one(&db)
        .await
        .expect("插入数据失败.");

    println!("插入数据成功, 对应的id是: {:?}", result.id);
    // 查询数据
    let result = sqlx::query!(r#"SELECT * from todos"#,)
        .fetch_all(&db)
        .await
        .expect("查询数据失败.");

    for row in result {
        println!("查询数据结果: [{}] {} {}", row.id, row.description, row.done);
    }

    // 更新数据
    let result = sqlx::query!(
        r#"
    UPDATE todos SET description = $1
    WHERE ID = 1
        "#, 
        "updated")
        .execute(&db)
        .await
        .expect("更新数据失败.");
    println!("更新的结果: {:?}", result);

    // 删除数据
    let result = sqlx::query!(
        r#"
    DELETE FROM todos WHERE ID = $1
        "#, 
        1)
        .execute(&db)
        .await
        .expect("删除数据失败.");
    println!("删除的结果: {:?}", result);
}

输出的结果如下:

插入数据成功, 对应的id是: 1
查询数据结果: [1] hello world false
更新的结果: PgQueryResult { rows_affected: 1 }
删除的结果: PgQueryResult { rows_affected: 1 }

绑定结构体

虽然上面的代码可以解决问题,但是,很多时候我们会将结果绑定到结构体中。

use sqlx::postgres::PgPoolOptions;
use sqlx::FromRow;

const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";

#[derive(Debug, FromRow)]
struct Todo {
    id: i64,
    description: String,
    done: bool
}

#[tokio::main]
async fn main() {
    // 创建连接池
    let db = PgPoolOptions::new()
        // 设置最大连接数
        .max_connections(20)
        .connect(DB_URL)
        .await.unwrap();

    // 插入数据
    let result = sqlx::query!(
        r#"
    INSERT into todos (description)
        values($1)
        RETURNING id
        "#, 
        "hello world")
        .fetch_one(&db)
        .await
        .expect("插入数据失败.");

    println!("插入数据成功, 对应的id是: {:?}", result.id);
    // 查询数据
    let result = sqlx::query_as!(Todo, r#"SELECT * from todos"#)
        .fetch_all(&db)
        .await
        .expect("查询数据失败.");

    for todo in result {
        println!("查询数据结果: [{}] {} {}", todo.id, todo.description, todo.done);
    }
}

这段代码的重点在于, 首先有一个存在数据的结构体,它需要至少使用sqlx提供的FromRow, 比如#[derive(FromRow)],最后就是需要在查询的时候在query_as!的第一个参数指定对应的类型。

插入多行数据

如果一次查询多条数据会稍微麻烦一点

use sqlx::postgres::PgPoolOptions;
use sqlx::FromRow;

const DB_URL: &str = "postgres://用户名:密码@数据库地址/数据库名";

#[derive(Debug, FromRow)]
struct Todo {
    id: i64,
    description: String,
    done: bool
}

#[tokio::main]
async fn main() {
    // 创建连接池
    let db = PgPoolOptions::new()
        // 设置最大连接数
        .max_connections(20)
        .connect(DB_URL)
        .await.unwrap();

    // 摘自: https://github.com/launchbadge/sqlx/issues/294#issuecomment-830409187
    let lala = vec![("abc", true), ("xyz", false)];
    let mut v1: Vec<String> = Vec::new();
    let mut v2: Vec<bool> = Vec::new();
    lala.into_iter().for_each(|todo| {
        v1.push(todo.0.into());
        v2.push(todo.1);
    });
    let result = sqlx::query(
        r#"INSERT INTO todos (description, done)
        SELECT * FROM UNNEST($1, $2)"#,
    )
    .bind(&v1)
    .bind(&v2)
    .execute(&db)
    .await
    .expect("插入多行数据失败");

    println!("插入多行数据结果: {:?}", result);
}

总结

直接使用sql并没有想象中的那么复杂,这是因为本篇的sql都比较简单,对于比较复杂的事务肯定是写起来比较烦人的,这个可以考虑使用ORM或者将sql写到一个文件,然后使用sqlx提供的query_file_as之类的宏来处理。

参考文章