Rust Web入门(五):完整的增删改查

本教程笔记来自 杨旭老师的 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的校验

现在我们有一个完整的增删改查系统了,但是有个问题就是我们的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)
    };

你可能感兴趣的:(rust入门,rust,前端,开发语言,后端,服务器)