本教程笔记来自 杨旭老师的 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 怎么样连接数据库:
首先我们需要在我们的本地先下载好数据库。这里以 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>,
}
之后我们需要将我们的连接在项目启动的时候注入到项目中,我们来到 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 这样的工具来测试添加数据等。