Rust Web 全栈开发(六):在 Web 项目中使用 MySQL 数据库

Rust Web 全栈开发(六):在 Web 项目中使用 MySQL 数据库

参考视频:https://www.bilibili.com/video/BV1RP4y1G7KF

继续使用之前的 Actix 项目。

配置项目

打开 Cargo.toml,把 edition 改成 “2021”。

修改 [dependencies] 部分:

actix-web="4.1.0"
actix-rt="2.7.0"
dotenv = "0.15.0"
chrono = {version = "0.4.19", features = ["serde"]}
serde = {version = "1.0.140", features = ["derive"]}
sqlx = {version = "0.6.0", default_features = false, features = [
    "mysql",
    "runtime-tokio-rustls",
    "macros",
    "chrono",
]}

注意:在添加 crate 时,注意使用版本要相互兼容,否则会出现编译警告。具体需要访问 crates.io 来查看合适的版本。

在终端执行命令 cargo build,构建成功:

在这里插入图片描述

修改 AppState

把 state.rs 修改为:

use std::sync::Mutex;
// use super::models::Course;
use sqlx::MySqlPool;

pub struct AppState {
    pub health_check_response: String,
    pub visit_count: Mutex<u32>,
    // pub courses: Mutex<Vec<Course>>,
    pub db: MySqlPool,
}

现在,课程不再存储在内存中,而是存储在 MySQL 数据库中。

修改 Course

打开 models.rs,让 Course 结构体实现 sqlx::FromRow trait,便于读取数据时的数据转换。

#[derive(Deserialize, Serialize, sqlx::FromRow, Debug, Clone)]
pub struct Course {
    pub teacher_id: i32,
    pub id: Option<i32>,
    pub name: String,
    pub time: Option<NaiveDateTime>,
}

数据库准备

在这里插入图片描述

新建一个名为 course 的 MySQL 数据库,再新建一个名为 course 的表:

在这里插入图片描述

time 如果用 timestamp 类型的话,会报错:error[E0277]: the trait bound `NaiveDate: From<DateTime>` is not satisfied,原因是:the trait `From<DateTime>` is not implemented for `NaiveDate`。

time 如果用 date 类型的话,会报错:mismatched types,原因是:Rust type `core::option::Option<chrono::naive::datetime::NaiveDateTime>` (as SQL type `DATETIME`) is not compatible with SQL type `DATE`。

内容如下:

在这里插入图片描述

连接请求

在 webservice 目录下,新建名为 .env 的文件,在文件内写入请求 URL,形如:

DATABASE_URL=mysql://{user}:{password}@{IP}:{port}/{database name}

这里,我的请求 URL 是:

DATABASE_URL=mysql://root:12138@127.0.0.1:3306/course

读取 MySQL 数据库

在 webservice/src 目录下新建 db_access.rs,实现访问 MySQL 数据库,执行 SQL 语句的功能。

use super::models::*;
use sqlx::MySqlPool;

pub async fn get_courses_for_teacher_db(pool: &MySqlPool, teacher_id: i32) -> Vec<Course> {
    let rows: Vec<Course> = sqlx::query_as(
        "SELECT id, teacher_id, name, time
            FROM course
            WHERE teacher_id = ?"
    )
        .bind(teacher_id)
        .fetch_all(pool) // 获取所有记录
        .await
        .unwrap();

    rows.iter()
        .map(|r| Course {
            id: Some(r.id.expect("Unknown")),
            teacher_id: r.teacher_id,
            name: r.name.clone(),
            time: Some(chrono::NaiveDateTime::from(r.time.unwrap())),
        })
        .collect()
}

pub async fn get_course_details_db(pool: &MySqlPool, teacher_id: i32, course_id: i32) -> Course {
    let row: Course = sqlx::query_as(
        "SELECT id, teacher_id, name, time
            FROM course
            WHERE teacher_id = ? and id = ?"

    )
        .bind(teacher_id)
        .bind(course_id)
        .fetch_one(pool) // 获取单条记录
        .await
        .unwrap();

    Course {
        id: Some(row.id.expect("Unknown")),
        teacher_id: row.teacher_id,
        name: row.name.clone(),
        time: Some(chrono::NaiveDateTime::from(row.time.unwrap())),
    }
}

pub async fn post_new_course_db(pool: &MySqlPool, new_course: Course) -> Result<(), sqlx::Error> {
    let _insert_query = sqlx::query!(
        "INSERT INTO course (id, teacher_id, name, time)
            VALUES (?, ?, ?, ?)",
        new_course.id.unwrap(),
        new_course.teacher_id,
        new_course.name,
        new_course.time
    )
        .execute(pool)
        .await?;

    Ok(())
}

其他修改

修改 teacher_service.rs,把 db_access.rs 的路径添加进去。

把读取 MySQL 数据库的内容添加进去,再对 AppState 实例的构建的代码进行修改。

use actix_web::{web, App, HttpServer};
use std::sync::Mutex;
use dotenv::dotenv;
use sqlx::mysql::MySqlPoolOptions;
use std::env;
use std::io;

#[path = "../handlers.rs"]
mod handlers;
#[path = "../models.rs"]
mod models;
#[path = "../routers.rs"]
mod routers;
#[path = "../state.rs"]
mod state;
#[path = "../db_access.rs"]
mod db_access;

use routers::*;
use state::AppState;

#[actix_rt::main]
async fn main() -> io::Result<()> {
    // 检测并读取 .env 文件中的内容,若不存在也会跳过异常
    dotenv().ok();

    let db_url = env::var("DATABASE_URL")
        .expect("DATABASE_URL 没有在 .env 文件里设置");
    
    // 创建数据库连接池
    let db_pool = MySqlPoolOptions::new()
        .connect(&db_url)
        .await
        .unwrap();
    
    let shared_data = web::Data::new(AppState {
        health_check_response: "I'm OK.".to_string(),
        visit_count: Mutex::new(0),
        // courses: Mutex::new(vec![]),
        db: db_pool,
    });
    
    let app = move || {
        App::new()
            .app_data(shared_data.clone())
            .configure(general_routes)
            .configure(course_routes)
    };

    HttpServer::new(app).bind("127.0.0.1:3000")?.run().await
}

修改 handlers.rs,对数据库的操作都调用 db_access.rs 中的函数

use super::state::AppState;
use actix_web::{web, HttpResponse};
use super::models::Course;
use super::db_access::*;

pub async fn health_check_handler(app_state: web::Data<AppState>) -> HttpResponse {
    println!("incoming for health check");

    let health_check_response = &app_state.health_check_response;
    let mut visit_count = app_state.visit_count.lock().unwrap();
    let response = format!("{} {} times", health_check_response, visit_count);
    *visit_count += 1;

    HttpResponse::Ok().json(&response)
}

pub async fn new_course(
    new_course: web::Json<Course>,
    app_state: web::Data<AppState>,
) -> HttpResponse {
    let _result = post_new_course_db(&app_state.db, new_course.into()).await;

    HttpResponse::Ok().json("Course added")
}

pub async fn get_courses_for_teacher(
    app_state: web::Data<AppState>,
    params: web::Path<i32>,
) -> HttpResponse {
    let teacher_id = params.into_inner();
    let courses = get_courses_for_teacher_db(&app_state.db, teacher_id).await;

    HttpResponse::Ok().json(courses)
}

pub async fn get_course_detail(
    app_state: web::Data<AppState>,
    params: web::Path<(i32, i32)>,
) -> HttpResponse {
    let (teacher_id, course_id) = params.into_inner();
    let course = get_course_details_db(&app_state.db, teacher_id, course_id).await;

    HttpResponse::Ok().json(course)
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::http::StatusCode;
    use std::sync::Mutex;
    use dotenv::dotenv;
    use sqlx::mysql::MySqlPoolOptions;
    use std::env;
    use chrono::{NaiveDate, NaiveDateTime, NaiveTime};

    #[actix_rt::test]
    async fn post_course_test() {
        // 检测并读取 .env 文件中的内容,若不存在也会跳过异常
        dotenv().ok();

        let db_url = env::var("DATABASE_URL")
            .expect("DATABASE_URL 没有在 .env 文件里设置");

        // 创建数据库连接池
        let db_pool = MySqlPoolOptions::new()
            .connect(&db_url)
            .await
            .unwrap();

        let course = web::Json(Course {
            teacher_id: 1,
            name: "Test course".into(),
            id: Some(3),
            time: Some(NaiveDateTime::new(
                NaiveDate::from_ymd_opt(2025, 7, 12).expect("Unknown date"),
                NaiveTime::from_hms_opt(10, 15, 0).expect("Unknown time"),
            )),
        });

        let app_state: web::Data<AppState> = web::Data::new(AppState {
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            db: db_pool,
        });

        // 模拟添加课程的请求
        let response = new_course(course, app_state).await;

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[actix_rt::test]
    async fn get_all_courses_success() {
        // 检测并读取 .env 文件中的内容,若不存在也会跳过异常
        dotenv().ok();

        let db_url = env::var("DATABASE_URL")
            .expect("DATABASE_URL 没有在 .env 文件里设置");

        // 创建数据库连接池
        let db_pool = MySqlPoolOptions::new()
            .connect(&db_url)
            .await
            .unwrap();

        let app_state: web::Data<AppState> = web::Data::new(AppState {
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            db: db_pool,
        });

        let teacher_id: web::Path<i32> = web::Path::from(1);
        let response = get_courses_for_teacher(app_state, teacher_id).await;

        assert_eq!(response.status(), StatusCode::OK);
    }

    #[actix_rt::test]
    async fn get_one_course_success() {
        // 检测并读取 .env 文件中的内容,若不存在也会跳过异常
        dotenv().ok();

        let db_url = env::var("DATABASE_URL")
            .expect("DATABASE_URL 没有在 .env 文件里设置");

        // 创建数据库连接池
        let db_pool = MySqlPoolOptions::new()
            .connect(&db_url)
            .await
            .unwrap();

        let app_state: web::Data<AppState> = web::Data::new(AppState {
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            db: db_pool,
        });

        let params: web::Path<(i32, i32)> = web::Path::from((1, 1));
        let response = get_course_detail(app_state, params).await;

        assert_eq!(response.status(), StatusCode::OK);
    }
}

测试

在终端执行命令 cargo test --bin teacher_service,三个测试都通过了:

在这里插入图片描述

成功向 MySQL 数据库插入了一条数据:

在这里插入图片描述

cd 到 webservice,执行命令 cargo run,在浏览器中测试两个查询功能,都成功了:

在这里插入图片描述

在这里插入图片描述

尾声

视频教程 中使用的是 PostgreSQL 数据库,本文使用的是 MySQL 数据库,在代码方面存在很多细微的差异(集中体现在 db_access.rs 中 SQL 语句的写法),请读者仔细比对。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值