惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
aimingoo的专栏
aimingoo的专栏
Jina AI
Jina AI
Microsoft Security Blog
Microsoft Security Blog
IT之家
IT之家
V
Visual Studio Blog
量子位
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
大猫的无限游戏
大猫的无限游戏
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
D
Docker
MyScale Blog
MyScale Blog
小众软件
小众软件
云风的 BLOG
云风的 BLOG
美团技术团队
Microsoft Azure Blog
Microsoft Azure Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Last Week in AI
Last Week in AI
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 【当耐特】

又耳笔记

用AI挣钱之AI建站:二维码生成器工具 用AI挣钱之AI建站:https://markdowntopdf.top Cloudflare Workers实战番外一:用Static Assets托管网站,告别传统方案 Cloudflare Workers实战(五):不止JavaScript,拥抱Python与Rust Cloudflare Workers实战(四):托管和分发静态文件 Cloudflare Workers实战(三):实现认证、重定向与缓存 Cloudflare Workers实战(二):动态修改后端响应 Cloudflare Workers实战(一):随心所欲操作客户端请求 Cloudflare workers不完全指南 使用n8n创作短篇小说 水果风波:信任的代价 用Python将PDF文件转换成图片 rust网络框架Pingora源码阅读3 rust网络框架Pingora源码阅读2 rust网络框架Pingora源码阅读1 Pingora快速入门教程1之总览 Rust模板引擎askama快速入门引擎 使用Loco快速搭建自己的后台系统 用Rust发一封图文并茂的邮件 Rust小项目: 写一个简单的恶意流量阻断器 Rust小项目: 写一个简单的网页爬虫 Rust命令行库Clap快速入门教程 Rust小项目:用Rust写一个端口扫描器 Rust真全栈开发快速入门 rust声明宏快速入门教程 Rust文本处理快速入门教程 RUST web框架axum快速入门教程6之测试 白嫖免费的Rust在线运行时shuttle RUST web框架axum快速入门教程5之中间件 用Rust来做以太坊开发5之事件日志及签名
RUST异步数据库工具库sqlx快速入门教程
About 又耳宁 关于技术与股票的一些零碎想法 · 2023-11-04 · via 又耳笔记

数据最终还是要落到某个地方的,很多时候都是关系型数据库,所以数据库的学习是必不可少的, 本教程主要记录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配套的命令行工具,可以管理数据库。

本文使用的版本是: 0.7.2

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

数据库管理

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

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

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

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

数据库管理

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

创建

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

删除

处理创建和删除还有其他命令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代码的各个版本。

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

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

创建数据表

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

注意: 每次修改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之类的宏来处理。

参考文章