本教程笔记来自 杨旭老师的 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<DateTime<Utc>>,
pub description: Option<String>,
pub format: Option<String>,
pub structure: Option<String>,
pub duration: Option<String>,
pub price: Option<i32>,
pub language: Option<String>,
pub level: Option<String>,
}
我们新建两个数据结构用于更新和创建数据,值得注意的是,当我们需要创建一个数据的时候,我们需要检测传入的数据是不是能转化成对应的结构,所以我们使用 TryFrom 这个 trait 来转化它:
#[derive(Deserialize, Debug, Clone, sqlx::FromRow)]
pub struct CreateCourse {
pub teacher_id: i32,
pub name: String,
pub description: Option<String>,
pub format: Option<String>,
pub structure: Option<String>,
pub duration: Option<String>,
pub price: Option<i32>,
pub language: Option<String>,
pub level: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct UpdateCourse {
pub name: Option<String>,
pub description: Option<String>,
pub format: Option<String>,
pub structure: Option<String>,
pub duration: Option<String>,
pub price: Option<i32>,
pub language: Option<String>,
pub level: Option<String>,
}
impl TryFrom<web::Json<CreateCourse>> for CreateCourse {
type Error = MyError;
fn try_from(course: web::Json<CreateCourse>) -> Result<Self, Self::Error> {
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<web::Json<UpdateCourse>> for UpdateCourse {
fn from(course: web::Json<UpdateCourse>) -> 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<Vec<Course>, MyError> {
let rows: Vec<Course> = sqlx::query_as!(
Course,
"SELECT * FROM course
WHERE 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<Course, MyError> {
let row = sqlx::query_as!(
Course,
"SELECT * FROM course
WHERE 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<Course, MyError> {
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 course
WHERE 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<String, MyError> {
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<Course, MyError> {
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 course
WHERE 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<CreateCourse>,
app_state: web::Data<AppState>,
) -> Result<HttpResponse, MyError> {
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<AppState>,
params: web::Path<i32>,
) -> Result<HttpResponse, MyError> {
// 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<AppState>,
params: web::Path<(i32, i32)>,
) -> Result<HttpResponse, MyError> {
// 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<AppState>,
params: web::Path<(i32, i32)>,
) -> Result<HttpResponse, MyError> {
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<AppState>,
update_course: web::Json<UpdateCourse>,
params: web::Path<(i32, i32)>,
) -> Result<HttpResponse, MyError> {
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 请求传递来的数据可能不是合法的内容,他不一定是我们需要的 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)
};