当前位置: 首页 > backend >正文

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

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

  • Rust Web 全栈开发(六):在 Web 项目中使用 MySQL 数据库
    • 配置项目
    • 修改 AppState
    • 修改 Course
    • 数据库准备
    • 连接请求
    • 读取 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, timeFROM courseWHERE 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, timeFROM courseWHERE 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 语句的写法),请读者仔细比对。

http://www.xdnf.cn/news/15293.html

相关文章:

  • Transforms
  • 单向链表反转 如何实现
  • JVM的垃圾回收算法和多种GC算法
  • Redis面试精讲 Day 1:Redis核心特性与应用场景
  • Day59
  • JavaWeb(苍穹外卖)--学习笔记05(MD5)
  • 2D转换之缩放scale
  • Cursor创建Spring Boot项目
  • 关于赛灵思的petalinux zynqmp.dtsi文件的理解
  • 网络连接:拨号连接宽带PPPOE
  • 使用 Java 开发大数据应用:Hadoop 与 Java API 的结合
  • Golang 面向对象(封装、继承、多态)
  • Eureka实战
  • Git企业级开发(多人协作)
  • 【设计模式】装饰(器)模式 透明装饰模式与半透明装饰模式
  • Java生产带文字、带边框的二维码
  • Flink创建执行环境的三种方式,也是Flink搭建程序的第一步
  • React 组件中怎么做事件代理?它的原理是什么?
  • MyBatis实现分页查询-苍穹外卖笔记
  • openGauss数据库管理实战指南——基本常用操作总结
  • Sentry 集成
  • 【王树森推荐系统】行为序列02:DIN模型(注意力机制)
  • 【LeetCode453.最小操作次数使数组元素相等】
  • 深入解析C#接口实现的两种核心技术:派生继承 vs 显式实现
  • 论文阅读:HybridTrack: A Hybrid Approach for Robust Multi-Object Tracking
  • 前端开发中的资源缓存详解
  • 面试现场:奇哥扮猪吃老虎,RocketMQ高级原理吊打面试官
  • Spring Ai Alibaba Gateway 实现存量应用转 MCP 工具
  • AI领域的黄埔军校:OpenAI是新一代的PayPal Mafia,门生故吏遍天下
  • 浅谈 Python 中的 yield——生成器对象与函数调用的区别