数据库:mongoDB
如果你更青睐 NoSQL 数据库,mongoDB 绝对是你的最佳选择。
尝试 mongoDB
- 在你的机器上安装 mongoDB 并启动它。
注意:在生产项目中记得为你的集合中字段创建所需索引。
- 添加 mongo 依赖:
cargo add mongodb
Cargo.toml 中变化:
@@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
chrono = { version = "0.4.35", features = ["serde"] }
lazy_static = "1.4.0"
+mongodb = { version = "2.8.2", features = ["sync"] }
mysql = "24.0.0"
rocket = { version = "0.5.0", features = ["json"] }
rusqlite = "0.31.0"
- 更新代码。
使用 mongoDB 执行 CRUD 操作
添加一个新的领域实体 Review
,domain/model/review.rs:
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Review {
pub id: String,
pub book_id: u32,
pub author: String,
pub title: String,
pub content: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
声明其业务能力,domain/gateway/review_manager.rs:
use std::error::Error;
use crate::domain::model;
pub trait ReviewManager: Send + Sync {
fn create_review(&self, b: &model::Review) -> Result<String, Box<dyn Error>>;
fn update_review(&self, id: &str, b: &model::Review) -> Result<(), Box<dyn Error>>;
fn delete_review(&self, id: &str) -> Result<(), Box<dyn Error>>;
fn get_review(&self, id: &str) -> Result<Option<model::Review>, Box<dyn Error>>;
fn get_reviews_of_book(&self, book_id: u32) -> Result<Vec<model::Review>, Box<dyn Error>>;
}
实现这些方法,infrastructure/database/mongo.rs:
use std::error::Error;
use mongodb::{
bson::{doc, oid::ObjectId, DateTime},
error::Error as MongoError,
sync::{Client, Collection},
};
use crate::domain::gateway::ReviewManager;
use crate::domain::model::Review;
const COLL_REVIEW: &str = "reviews";
const ID_FIELD: &str = "_id";
pub struct MongoPersistence {
coll: Collection<Review>,
}
impl MongoPersistence {
pub fn new(mongo_uri: &str, db_name: &str) -> Result<Self, MongoError> {
let client = Client::with_uri_str(mongo_uri)?;
let coll = client.database(db_name).collection::<Review>(COLL_REVIEW);
Ok(Self { coll })
}
}
impl ReviewManager for MongoPersistence {
fn create_review(&self, review: &Review) -> Result<String, Box<dyn Error>> {
let result = self.coll.insert_one(review.clone(), None)?;
let inserted_id = result
.inserted_id
.as_object_id()
.expect("Failed to extract inserted ID");
Ok(inserted_id.to_hex())
}
fn update_review(&self, id: &str, review: &Review) -> Result<(), Box<dyn Error>> {
let object_id = ObjectId::parse_str(id)?;
let update_values = doc! {
"title": &review.title,
"content": &review.content,
"updated_at": DateTime::now(),
};
let filter = doc! { ID_FIELD: object_id };
let _result = self
.coll
.update_one(filter, doc! { "$set": update_values }, None)?;
Ok(())
}
fn delete_review(&self, id: &str) -> Result<(), Box<dyn Error>> {
let object_id = ObjectId::parse_str(id)?;
self.coll.delete_one(doc! { ID_FIELD: object_id }, None)?;
Ok(())
}
fn get_review(&self, id: &str) -> Result<Option<Review>, Box<dyn Error>> {
let object_id = ObjectId::parse_str(id)?;
let filter = doc! { ID_FIELD: object_id };
let review = self.coll.find_one(filter, None)?;
if let Some(review_doc) = review {
Ok(Some(Review {
id: id.to_string(),
..review_doc
}))
} else {
Ok(None)
}
}
fn get_reviews_of_book(&self, book_id: u32) -> Result<Vec<Review>, Box<dyn Error>> {
let filter = doc! { "book_id": book_id };
let cursor = self.coll.find(filter, None)?;
let mut reviews = Vec::new();
for result in cursor {
reviews.push(result?);
}
Ok(reviews)
}
}
添加 mongodb 配置项,infrastructure/config/mod.rs:
@@ -12,6 +12,8 @@ pub struct Config {
pub struct DBConfig {
pub file_name: String,
pub dsn: String,
+ pub mongo_uri: String,
+ pub mongo_db_name: String,
}
#[derive(Debug, Deserialize, Serialize)]
添加配置值,config.toml:
@@ -4,3 +4,5 @@ port = 8080
[db]
file_name = "test.db"
dsn = "mysql://test_user:test_pass@127.0.0.1:3306/lr_book"
+mongo_uri = "mongodb://localhost:27017"
+mongo_db_name = "lr_book"
在应用层添加 review_operator
,application/executor/review_operator.rs:
use std::sync::Arc;
use chrono::Utc;
use crate::application::dto;
use crate::domain::gateway;
use crate::domain::model;
pub struct ReviewOperator {
review_manager: Arc<dyn gateway::ReviewManager>,
}
impl ReviewOperator {
pub fn new(b: Arc<dyn gateway::ReviewManager>) -> Self {
ReviewOperator { review_manager: b }
}
pub fn create_review(
&self,
body: &dto::ReviewBody,
) -> Result<model::Review, Box<dyn std::error::Error>> {
let now = Utc::now();
let review = model::Review {
id: String::new(),
book_id: body.book_id,
author: body.author.clone(),
title: body.title.clone(),
content: body.content.clone(),
created_at: now,
updated_at: now,
};
let id = self.review_manager.create_review(&review)?;
Ok(model::Review { id, ..review })
}
pub fn get_review(
&self,
id: &str,
) -> Result<Option<model::Review>, Box<dyn std::error::Error>> {
self.review_manager.get_review(id)
}
pub fn get_reviews_of_book(
&self,
book_id: u32,
) -> Result<Vec<model::Review>, Box<dyn std::error::Error>> {
self.review_manager.get_reviews_of_book(book_id)
}
pub fn update_review(
&self,
id: &str,
body: dto::ReviewBody,
) -> Result<model::Review, Box<dyn std::error::Error>> {
if body.title.is_empty() || body.content.is_empty() {
return Err("Required field cannot be empty".into());
}
let now = Utc::now();
let review = model::Review {
id: id.to_string(),
book_id: body.book_id,
author: body.author.clone(),
title: body.title.clone(),
content: body.content.clone(),
created_at: now,
updated_at: now,
};
self.review_manager.update_review(id, &review)?;
Ok(review)
}
pub fn delete_review(&self, id: &str) -> Result<(), Box<dyn std::error::Error>> {
self.review_manager.delete_review(id)
}
}
定义 ReviewBody
结构体,application/dto/review.rs:
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ReviewBody {
pub book_id: u32,
pub author: String,
pub title: String,
pub content: String,
}
调整 application/wire_helper.rs 来引入 mongodb 连接:
@@ -5,16 +5,28 @@ use crate::infrastructure::database;
use crate::infrastructure::Config;
pub struct WireHelper {
- persistence: Arc<database::MySQLPersistence>,
+ sql_persistence: Arc<database::MySQLPersistence>,
+ no_sql_persistence: Arc<database::MongoPersistence>,
}
impl WireHelper {
pub fn new(c: &Config) -> Result<Self, Box<dyn std::error::Error>> {
- let persistence = Arc::new(database::MySQLPersistence::new(&c.db.dsn)?);
- Ok(WireHelper { persistence })
+ let sql_persistence = Arc::new(database::MySQLPersistence::new(&c.db.dsn)?);
+ let no_sql_persistence = Arc::new(database::MongoPersistence::new(
+ &c.db.mongo_uri,
+ &c.db.mongo_db_name,
+ )?);
+ Ok(WireHelper {
+ sql_persistence,
+ no_sql_persistence,
+ })
}
pub fn book_manager(&self) -> Arc<dyn gateway::BookManager> {
- Arc::clone(&self.persistence) as Arc<dyn gateway::BookManager>
+ Arc::clone(&self.sql_persistence) as Arc<dyn gateway::BookManager>
+ }
+
+ pub fn review_manager(&self) -> Arc<dyn gateway::ReviewManager> {
+ Arc::clone(&self.no_sql_persistence) as Arc<dyn gateway::ReviewManager>
}
}
添加 review 相关路由,adapter/router.rs:
@@ -3,11 +3,13 @@ use rocket::response::{content, status};
use rocket::serde::json::Json;
use crate::application;
+use crate::application::dto;
use crate::application::executor;
use crate::domain::model;
pub struct RestHandler {
book_operator: executor::BookOperator,
+ review_operator: executor::ReviewOperator,
}
#[derive(serde::Serialize)]
@@ -113,8 +115,104 @@ pub fn delete_book(
}
}
+#[get("/books/<id>/reviews")]
+pub fn get_reviews_of_book(
+ rest_handler: &rocket::State<RestHandler>,
+ id: u32,
+) -> Result<Json<Vec<model::Review>>, status::Custom<Json<ErrorResponse>>> {
+ match rest_handler.review_operator.get_reviews_of_book(id) {
+ Ok(reviews) => Ok(Json(reviews)),
+ Err(err) => Err(status::Custom(
+ Status::InternalServerError,
+ Json(ErrorResponse {
+ error: err.to_string(),
+ }),
+ )),
+ }
+}
+
+#[get("/reviews/<id>")]
+pub fn get_review(
+ rest_handler: &rocket::State<RestHandler>,
+ id: &str,
+) -> Result<Json<model::Review>, status::Custom<Json<ErrorResponse>>> {
+ match rest_handler.review_operator.get_review(id) {
+ Ok(review) => match review {
+ Some(r) => Ok(Json(r)),
+ None => Err(status::Custom(
+ Status::NotFound,
+ Json(ErrorResponse {
+ error: format!("review {id} not found"),
+ }),
+ )),
+ },
+ Err(err) => Err(status::Custom(
+ Status::InternalServerError,
+ Json(ErrorResponse {
+ error: err.to_string(),
+ }),
+ )),
+ }
+}
+
+#[post("/reviews", format = "json", data = "<review>")]
+pub fn create_review(
+ rest_handler: &rocket::State<RestHandler>,
+ review: Json<dto::ReviewBody>,
+) -> Result<Json<model::Review>, status::Custom<Json<ErrorResponse>>> {
+ match rest_handler
+ .review_operator
+ .create_review(&review.into_inner())
+ {
+ Ok(b) => Ok(Json(b)),
+ Err(err) => Err(status::Custom(
+ Status::InternalServerError,
+ Json(ErrorResponse {
+ error: err.to_string(),
+ }),
+ )),
+ }
+}
+
+#[put("/reviews/<id>", format = "json", data = "<review>")]
+pub fn update_review(
+ rest_handler: &rocket::State<RestHandler>,
+ id: &str,
+ review: Json<dto::ReviewBody>,
+) -> Result<Json<model::Review>, status::Custom<Json<ErrorResponse>>> {
+ match rest_handler
+ .review_operator
+ .update_review(id, review.into_inner())
+ {
+ Ok(b) => Ok(Json(b)),
+ Err(err) => Err(status::Custom(
+ Status::InternalServerError,
+ Json(ErrorResponse {
+ error: err.to_string(),
+ }),
+ )),
+ }
+}
+
+#[delete("/reviews/<id>")]
+pub fn delete_review(
+ rest_handler: &rocket::State<RestHandler>,
+ id: &str,
+) -> Result<status::NoContent, status::Custom<Json<ErrorResponse>>> {
+ match rest_handler.review_operator.delete_review(id) {
+ Ok(_) => Ok(status::NoContent),
+ Err(err) => Err(status::Custom(
+ Status::InternalServerError,
+ Json(ErrorResponse {
+ error: err.to_string(),
+ }),
+ )),
+ }
+}
+
pub fn make_router(wire_helper: &application::WireHelper) -> RestHandler {
RestHandler {
book_operator: executor::BookOperator::new(wire_helper.book_manager()),
+ review_operator: executor::ReviewOperator::new(wire_helper.review_manager()),
}
}
所有更改已合入。让我们用 curl 来试下效果。
curl 测试
创建一个新的书评:
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"book_id": 1,
"author": "John Doe",
"title": "Great Book",
"content": "This is a great book!"
}' \
http://localhost:8000/reviews
响应如下:
{"id":"65f6e07ff274dab9361db624","book_id":1,"author":"John Doe","title":"Great Book","content":"This is a great book!","created_at":"2024-03-17 12:22:23","updated_at":"2024-03-17 12:22:23"}
根据 ID 获取单个书评:
curl -X GET http://localhost:8000/reviews/65f6e07ff274dab9361db624
结果:
{
"id": "65f6e07ff274dab9361db624",
"book_id": 1,
"author": "John Doe",
"title": "Great Book",
"content": "This is a great book!",
"created_at": "2024-03-17 12:22:23",
"updated_at": "2024-03-17 12:22:23"
}
列出某本书的所有书评:
curl -X GET http://localhost:8000/books/1/reviews
结果列表:
[
{
"id": "",
"book_id": 1,
"author": "John Doe",
"title": "Great Book",
"content": "This is a great book!",
"created_at": "2024-03-17T15:36:08.059307Z",
"updated_at": "2024-03-17T15:36:08.059307Z"
}
]
更新已有书评:
curl -X PUT \
-H "Content-Type: application/json" \
-d '{
"book_id": 1,
"author": "John Doe",
"content": "I prefer Robert Smith new book",
"title": "Not that good"
}' \
http://localhost:8000/reviews/65f6e07ff274dab9361db624
结果:
{"id":"65f6e07ff274dab9361db624","book_id":1,"author":"John Doe","title":"Not that good","content":"I prefer Robert Smith new book","created_at":"","updated_at":"2024-03-17 15:00:24"}
删除已有书评:
curl -X DELETE http://localhost:8000/reviews/65f6e07ff274dab9361db624
其返回 code 204 表示一次成功删除。
瞧!你的 API 服务器把 mongoDB 也用上啦。
Loading...
> 此处输出代码运行结果