Rust Web入门(五):完整的增删改查
创始人
2024-05-28 15:09:57
0

本教程笔记来自 杨旭老师的 rust web 全栈教程,链接如下:

https://www.bilibili.com/video/BV1RP4y1G7KF?p=1&vd_source=8595fbbf160cc11a0cc07cadacf22951

学习 Rust Web 需要学习 rust 的前置知识可以学习杨旭老师的另一门教程

https://www.bilibili.com/video/BV1hp4y1k7SV/?spm_id_from=333.999.0.0&vd_source=8595fbbf160cc11a0cc07cadacf22951

项目的源代码可以查看 git:(注意作者使用的是 mysql 数据库而不是原教程的数据库)

https://github.com/aiai0603/rust_web_mysql

今天来入门完善我们的项目,使得它可以进行完善的增删改查操作:

目录结构重构

之前我们开发了一个简单的 demo,我们把所有的代码都放在一个目录下,但是如果有多个模块的话,这样的结构就会很乱,所以现在我们需要优化一下我们的结构,我们新建三个文件夹 models 、handlers 和 db_access 分别负责每个模块的数据结构、事务处理和数据库操作,在每个文件夹下,我们编写一个 mod.rs 来依次导出我们的模块,之后我们就可以分模块编写我们的逻辑了:

|- db_access (数据库操作)
|--- course.rs
|--- mod.rs
|- handlers (事务逻辑)
|--- course.rs
|--- general.rs
|--- mod.rs
|- models (数据结构)
|--- course.rs
|--- mod.rs

更新数据结构

之后我们需要一份全新的数据结构,我们首先优化我们的数据库,新增一些可能是空的字段:

DROP TABLE IF EXISTS `course`;
CREATE TABLE `course`  (`id` int(0) NOT NULL AUTO_INCREMENT,`teacher_id` int(0) NOT NULL,`name` varchar(140) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`time` timestamp(0) NULL DEFAULT CURRENT_TIMESTAMP(0),`description` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`format` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`structure` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`duration` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`price` int(0) NULL DEFAULT NULL,`language` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`level` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `id`(`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of course
-- ----------------------------
INSERT INTO `course` VALUES (1, 1, 'First course', '2022-01-17 05:40:00', NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `course` VALUES (2, 1, 'Second course', '2022-01-18 05:45:00', NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `course` VALUES (4, 1, 'Test course', '2023-03-01 21:14:52', 'This is a course', NULL, NULL, NULL, NULL, 'English', 'Beginner');SET FOREIGN_KEY_CHECKS = 1;

之后我们在 models/course.rs 里面更新我们的数据结构,因为有些字段可能是空的,所以我们将他们用 Option 包裹

#[derive(Serialize, Debug, Clone, sqlx::FromRow)]
pub struct Course {pub id: i32,pub teacher_id: i32,pub name: String,pub time: Option>,pub description: Option,pub format: Option,pub structure: Option,pub duration: Option,pub price: Option,pub language: Option,pub level: Option,
}

我们新建两个数据结构用于更新和创建数据,值得注意的是,当我们需要创建一个数据的时候,我们需要检测传入的数据是不是能转化成对应的结构,所以我们使用 TryFrom 这个 trait 来转化它:

#[derive(Deserialize, Debug, Clone, sqlx::FromRow)]
pub struct CreateCourse {pub teacher_id: i32,pub name: String,pub description: Option,pub format: Option,pub structure: Option,pub duration: Option,pub price: Option,pub language: Option,pub level: Option,
}#[derive(Deserialize, Debug, Clone)]
pub struct UpdateCourse {pub name: Option,pub description: Option,pub format: Option,pub structure: Option,pub duration: Option,pub price: Option,pub language: Option,pub level: Option,
}impl TryFrom> for CreateCourse {type Error = MyError;fn try_from(course: web::Json) -> Result {Ok(CreateCourse {teacher_id: course.teacher_id,name: course.name.clone(),description: course.description.clone(),format: course.format.clone(),structure: course.structure.clone(),duration: course.duration.clone(),price: course.price,language: course.language.clone(),level: course.level.clone(),})}
}impl From> for UpdateCourse {fn from(course: web::Json) -> Self {UpdateCourse {name: course.name.clone(),description: course.description.clone(),format: course.format.clone(),structure: course.structure.clone(),duration: course.duration.clone(),price: course.price,language: course.language.clone(),level: course.level.clone(),}}
}

更新业务逻辑

之后我们开始编写 db_access 相关的模块,因为实现了 From trait 和 TryFrom trait,所以我们可以直接把查询出来的数据转化成刚刚定义的数据结构。

在查询的时候使用了 query_as 这个宏,然后指定一个数据结构,当我们查询出内容时,会包装成指定的数据结构。

注意在进行更新数据的时候,我们的逻辑是,先查询出当前 id 对应的数据,如果没有这条数据则报错,之后检索当前更新的数据,如果有任何一个字段不存在则填入数据库中的数据。

use crate::error::MyError;
use crate::models::course::{Course, CreateCourse, UpdateCourse};
use sqlx::mysql::MySqlPool;pub async fn get_courses_for_teacher_db(pool: &MySqlPool,teacher_id: i32,
) -> Result, MyError> {let rows: Vec = sqlx::query_as!(Course,"SELECT * FROM courseWHERE teacher_id = ?",teacher_id).fetch_all(pool).await?;Ok(rows)
}pub async fn get_course_details_db(pool: &MySqlPool,teacher_id: i32,course_id: i32,
) -> Result {let row = sqlx::query_as!(Course,"SELECT * FROM courseWHERE teacher_id = ? and id = ?",teacher_id,course_id).fetch_optional(pool).await?;if let Some(course) = row {Ok(course)} else {Err(MyError::NotFound("Course didn't founded".into()))}
}pub async fn post_new_course_db(pool: &MySqlPool,new_course: CreateCourse,
) -> Result {let data = sqlx::query_as!(Course,"INSERT INTO course (teacher_id, name, description, format, structure, duration, price, language, level)VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",new_course.teacher_id, new_course.name, new_course.description,new_course.format, new_course.structure, new_course.duration,new_course.price, new_course.language, new_course.level).execute(pool).await?;let row = sqlx::query_as!(Course,"SELECT * FROM courseWHERE id = ?",data.last_insert_id(),).fetch_optional(pool).await?;if let Some(course) = row {Ok(course)} else {Err(MyError::NotFound("Course didn't founded".into()))}
}pub async fn delete_course_db(pool: &MySqlPool,teacher_id: i32,id: i32,
) -> Result {let course_row = sqlx::query!("DELETE FROM course where teacher_id = ? and id=?",teacher_id,id,).execute(pool).await?;Ok(format!("DeletedI{:?}record", course_row))
}pub async fn update_course_details_db(pool: &MySqlPool,teacher_id: i32,id: i32,update_course: UpdateCourse,
) -> Result {let current_course_row = sqlx::query_as!(Course,"SELECT * FROM course where teacher_id=? and id=?",teacher_id,id).fetch_one(pool).await.map_err(|_err| MyError::NotFound("Course Id not found".into()))?;let name: String = if let Some(name) = update_course.name {name} else {current_course_row.name};let description: String = if let Some(description) = update_course.description {description} else {current_course_row.description.unwrap_or_default()};let format: String = if let Some(format) = update_course.format {format} else {current_course_row.format.unwrap_or_default()};let structure: String = if let Some(structure) = update_course.structure {structure} else {current_course_row.structure.unwrap_or_default()};let duration: String = if let Some(duration) = update_course.duration {duration} else {current_course_row.duration.unwrap_or_default()};let level: String = if let Some(level) = update_course.level {level} else {current_course_row.level.unwrap_or_default()};let language: String = if let Some(language) = update_course.language {language} else {current_course_row.language.unwrap_or_default()};let price: i32 = if let Some(price) = update_course.price {price} else {current_course_row.price.unwrap_or_default()};let course_row = sqlx::query_as!(Course,"UPDATE course SET name = ?, description = ?, format = ?,structure = ?, duration = ?, price = ?, language = ?,level = ? where teacher_id = ? and id = ?",name,description,format,structure,duration,price,language,level,teacher_id,id).execute(pool).await;if let Ok(course) = course_row {let row = sqlx::query_as!(Course,"SELECT * FROM courseWHERE id = ?",course.last_insert_id(),).fetch_optional(pool).await?;if let Some(course) = row {Ok(course)} else {Err(MyError::NotFound("Course didn't founded".into()))}} else {Err(MyError::NotFound("Course id not found".into()))}
}

最后我们将数据库操作和事务逻辑绑定起来

use crate::db_access::course::*;
use crate::error::MyError;
use crate::models::course::{CreateCourse, UpdateCourse};
use crate::state::AppState;
use actix_web::{web, HttpResponse};pub async fn post_new_course(new_course: web::Json,app_state: web::Data,
) -> Result {post_new_course_db(&app_state.db, new_course.try_into()?).await.map(|course| HttpResponse::Ok().json(course))
}pub async fn get_courses_for_teacher(app_state: web::Data,params: web::Path,
) -> Result {// let teacher_id = i32::try_from(params.0).unwrap();let teacher_id = params.into_inner();get_courses_for_teacher_db(&app_state.db, teacher_id).await.map(|courses| HttpResponse::Ok().json(courses))
}pub async fn get_course_detail(app_state: web::Data,params: web::Path<(i32, i32)>,
) -> Result {// let teacher_id = i32::try_from(params.0).unwrap();// let course_id = i32::try_from(params.1).unwrap();let (teacher_id, course_id) = params.into_inner();get_course_details_db(&app_state.db, teacher_id, course_id).await.map(|course| HttpResponse::Ok().json(course))
}pub async fn delete_course(app_state: web::Data,params: web::Path<(i32, i32)>,
) -> Result {let (teacher_id, course_id) = params.into_inner();delete_course_db(&app_state.db, teacher_id, course_id).await.map(|resp| HttpResponse::Ok().json(resp))
}pub async fn update_course_details(app_state: web::Data,update_course: web::Json,params: web::Path<(i32, i32)>,
) -> Result {let (teacher_id, course_id) = params.into_inner();update_course_details_db(&app_state.db, teacher_id, course_id, update_course.into()).await.map(|course| HttpResponse::Ok().json(course))
}

最后我们将编写的方法绑定到路由中:

pub fn course_routes(cfg: &mut web::ServiceConfig) {cfg.service(web::scope("/courses").route("/", web::post().to(post_new_course)).route("/{teacher_id}", web::get().to(get_courses_for_teacher)).route("/{teacher_id}/{course_id}",web::get().to(get_course_detail),).route("/{teacher_id}/{course_id}", web::delete().to(delete_course)).route("/{teacher_id}/{course_id}",web::put().to(update_course_details),),);
}

创建老师的增删改查

在完成了课程的增删改查之后,我们可以继续添加新的模块,比如我们可以对老师进行增删改查,我们新建一个表:

DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher`  (`id` int(0) NOT NULL AUTO_INCREMENT,`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`picture_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`profile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of teacher
-- ----------------------------
INSERT INTO `teacher` VALUES (2, 'zhangshuai', 'www.baidu.com', 'test');
INSERT INTO `teacher` VALUES (3, 'zhangshuai', 'www.baidu.com', 'test');
INSERT INTO `teacher` VALUES (4, 'zhangshuai', 'www.baidu.com', 'test');
INSERT INTO `teacher` VALUES (5, 'zhangshuai', 'www.baidu.com', 'test');SET FOREIGN_KEY_CHECKS = 1;

之后我们依次新增 数据结构、数据库方法、事务操作、路由、最后绑定到主函数中,这样我们就有了两个独立的模块,完整的代码可以查看作者的 git :

https://github.com/aiai0603/rust_web_mysql

POST的校验

现在我们有一个完整的增删改查系统了,但是有个问题就是我们的post 请求传递来的数据可能不是合法的内容,他不一定是我们需要的 json 信息,那么我们可以在自定义错误里新增一条来定义这个错误,对于这个错误我们返回一个 BAD_REQUEST 的错误代码

pub enum MyError {DBError(String),ActixError(String),NotFound(String),InvalidInput(String),
}#[derive(Debug, Serialize)]
pub struct MyErrorResponse {error_message: String,
}impl MyError {fn error_response(&self) -> String {match self {MyError::DBError(msg) => {println!("Database error occurred: {:?}", msg);"Database error".into()}MyError::ActixError(msg) => {println!("Server error occurred: {:?}", msg);"Internal server error".into()}MyError::NotFound(msg) => {println!("Not found error occurred: {:?}", msg);msg.into()}MyError::InvalidInput(msg) => {println!("Invalid Input error occurred: {:?}", msg);msg.into()}}}
}
impl error::ResponseError for MyError {fn status_code(&self) -> StatusCode {match self {MyError::DBError(_) | MyError::ActixError(_) => StatusCode::INTERNAL_SERVER_ERROR,MyError::NotFound(_) => StatusCode::NOT_FOUND,MyError::InvalidInput(_) => StatusCode::BAD_REQUEST,}}fn error_response(&self) -> HttpResponse {HttpResponse::build(self.status_code()).json(MyErrorResponse {error_message: self.error_response(),})}
}

之后我们在主函数里加入这个错误的注册:我们在收到数据的时候,查看传递来的数据是不是 json 格式或者不合法的 json 数据,如果是的话,直接返回我们的 InvalidInput 错误,而不再进行路由的处理了

let app = move || {App::new().app_data(shared_data.clone()).app_data(web::JsonConfig::default().error_handler(|_err, _req| {MyError::InvalidInput(" please  provide valid json input".to_string()).into()})).configure(general_routes).configure(course_routes).configure(teacher_routes)};

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...