Getting Started with Rust Web (3): Connecting to the Database

The notes of this tutorial come from the rust web full-stack tutorial of Mr. Yang Xu, the link is as follows:

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

To learn Rust Web, you need to learn the pre-knowledge of rust. You can learn another course from Mr. Yang Xu

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

The source code of the project can be viewed in git: (note that the author uses the mysql database instead of the database of the original tutorial)

https://github.com/aiai0603/rust_web_mysql

Let's get started today how to connect to the database based on the rust-based web:

Connect to the database demo

First of all, we need to download the database locally. Here we take mysql as an example (note that the operation here is different from Mr. Yang’s complete database, because the author only has a mysql database locally. You can also check the information and use other databases yourself), and then we create a new project and download the dependencies we need:

[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",
]}

Here we use a dotenv package. Its function is to get the system variables you need in .envthis We create a new .envfile in the root directory, and then write our statement to connect to the database.

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

Then we call it in main.rs to call the system variable we just defined and connect to our database

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

After that, we create a new table in the database, which stores some information, and we can use such SQL to build the table:

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');

Then we write sql statements to query the data:

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

Run our project, if you see some data printed out, it means that our demo of connecting to the database query is successful, and then we will change our previous project with an interface to a project with database persistence

Rewrite our project with a database

Let's add our dependencies first:

[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",
] }

Then we rewrite our state.rs, we put a data structure in it to simulate our database, now we use a real database, so we don't need this data structure anymore, but we need to connect our database in there, since a database connection is required at various places in the project:

// 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>>,
}

Then we need to inject our connection into the project when the project starts. We come to the teacher-service.rs file, first write the .env file to store the connection, then read the content, establish a database connection, and inject it into the project :

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
}

Then we modify the data structure in our model.rs to store data in our database:

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

Then we write a db_access.rs file, which stores the database operations we need, including adding, deleting, modifying, and checking. The logic is consistent with the demo written before, and then we need to introduce this part into the main function:

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

Finally, we call the function to operate the database just written in handlers.rs to rewrite:

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

You can write some tests to verify that your functions are correct, run the tests, and if all your tests pass, your project is written successfully

#[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);
    }
}

Now you can start our project, and then use a browser to enter the corresponding path to test the interface, or use a tool like POSTMAN to test adding data, etc.

Guess you like

Origin blog.csdn.net/weixin_46463785/article/details/129218227