Rust Web 全栈开发之在 Web 项目中使用数据库
Rust Web 全栈开发之在 Web 项目中使用数据库
目录
ws on master [?] via 🦀 1.67.1 via 🅒 base
➜ tree -a -I target
.
├── .git
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── src
│ └── main.rs
└── webservice
├── Cargo.toml
├── .env
└── src
├── bin
│ ├── server1.rs
│ └── teacher-service.rs
├── handlers.rs
├── main.rs
├── models.rs
├── routers.rs
└── state.rs
14 directories, 18 files
ws on master [?] via 🦀 1.67.1 via 🅒 base
➜
webservice/Cargo.toml
[package]
name = "webservice"
version = "0.1.0"
edition = "2021"
default-run = "teacher-service"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "4.3.1"
actix-rt = "2.8.0"
dotenv = "0.15.0"
serde = { version = "1.0.163", features = ["derive"] }
chrono = { version = "0.4.24", features = ["serde"] }
openssl = { version = "0.10.52", features = ["vendored"] }
sqlx = { version = "0.6.3", default_features = false, features = [
"postgres",
"runtime-tokio-rustls",
"macros",
"chrono",
] }
[[bin]]
name = "server1"
[[bin]]
name = "teacher-service"
webservice/src/state.rs
// use super::models::Course;
use sqlx::postgres::PgPool;
use std::sync::Mutex;
pub struct AppState {
pub health_check_response: String,
pub visit_count: Mutex<u32>,
// pub courses: Mutex<Vec<Course>>,
pub db: PgPool,
}
webservice/src/bin/teacher-service.rs
use actix_web::{web, App, HttpServer};
use dotenv::dotenv;
use sqlx::postgres::PgPoolOptions;
use std::env;
use std::io;
use std::sync::Mutex;
#[path = "../handlers.rs"]
mod handlers;
#[path = "../models.rs"]
mod models;
#[path = "../routers.rs"]
mod routers;
#[path = "../state.rs"]
mod state;
use routers::*;
use state::AppState;
#[actix_rt::main]
async fn main() -> io::Result<()> {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set.");
let db_pool = PgPoolOptions::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),
// courses: Mutex::new(vec![]),
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
}
webservice/src/handlers.rs
use super::state::AppState;
use actix_web::{web, HttpResponse};
pub async fn health_check_handler(app_state: web::Data<AppState>) -> HttpResponse {
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;
use chrono::Utc;
pub async fn new_course(
new_course: web::Json<Course>,
app_state: web::Data<AppState>,
) -> HttpResponse {
HttpResponse::Ok().json("Success")
}
pub async fn get_courses_for_tescher(
app_state: web::Data<AppState>,
params: web::Path<usize>,
) -> HttpResponse {
HttpResponse::Ok().json("Success")
}
pub async fn get_courses_detail(
app_state: web::Data<AppState>,
params: web::Path<(usize, usize)>,
) -> HttpResponse {
HttpResponse::Ok().json("Success")
}
webservice/src/.env
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/postgres
检查
ws on master [?] via 🦀 1.67.1 via 🅒 base
➜ cargo check --bin teacher-service
warning: unused import: `chrono::Utc`
--> webservice/src/bin/../handlers.rs:13:5
|
13 | use chrono::Utc;
| ^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: unused variable: `new_course`
--> webservice/src/bin/../handlers.rs:16:5
|
16 | new_course: web::Json<Course>,
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_new_course`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `app_state`
--> webservice/src/bin/../handlers.rs:17:5
|
17 | app_state: web::Data<AppState>,
| ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_app_state`
warning: unused variable: `app_state`
--> webservice/src/bin/../handlers.rs:23:5
|
23 | app_state: web::Data<AppState>,
| ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_app_state`
warning: unused variable: `params`
--> webservice/src/bin/../handlers.rs:24:5
|
24 | params: web::Path<usize>,
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_params`
warning: unused variable: `app_state`
--> webservice/src/bin/../handlers.rs:30:5
|
30 | app_state: web::Data<AppState>,
| ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_app_state`
warning: unused variable: `params`
--> webservice/src/bin/../handlers.rs:31:5
|
31 | params: web::Path<(usize, usize)>,
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_params`
warning: `webservice` (bin "teacher-service") generated 7 warnings (run `cargo fix --bin "teacher-service"` to apply 7 suggestions)
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
ws on master [?] via 🦀 1.67.1 via 🅒 base
➜
webservice/src/handlers.rs
use super::state::AppState;
use actix_web::{web, HttpResponse};
pub async fn health_check_handler(app_state: web::Data<AppState>) -> HttpResponse {
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;
use chrono::Utc;
pub async fn new_course(
new_course: web::Json<Course>,
app_state: web::Data<AppState>,
) -> HttpResponse {
HttpResponse::Ok().json("Success")
}
pub async fn get_courses_for_tescher(
app_state: web::Data<AppState>,
params: web::Path<usize>,
) -> HttpResponse {
HttpResponse::Ok().json("Success")
}
pub async fn get_courses_detail(
app_state: web::Data<AppState>,
params: web::Path<(usize, usize)>,
) -> HttpResponse {
HttpResponse::Ok().json("Success")
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::http::StatusCode;
use chrono::NaiveDateTime;
use dotenv::dotenv;
use sqlx::postgres::PgPoolOptions;
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 is not set");
let db_pool = PgPoolOptions::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: None,
time: None,
});
let resp = new_course(course, app_state).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn get_all_course_success() {
dotenv().ok();
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set");
let db_pool = PgPoolOptions::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_tescher(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 is not set");
let db_pool = PgPoolOptions::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_courses_detail(app_state, params).await;
assert_eq!(resp.status(), StatusCode::OK);
}
}
测试
ws on master [?] via 🦀 1.67.1 via 🅒 base
➜ cargo test --bin teacher-service
warning: unused import: `chrono::Utc`
--> webservice/src/bin/../handlers.rs:13:5
|
13 | use chrono::Utc;
| ^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: unused import: `chrono::NaiveDateTime`
--> webservice/src/bin/../handlers.rs:40:9
|
40 | use chrono::NaiveDateTime;
| ^^^^^^^^^^^^^^^^^^^^^
warning: unused variable: `new_course`
--> webservice/src/bin/../handlers.rs:16:5
|
16 | new_course: web::Json<Course>,
| ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_new_course`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `app_state`
--> webservice/src/bin/../handlers.rs:17:5
|
17 | app_state: web::Data<AppState>,
| ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_app_state`
warning: unused variable: `app_state`
--> webservice/src/bin/../handlers.rs:23:5
|
23 | app_state: web::Data<AppState>,
| ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_app_state`
warning: unused variable: `params`
--> webservice/src/bin/../handlers.rs:24:5
|
24 | params: web::Path<usize>,
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_params`
warning: unused variable: `app_state`
--> webservice/src/bin/../handlers.rs:30:5
|
30 | app_state: web::Data<AppState>,
| ^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_app_state`
warning: unused variable: `params`
--> webservice/src/bin/../handlers.rs:31:5
|
31 | params: web::Path<(usize, usize)>,
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_params`
warning: `webservice` (bin "teacher-service" test) generated 8 warnings (run `cargo fix --bin "teacher-service" --tests` to apply 8 suggestions)
Finished test [unoptimized + debuginfo] target(s) in 0.22s
Running unittests src/bin/teacher-service.rs (target/debug/deps/teacher_service-32d6a48d6ee3c4b4)
running 3 tests
test handlers::tests::get_all_course_success ... ok
test handlers::tests::get_one_course_success ... ok
test handlers::tests::post_course_test ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s
ws on master [?] via 🦀 1.67.1 via 🅒 base
➜
项目目录
ws on master [?] via 🦀 1.67.1 via 🅒 base
➜ tree -a -I target
.
├── .git
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── .env
├── src
│ └── main.rs
└── webservice
├── Cargo.toml
└── src
├── bin
│ ├── server1.rs
│ └── teacher-service.rs
├── db_access.rs
├── handlers.rs
├── main.rs
├── models.rs
├── routers.rs
└── state.rs
14 directories, 19 files
ws on master [?] via 🦀 1.67.1 via 🅒 base
webservice/src/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 {
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_tescher(
app_state: web::Data<AppState>,
params: web::Path<usize>,
) -> HttpResponse {
let teacher_id = i32::try_from(params.into_inner()).unwrap();
let courses = get_courses_for_teacher_db(&app_state.db, teacher_id).await;
HttpResponse::Ok().json(courses)
}
pub async fn get_courses_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_courses_detail_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 dotenv::dotenv;
use sqlx::postgres::PgPoolOptions;
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 is not set");
let db_pool = PgPoolOptions::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), // serial
time: None,
});
let resp = new_course(course, app_state).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn get_all_course_success() {
dotenv().ok();
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set");
let db_pool = PgPoolOptions::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_tescher(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 is not set");
let db_pool = PgPoolOptions::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_courses_detail(app_state, params).await;
assert_eq!(resp.status(), StatusCode::OK);
}
}
webservice/src/bin/teacher-service.rs
use actix_web::{web, App, HttpServer};
use dotenv::dotenv;
use sqlx::postgres::PgPoolOptions;
use std::env;
use std::io;
use std::sync::Mutex;
#[path = "../db_access.rs"]
mod db_access;
#[path = "../handlers.rs"]
mod handlers;
#[path = "../models.rs"]
mod models;
#[path = "../routers.rs"]
mod routers;
#[path = "../state.rs"]
mod state;
use routers::*;
use state::AppState;
#[actix_rt::main]
async fn main() -> io::Result<()> {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set.");
let db_pool = PgPoolOptions::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),
// courses: Mutex::new(vec![]),
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
}
webservice/src/db_access.rs
use super::models::*;
use chrono::NaiveDateTime;
use sqlx::postgres::PgPool;
pub async fn get_courses_for_teacher_db(pool: &PgPool, teacher_id: i32) -> Vec<Course> {
let rows = sqlx::query!(
r#"SELECT id, teacher_id, name, time FROM course WHERE teacher_id = $1"#,
teacher_id
)
.fetch_all(pool)
.await
.unwrap();
rows.iter()
.map(|row| Course {
id: Some(row.id),
teacher_id: row.teacher_id,
name: row.name.clone(),
time: Some(NaiveDateTime::from(row.time.unwrap())),
})
.collect()
}
pub async fn get_courses_detail_db(pool: &PgPool, teacher_id: i32, course_id: i32) -> Course {
let row = sqlx::query!(
r#"SELECT id, teacher_id, name, time FROM course WHERE teacher_id = $1 and id = $2"#,
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(NaiveDateTime::from(row.time.unwrap())),
}
}
pub async fn post_new_course_db(pool: &PgPool, new_course: Course) -> Course {
let row = sqlx::query!(
r#"INSERT INTO course (id, teacher_id, name) VALUES ($1, $2, $3)
RETURNING id, teacher_id, name, time"#,
new_course.id,
new_course.teacher_id,
new_course.name
)
.fetch_one(pool)
.await
.unwrap();
Course {
id: Some(row.id),
teacher_id: row.teacher_id,
name: row.name.clone(),
time: Some(NaiveDateTime::from(row.time.unwrap())),
}
}
webservice/src/models.rs
use actix_web::web;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Course {
pub teacher_id: i32,
pub id: Option<i32>,
pub name: String,
pub time: Option<NaiveDateTime>,
}
impl From<web::Json<Course>> for Course {
fn from(course: web::Json<Course>) -> Self {
Course {
teacher_id: course.teacher_id,
id: course.id,
name: course.name.clone(),
time: course.time,
}
}
}
测试
ws on master [?] via 🦀 1.67.1 via 🅒 base
➜ cargo test --bin teacher-service
Compiling webservice v0.1.0 (/Users/qiaopengjun/rust/ws/webservice)
Finished test [unoptimized + debuginfo] target(s) in 1.61s
Running unittests src/bin/teacher-service.rs (target/debug/deps/teacher_service-32d6a48d6ee3c4b4)
running 3 tests
test handlers::tests::get_one_course_success ... ok
test handlers::tests::get_all_course_success ... ok
test handlers::tests::post_course_test ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
ws on master [?] via 🦀 1.67.1 via 🅒 base took 2.7s
➜
运行
ws on master [?] via 🦀 1.67.1 via 🅒 base took 3.1s
➜ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/teacher-service`
测试
ws on master [?] via 🦀 1.67.1 via 🅒 base took 2.7s
➜ curl -X POST localhost:3000/courses/ -H "Content-Type: application/json" -d '{"teacher_id":1, "id":4, "name":"Calculus"}'
{"teacher_id":1,"id":4,"name":"Calculus","time":"2023-05-29T15:51:52.245753"}%
ws on master [?] via 🦀 1.67.1 via 🅒 base
➜
本文来自博客园,作者:寻月隐君,转载请注明原文链接:https://www.cnblogs.com/QiaoPengjun/p/17442063.html