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

今天来入门基于 rust 的 web 怎么样连接数据库:

连接数据库 demo

首先我们需要在我们的本地先下载好数据库。这里以 mysql 为例(注意 ,这里的操作和杨老师的完全数据库不同,因为笔者本地只有 mysql 数据库。你也可以自行查阅资料使用其他数据库),之后我们新建一个项目,下载我们需要的依赖:

[dependencies]
actix-rt="2.6.0"
actix-web="4.1.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",
]}

这里我们使用了一个 dotenv 这个包,他的作用就是可以在运行的时候,在 .env 这个包里面拿到你需要的系统变量,我们在根目录新建有一个 .env 文件,然后写入我们连接数据库的语句

DATABASE_URL=mysql://你的用户名:你的密码@数据库地址:数据库端口/数据库名

然后我们在 main.rs 里调用来调用我们刚刚定义的系统变量,连接我们的数据库

use dotenv::dotenv;
use sqlx::mysql::MySqlPoolOptions;
use std::env;
use std::io;

#[actix_rt::main]
async fn main() -> io::Result<()> {
    
    
    dotenv().ok();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL 没有在 .env 文件里设置");
    let db_pool = MySqlPoolOptions::new()
        .connect(&database_url)
        .await
        .unwrap();
}

之后我们在数据库里面新建一张表,里面存放一些信息,可以用这样的 sql 来建表:

drop table if exists course;


create table course (
       id serial primary key, 
       teacher_id INT not null,
       name varchar(140) not null,
       time TIMESTAMP default now()
);


insert into course (id, teacher_id, name, time)
values(1,
       1,
       'First course',
       '2022-01-17 05:40:00');


insert into course (id, teacher_id, name, time)
values(2,
       1,
       'Second course',
       '2022-01-18 05:45:00');

之后我们编写 sql 语句将数据查询出来:

use dotenv::dotenv;
use sqlx::mysql::MySqlPoolOptions;
use std::env;
use std::io;

#[derive(Debug)]
pub struct Course {
    
    
    pub id: u64,
    pub teacher_id: i32,
    pub name: String,
}

#[actix_rt::main]
async fn main() -> io::Result<()> {
    
    
    dotenv().ok();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL 没有在 .env 文件里设置");
    let db_pool = MySqlPoolOptions::new()
        .connect(&database_url)
        .await
        .unwrap();

    println!("db_pool is : {:?}", db_pool);
    let course_rows = sqlx::query!(
        "select id, teacher_id, name, time from course where id = ? ",
        1
    )
    .fetch_all(&db_pool)
    .await
    .unwrap();

    let mut courses_list = vec![];
    for row in course_rows {
    
    
        courses_list.push(Course {
    
    
            id: row.id,
            teacher_id: row.teacher_id,
            name: row.name
        })
    }
    println!("Courses = {:?}", courses_list);

    Ok(())
}

运行我们的项目,如果看到打印出了一些数据,说明我们的连接数据库查询的 demo 成功了,之后我们将我们之前的具有接口的项目改成数据库持久化的项目

用数据库改写我们的项目

我们还是首先添加我们的依赖:

[dependencies]
actix-rt = "2.6.0"
actix-web = "4.0.0"
chrono = { version = "0.4.19", features = ["serde"] }
dotenv = "0.15.0"
# openssl = { version = "0.10.38", features = ["vendored"] }
serde = { version = "1.0.134", features = ["derive"] }
sqlx = { version = "0.5.10", features = [
    "mysql",
    "runtime-tokio-rustls",
    "macros",
    "chrono",
] }

之后我们将我们的 state.rs 改写,之前我们将一个数据结构放到其中来模拟我们的数据库,现在使用真正的数据库了,所以我们不再需要这个数据结构了,但是我们需要将我们的数据库连接放在其中,因为在项目的各个位置都需要一个数据库连接:

// use crate::modelds::Course;
use sqlx::MySqlPool;
use std::sync::Mutex;

pub struct AppState {
    
    
    pub health_check_response: String,
    pub visit_count: Mutex<u32>,
    pub db: MySqlPool,
    // pub courses: Mutex<Vec<Course>>,
}

之后我们需要将我们的连接在项目启动的时候注入到项目中,我们来到 teacher-service.rs 这个文件,先编写 .env 文件存放连接,然后读取内容,建立数据库连接,并且注入到项目中:

async fn main() -> io::Result<()> {
    
    
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL 没有在 .env 文件里设置");

    let db_pool = MySqlPoolOptions::new()
        .connect(&database_url)
        .await
        .unwrap();

    let shared_data = web::Data::new(AppState {
    
    
        health_check_response: "I'm OK.".to_string(),
        visit_count: Mutex::new(0),
        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
}

之后我们修改我们 model.rs 里的数据结构为我们数据库存储数据的结构:

use actix_web::web;
use chrono::{
    
    DateTime, Utc};
use serde::{
    
    Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Course {
    
    
    pub id: Option<u64>,
    pub teacher_id: i32,
    pub name: String,
    pub time: Option<DateTime<Utc>>,
}

impl From<web::Json<Course>> for Course {
    
    
    fn from(course: web::Json<Course>) -> Self {
    
    
        Course {
    
    
            id: course.id,
            teacher_id: course.teacher_id,
            name: course.name.clone(),
            time: course.time,
        }
    }
}

之后我们编写一个 db_access.rs 文件,里面存放我们需要的数据库操作,包括增删改查,逻辑和之前编写的 demo 一致,然后我们需要将这个部分也引入到主函数中:

use crate::models::*;
use sqlx::mysql::{
    
    MySqlPool};

pub async fn get_courses_for_teacher_db(pool: &MySqlPool, teacher_id: i32) -> Vec<Course> {
    
    
    let rows = sqlx::query!(
        "SELECT id, teacher_id, name, time
        FROM course
        WHERE teacher_id = ?",
        teacher_id
    )
    .fetch_all(pool)
    .await
    .unwrap();

    rows.iter()
        .map(|r| Course {
    
    
            id: Some(r.id),
            teacher_id: r.teacher_id,
            name: r.name.clone(),
            time: Some(r.time.unwrap()),
        })
        .collect()
}

pub async fn get_course_details_db(pool: &MySqlPool, teacher_id: i32, course_id: i32) -> Course {
    
    
    let row = sqlx::query!(
        "SELECT id, teacher_id, name, time
            FROM course
            WHERE teacher_id = ? and id = ?",
        teacher_id,
        course_id
    )
    .fetch_one(pool)
    .await
    .unwrap();

    Course {
    
    
        id: Some(row.id),
        teacher_id: row.teacher_id,
        name: row.name.clone(),
        time: Some(row.time.unwrap()),
    }
}

pub async fn post_new_course_db(pool: &MySqlPool, new_course: Course) -> Course {
    
    
    let data = sqlx::query!(
        "INSERT INTO course ( teacher_id, name)
            VALUES ( ?, ?)",
        new_course.teacher_id,
        new_course.name,
    )
    .execute(pool)
    .await
    .unwrap();
    let row = sqlx::query!(
        "SELECT id, teacher_id, name, time
        FROM course
        WHERE id = ?",
        data.last_insert_id()
    )
    .fetch_one(pool)
    .await
    .unwrap();

    Course {
    
    
        id: Some(row.id),
        teacher_id: row.teacher_id,
        name: row.name.clone(),
        time: Some(row.time.unwrap()),
    }
}

最后,我们在 handlers.rs 中调用刚刚编写的操作数据库的函数来进行改写:

use super::db_access::*;
use super::state::AppState;
use actix_web::{
    
    web, HttpResponse};

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)
}

use super::models::Course;
pub async fn new_course(
    new_course: web::Json<Course>,
    app_state: web::Data<AppState>,
) -> HttpResponse {
    
    
    let course = post_new_course_db(&&app_state.db, new_course.into()).await;
    HttpResponse::Ok().json(course)
}

pub async fn get_courses_for_teacher(
    app_state: web::Data<AppState>,
    params: web::Path<(usize,)>,
) -> HttpResponse {
    
    
    let teacher_id = i32::try_from(params.0).unwrap();
    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<(usize, usize)>,
) -> HttpResponse {
    
    
    let teacher_id = i32::try_from(params.0).unwrap();
    let course_id = i32::try_from(params.1).unwrap();
    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 chrono::NaiveDateTime;
    use dotenv::dotenv;
    use sqlx::mysql::MySqlPoolOptions;
    use std::env;
    use std::sync::Mutex;

    #[actix_rt::test]
    async fn post_course_test() {
    
    
        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 course = web::Json(Course {
    
    
            teacher_id: 1,
            name: "Test course".into(),
            id: Some(3),
            time: None,
        });

        let resp = new_course(course, app_state).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }

    #[actix_rt::test]
    async fn get_all_courses_success() {
    
    
        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<(usize,)> = web::Path::from((1,));
        let resp = get_courses_for_teacher(app_state, teacher_id).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }

    #[actix_rt::test]
    async fn get_one_course_success() {
    
    
        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<(usize, usize)> = web::Path::from((1, 1));
        let resp = get_course_detail(app_state, params).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }
}

现在你可以启动我们的项目,之后使用浏览器输入对应的路径来测试接口,或者使用 POSTMAN 这样的工具来测试添加数据等。

猜你喜欢

转载自blog.csdn.net/weixin_46463785/article/details/129218227
今日推荐