如何使用Diesel与SQLx在Rust中与数据库进行交互(附代码实例)

1,902 阅读7分钟

在本教程中,我们将探讨在Rust中与关系型数据库交互时使用的两个库。Diesel和SQLx。

本文将使用一个有学生的简单教室数据库来演示每种方法。我们将使用Diesel ORM和SQLx进行CRUD操作。

要跟上这个教程,你需要有Rust的工作知识,以及访问和使用Rust、Rust的构建系统和包管理器Cargo的能力,以及一个MySQL服务器实例。

什么是Diesel?

Diesel是一个ORM,支持PostgreSQL、MySQL、SQLite。ORM是对象关系映射的意思。ORM帮助面向对象的程序员抽象出关系数据库的细节。

ORM带有查询生成器,所以你不必担心编写原始SQL查询。使用ORM,你可以与关系型数据库沟通,就像它们是面向对象的一样。

对于经验不足的开发者来说,使用ORM可能会更好,因为ORM会编写优化的SQL查询。ORM也使你不容易受到SQL注入攻击。

什么是SQLx?

与Diesel不同,SQLx不是一个ORM。SQLx是一个异步的Rust SQL crate,具有编译时SQL查询检查功能。它与数据库和运行时间无关。

SQLx支持连接池、跨平台开发、嵌套池、异步通知、传输层安全和其他令人兴奋的功能。当使用SQLx时,你必须自己制作SQL查询和迁移。

在了解了表面情况之后,让我们来探讨一下如何用Diesel和SQLx与关系型数据库进行交互。

开始使用Diesel ORM

下面的步骤展示了如何用Cargo建立一个使用Diesel ORM的Rust项目。

用Diesel ORM初始化一个新项目

你的第一步是通过运行以下命令来初始化项目:

cargo new -- lib classroom_diesel
cd classroom_diesel

在上面的代码中,我们建立了项目并将其命名为classroom_diesel 。新的项目目录应该是这样的:

./
│
├── src/
│   └── lib.rs
│
├── .gitignore
└── Cargo.toml

我们还需要用我们在项目中需要的依赖项来更新Cargo.toml 文件,像这样。

[dependencies]
diesel = { version = "1.4.4", features = ["mysql"] }
dotenv = "0.15.0"

dotenv 依赖关系帮助我们管理项目中的环境变量。

安装Diesel CLI

Diesel使用一个单独的CLI工具。它是一个独立的二进制文件;我们不需要在cargo.toml 文件中把它作为一个依赖项。只需用下面的命令安装它:

cargo install diesel_cli

设置我们的Diesel环境

我们需要在我们的环境中设置一个DATABASE_URL 变量。这就是Diesel如何知道要连接到哪个MySQL数据库:

echo DATABASE_URL=mysql://<username>:<password>@localhost/<database>  > .env

编辑连接字符串以匹配你的本地数据库凭证。

你的项目目录现在看起来会是这样的:

./
│
├── src/
│   └── lib.rs
│
├── .env
├── .gitignore
└── Cargo.toml

现在运行以下命令:

diesel setup

这个命令将帮助我们设置数据库,并创建一个空的migrations目录来管理数据库模式。

设置Diesel迁移

迁移可以帮助ORM跟踪数据库的操作,比如增加一个字段或者删除一个表。你可以把它们看作是数据库的一个版本控制系统。

首先,让我们使用Diesel CLI为教室应用程序创建一些迁移。理想情况下,我们应该有一个包含教室学生数据的表。
我们需要创建空的迁移文件,然后用SQL语句填充它们,以创建一个表:

diesel migration generate create_students

你的文件树将看起来与此类似:

./
│
├── migrations/
│   │
│   ├── 2022-07-04-062521_create_students/
│   │   ├── down.sql
│   │   └── up.sql
│   │
│   └── .gitkeep
│
├── src/
│   └── lib.rs
│
├── .env
├── .gitignore
├── Cargo.toml
└── diesel.toml

up.sql 文件是用来创建迁移的,而down.sql 文件是用来反转的。

用迁移的SQL来更新up.sql 文件:

sql
CREATE TABLE students (
  id INTEGER AUTO_INCREMENT PRIMARY KEY,
  firstname VARCHAR(255) NOT NULL,
  lastname TEXT NOT NULL,
  age INTEGER NOT NULL
);

用可以逆转迁移的SQL来修改down.sql 文件:

sql
DROP TABLE students;

在创建了updown 迁移之后,我们需要在数据库中执行SQL:

diesel migration run

我们可以开始编写Rust来对表进行查询。

用Diesel ORM创建行

让我们编写代码,使用.env 文件中设置的连接字符串建立一个与MySQL服务器的连接:

#[macro_use]
extern crate diesel;
extern crate dotenv;

pub mod models;
pub mod schema;

use diesel::prelude::*;
use dotenv::dotenv;
use std::env;

pub fn create_connection() -> MysqlConnection {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    MysqlConnection::establish(&database_url)
        .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

接下来,我们必须为Students 表写一个模型。模型是发生对象-关系映射的地方。模型将生成所需的代码,将Students 表上的一行或多行转换为Rust中的 Student 结构

cd ./src
touch model.rs

在我们刚刚创建的新的model.rs 文件中,添加以下内容:

use super::schema::students;

#[derive(Queryable)]
pub struct Student {
    pub id: i32,
    pub firstname: String,
    pub lastname: String,
    pub age: i32,
}

#[derive(Insertable)]
#[table_name = "students"]
pub struct NewStudent<'a> {
    pub firstname: &'a str,
    pub lastname: &'a str,
    pub age: &'a i32,
}

有了这个模型,来自Students 表的信息将映射到Rust中相应的Student 结构。现在的src 文件夹应该是这样的:

src/
├── lib.rs
├── models.rs
└── schema.rs

现在,我们可以写一个脚本来添加一个学生:

cd src
mkdir bin
cd bin
touch create_students.rs

create_students.rs 文件中,我们可以调用之前写的模型和函数来创建一个新的学生:

extern crate classroom_diesel;
extern crate diesel;

use self::classroom_diesel::*;
fn main() {
    let connection = create_connection();
    let firstname = "John";
    let lastname = "Doe";
    let age: i32 = 64;

    let student = create_post(&connection, firstname, lastname, &age);
    println!(
        "Saved student {} with id {}",
        student.firstname, student.id
    );
}

项目的结构现在看起来将类似于这样:

./
│
├── migrations/
│   │
│   ├── 2022-07-04-062521_create_students/
│   │   ├── down.sql
│   │   └── up.sql
│   │
│   └── .gitkeep
│
├── src/
│   │
│   ├── bin/
│   │   └── create_students.rs
│   │
│   ├── lib.rs
│   ├── models.rs
│   └── schema.rs
│
├── .env
├── .gitignore
├── Cargo.lock
├── Cargo.toml
└── diesel.toml

使用以下命令执行新的脚本:

cargo run --bin create_students

正如你在下面的图片中看到的,John 的新学生文件已经被保存为id ,即1 。我们可以使用这个id 来查询Rust数据库,我们将在下一节中看一下:

Result Of Mapping Students Table To A Struct To Create A New Student File In Diesel ORM

使用Diesel ORM查询Rust数据库

在上一节中,我们回顾了如何使用Diesel ORM在Rust中写进数据库。了解查询或阅读的工作方式也是非常重要的。

让我们写一个脚本来查询一个学生,他的id1 。首先,创建一个query_students.rs 文件:

cd bin
touch query_students.rs

然后,在我们刚刚创建的query_students.rs 文件中,添加以下内容:

extern crate classroom_diesel;
extern crate diesel;

use self::models::*;
use classroom_diesel::*;
use diesel::prelude::*;

fn main() {
    use self::schema::students::dsl::*;

    let connection = create_connection();
    let result = students
        .filter(id.eq(1))
        .load::<Student>(&connection)
        .expect("Error loading students");

    println!(
        "Student: {} {} {} years",
        result[0].firstname, result[0].lastname, result[0].age
    );
}

执行该脚本:

cargo run --bin query_students

正如你在下面的图片中所看到的,结果是一个打印行,包含了我们从数据库中查询的学生文件的名、姓和年龄。

Result Of Querying A Student File From A Rust Database Using Diesel ORM

开始使用SQLx

现在我们知道了如何在Rust中创建一个使用Diesel ORM与数据库交互的项目,让我们来看看如何创建一个使用SQLx的项目来代替。

用SQLx初始化一个新项目

首先运行下面的命令:

cargo new classroom_sqlx --bin

然后,在cargo.toml 文件中添加所需的依赖项:

[dependencies]
sqlx = { version = "0.5", features = [  "runtime-async-std-native-tls", "mysql" ] }
async-std = { version = "1", features = [ "attributes" ] }

这就是你在设置方面所需要的。很简单,对吗?

要使用SQLx与Rust中的数据库进行交互,我们所要做的就是编写一些SQL查询和Rust代码。在Diesel ORM部分,我们创建并读取了一条学生记录;在本节中,我们将编写查询来更新和删除一条记录。

使用SQLx和Rust来更新或删除数据库记录

首先,我们需要写一些Rust代码来连接SQLx和MySQL服务器:

//main.rs

use sqlx::mysql::MySqlPoolOptions;

#[async_std::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = MySqlPoolOptions::new()
        .max_connections(7)
        .connect("mysql://root:@localhost/classroom_diesel")
        .await?;

    Ok(())
}

SQLx支持有准备和无准备的SQL查询。准备好的SQL查询是对SQL注入的厌恶。

让我们看看如何更新一个主键为1的记录的名和姓:

use sqlx::mysql::MySqlPoolOptions;

#[async_std::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = MySqlPoolOptions::new()
        .max_connections(5)
        .connect("mysql://root:@localhost/classroom_diesel")
        .await?;

    sqlx::query("UPDATE students SET firstname=?, lastname=? WHERE id=?")
        .bind("Richard")
        .bind("Roe")
        .bind(1)
        .execute(&pool)
        .await?;
    Ok(())
}

用下面的命令执行该脚本:

cargo run

删除记录的方式也很相似,唯一的区别是SQL查询:

use sqlx::mysql::MySqlPoolOptions;

#[async_std::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = MySqlPoolOptions::new()
        .max_connections(5)
        .connect("mysql://root:@localhost/classroom_diesel")
        .await?;

    sqlx::query("DELETE FROM students WHERE id=?")
        .bind(1)
        .execute(&pool)
        .await?;
    Ok(())
}

用下面的命令执行该脚本。

cargo run

现在你可以使用Diesel或SQLx与Rust中的数据库进行交互。

总结

像Diesel这样的ORM是足够的;它们可以帮助你生成一些你需要的SQL。大多数时候,你的应用程序中只需要足够的东西。

然而,在更广泛的应用中,可能需要更多的 "魔法"--换句话说,你的时间和精力--来使ORM正确工作并生成高性能的SQL查询。

如果需要创建更复杂的查询,并有高容量和低延迟的要求,使用SQLx等库来执行原始SQL查询可能会更好。