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 语句的写法),请读者仔细比对。