cd rust-blog

在此之后,我們需要刪除默認(rèn)的src文件夾。因為我們將按照Clean Architecture的原則重新組織項目結(jié)構(gòu)。接下來,我們將為架構(gòu)中的每一層創(chuàng)建一個新的Rust項目或模塊。我們的架構(gòu)將遵循以下結(jié)構(gòu):

cargo new api --lib
cargo new application --lib
cargo new domain --lib
cargo new infrastructure --lib
cargo new shared --lib

到最后,我們的項目目錄結(jié)構(gòu)應(yīng)該類似于以下形式:

.
├── Cargo.lock
├── Cargo.toml
├── api
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── application
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── domain
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── infrastructure
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── shared
├── Cargo.toml
└── src
└── lib.rs

現(xiàn)在我們需要在項目的根目錄下的 Cargo.toml 文件中整合所有子項目。請清空該文件中的所有內(nèi)容,并按照以下格式輸入:

[workspace]
members = [
"api",
"domain",
"infrastructure",
"application",
"shared",
]

很好!我們的模板已經(jīng)接近完成,現(xiàn)在我們可以開始享受實際操作的樂趣了。

遷移

由于我們選擇使用 Diesel.rs 作為數(shù)據(jù)庫管理工具,我們需要安裝它的 CLI 工具。Diesel CLI 有一些依賴項,這些依賴項取決于您計劃使用的數(shù)據(jù)庫類型:

在這個項目中,我們將使用 PostgreSQL,所以我們只需要關(guān)注 libpq。請查閱相關(guān)文檔,了解如何在您的操作系統(tǒng)上安裝這些依賴項。

安裝好 libpq 之后,我們可以通過以下命令來安裝 Diesel CLI:

cargo install diesel_cli --no-default-features --features postgres

安裝完成后,我們需要設(shè)置一個連接字符串,以便連接到數(shù)據(jù)庫。在項目的根目錄下,運行以下命令,并包含您的連接信息:

echo DATABASE_URL=postgres://username:password@localhost/blog > .env

現(xiàn)在,Diesel CLI 已經(jīng)準(zhǔn)備好幫助我們完成繁重的工作了。進入 infrastructure 文件夾,然后運行以下命令:

diesel setup

這將生成一些文件和文件夾:

接下來,使用 Diesel CLI 工具創(chuàng)建一個新的遷移,用于初始化帖子表。

Diesel CLI 會生成一個新的遷移文件,文件名類似于 2022–11–18–090125_create_posts。文件名的第一部分是生成遷移的日期和唯一代碼,后面跟著遷移的名稱。在這個遷移文件夾中,有兩個文件:up.sql 和 down.sql,前者告訴 Diesel CLI 在遷移時需要執(zhí)行哪些操作,后者告訴 Diesel CLI 如何撤銷這些操作。

現(xiàn)在,我們可以開始為遷移編寫 SQL 代碼了。

-- up.sql

CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
body TEXT NOT NULL,
genre VARCHAR NOT NULL,
published BOOLEAN NOT NULL DEFAULT false
)
-- down.sql

DROP TABLE posts

使用 Diesel CLI,我們可以輕松地應(yīng)用剛才創(chuàng)建的新遷移。

diesel migration run

有關(guān)使用 Diesel.rs 運行遷移的更多信息,請訪問官方入門指南

創(chuàng)建連接

在完成第一組遷移并搭建好項目架構(gòu)之后,接下來我們來編寫一些 Rust 代碼,以便將我們的應(yīng)用程序連接到數(shù)據(jù)庫。

# infrastructure/Cargo.toml

[package]
name = "infrastructure"
version = "0.1.0"
edition = "2021"

[dependencies]
diesel = { version = "2.0.0", features = ["postgres"] }
dotenvy = "0.15"
// infrastructure/src/lib.rs

use diesel::pg::PgConnection;
use diesel::prelude::*;
use dotenvy::dotenv;
use std::env;

pub fn establish_connection() -> PgConnection {
dotenv().ok();

let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set.");

PgConnection::establish(&database_url).unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}


建立數(shù)據(jù)庫連接后,下一步是為數(shù)據(jù)庫創(chuàng)建一些模型,具體來說就是 Post 和 NewPost。。

模型與架構(gòu)

首先,我們需要導(dǎo)航到 domain 目錄,并將以下模塊添加到 lib.rs 文件中。

// domain/src/lib.rs

pub mod models;
pub mod schema;

我們將利用模型來定義數(shù)據(jù)庫的結(jié)構(gòu)以及代碼中將使用的結(jié)構(gòu),而 schema 將由 Diesel CLI 自動生成。在我們生成遷移時,Diesel CLI 在 schema.rs 中創(chuàng)建了一個名為 infrastructure 的文件。請將這個文件移動到 #5 位置。如果出于某種原因沒有生成 domain/src 目錄,您可以在終端中運行 schema.rs 來查看模式。

# domain/Cargo.toml

[package]
name = "domain"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = { version = "0.5.0-rc.2", features = ["json"] }
diesel = { version = "2.0.0", features = ["postgres"] }
serde = { version = "1.0.147", features = ["derive"] }
// domain/src/models.rs

use crate::schema::posts;
use diesel::prelude::*;
use rocket::serde::{Deserialize, Serialize};
use std::cmp::{Ord, Eq, PartialOrd, PartialEq};

// Queryable will generate the code needed to load the struct from an SQL statement
#[derive(Queryable, Serialize, Ord, Eq, PartialEq, PartialOrd)]
pub struct Post {
pub id: i32,
pub title: String,
pub body: String,
pub genre: String,
pub published: bool,
}

#[derive(Insertable, Deserialize)]
#[serde(crate = "rocket::serde")]
#[diesel(table_name = posts)]
pub struct NewPost {
pub title: String,
pub body: String,
pub genre: String,
}
// domain/src/schema.rs

// @generated automatically by Diesel CLI.

diesel::table! {
posts (id) {
id -> Int4,
title -> Varchar,
body -> Text,
genre -> Varchar,
published -> Bool,
}
}

schema.rs 中的代碼可能有所差異,但其核心概念是一致的。每次我們運行或回滾遷移時,這個文件都會得到更新。重要的是要確保我們的 Post 結(jié)構(gòu)體和 posts 表中的字段順序是一致的。

在定義數(shù)據(jù)庫模型的同時,我們也應(yīng)該創(chuàng)建一個模型來規(guī)范如何格式化 API 響應(yīng)。請導(dǎo)航到 shared/src 目錄,并在那里創(chuàng)建一個新的文件 response_models.rs

# shared/Cargo.toml

[package]
name = "shared"
version = "0.1.0"
edition = "2021"

[dependencies]
domain = { path = "../domain" }

rocket = { version = "0.5.0-rc.2", features = ["json"] }
serde = { version = "1.0.147", features = ["derive"] }
// shared/src/lib.rs

pub mod response_models;
// shared/src/response_models.rs

use domain::models::Post;
use rocket::serde::Serialize;

#[derive(Serialize)]
pub enum ResponseBody {
Message(String),
Post(Post),
Posts(Vec<Post>)
}

#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
pub struct Response {
pub body: ResponseBody,
}

ResponseBody 枚舉將用于定義我們的API可以返回哪些類型的數(shù)據(jù),而 Response 結(jié)構(gòu)體將定義如何構(gòu)造這些響應(yīng)。

配置 Rocket.rs

哇,我們已經(jīng)為數(shù)據(jù)庫做了很多設(shè)置工作,這一切都是為了確保我們的項目保持最新狀態(tài)。以下是我們項目結(jié)構(gòu)的當(dāng)前狀況:

.
├── Cargo.lock
├── Cargo.toml
├── api
│ └── ...
├── application
│ └── ...
├── domain
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ └── models.rs
├── infrastructure
│ ├── Cargo.toml
│ ├── migrations
│ │ └── 2022–11–18–090125_create_posts
│ │ ├── up.sql
│ │ └── down.sql
│ └── src
│ ├── lib.rs
│ └── schema.rs
└── shared
├── Cargo.toml
└── src
├── lib.rs
└── response_models.rs

完成大部分?jǐn)?shù)據(jù)庫設(shè)置后,讓我們開始設(shè)置項目的 API 部分。

導(dǎo)航到 api 并導(dǎo)入以下依賴項:

# api/Cargo.toml

[package]
name = "api"
version = "0.1.0"
edition = "2021"

[dependencies]
domain = { path = "../domain" }
application = { path = "../application" }
shared = { path = "../shared" }

rocket = { version = "0.5.0-rc.2", features = ["json"] }
serde_json = "1.0.88"

設(shè)置了依賴項和對其他文件夾的引用后,讓我們創(chuàng)建一個 bin 文件夾來保存 main.rs

.
└── api
├── Cargo.toml
└── src
├── bin
│ └── main.rs
└── lib.rs

main.rs 將作為我們API的入口點,也就是我們定義計劃使用的路由的地方。在構(gòu)建應(yīng)用程序的過程中,我們將逐一定義這些路由。

// api/src/lib.rs

pub mod post_handler;
// api/src/bin/main.rs

#[macro_use] extern crate rocket;
use api::post_handler;

#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/api", routes![
post_handler::list_posts_handler,
post_handler::list_post_handler,
])
}

我們將通過 post_handler.rs 來定義路由的具體實現(xiàn)。為了避免在開發(fā)過程中持續(xù)遇到LSP(Language Server Protocol)的錯誤提示,我們將使用 todo!() 宏來告知Rust這些函數(shù)或路由尚未完成。

post_handler.rs 中,我們將創(chuàng)建一個新的文件 src,并編寫以下模板代碼:

// api/src/post_handler.rs

use shared::response_models::{Response, ResponseBody};
use application::post::{read};
use domain::models::{Post};
use rocket::{get};
use rocket::response::status::{NotFound};
use rocket::serde::json::Json;

#[get("/")]
pub fn list_posts_handler() -> String {
todo!()
}

#[get("/post/<post_id>")]
pub fn list_post_handler(post_id: i32) -> Result<String, NotFound<String>> {
todo!()
}

這里我們定義了兩個API請求:

處理 API 邏輯

在請求處理程序模板化之后,我們現(xiàn)在來編寫路由所需的邏輯。在application內(nèi)部,我們將創(chuàng)建一個名為post的新文件夾。這個文件夾將包含處理每個路由邏輯的文件。

# application/Cargo.toml

[package]
name = "application"
version = "0.1.0"
edition = "2021"

[dependencies]
domain = { path = "../domain" }
infrastructure = { path = "../infrastructure" }
shared = { path = "../shared" }

diesel = { version = "2.0.0", features = ["postgres"] }
serde_json = "1.0.88"
rocket = { version = "0.5.0-rc.2", features = ["json"] }
// application/src/lib.rs

pub mod post;
// application/src/post/mod.rs

pub mod read;
// application/src/post/read.rs

use domain::models::Post;
use shared::response_models::{Response, ResponseBody};
use infrastructure::establish_connection;
use diesel::prelude::*;
use rocket::response::status::NotFound;

pub fn list_post(post_id: i32) -> Result<Post, NotFound<String>> {
use domain::schema::posts;

match posts::table.find(post_id).first::<Post>(&mut establish_connection()) {
Ok(post) => Ok(post),
Err(err) => match err {
diesel::result::Error::NotFound => {
let response = Response { body: ResponseBody::Message(format!("Error selecting post with id {} - {}", post_id, err))};
return Err(NotFound(serde_json::to_string(&response).unwrap()));
},
_ => {
panic!("Database error - {}", err);
}
}
}
}

pub fn list_posts() -> Vec<Post> {
use domain::schema::posts;

match posts::table.select(posts::all_columns).load::<Post>(&mut establish_connection()) {
Ok(mut posts) => {
posts.sort();
posts
},
Err(err) => match err {
_ => {
panic!("Database error - {}", err);
}
}
}
}

請注意,在使用Rocket.rs框架時,panic!() 會導(dǎo)致返回500 Internal Server Error狀態(tài)碼,而不會直接導(dǎo)致程序崩潰。

完成路由邏輯編寫后,讓我們回到我們的帖子處理程序,以完善我們的兩個GET路由。

// api/src/post_handler.rs

// ...

#[get("/")]
pub fn list_posts_handler() -> String {
// ?? New function body!
let posts: Vec<Post> = read::list_posts();
let response = Response { body: ResponseBody::Posts(posts) };

serde_json::to_string(&response).unwrap()
}

#[get("/post/<post_id>")]
pub fn list_post_handler(post_id: i32) -> Result<String, NotFound<String>> {
// ?? New function body!
let post = read::list_post(post_id)?;
let response = Response { body: ResponseBody::Post(post) };

Ok(serde_json::to_string(&response).unwrap())
}

恭喜!您剛剛成功編寫了前兩個路由,并將它們與數(shù)據(jù)庫連接,現(xiàn)在它們都能從數(shù)據(jù)庫中讀取內(nèi)容了。不過,由于我們的表中還沒有博客文章,所以可讀的內(nèi)容還不多。

讓我們來改變這個狀況。

創(chuàng)建帖子

和之前一樣,我們將從創(chuàng)建路由處理程序的模板開始。這次,我們將創(chuàng)建一個接受JSON數(shù)據(jù)的POST請求。

// api/src/post_handler.rs

use shared::response_models::{Response, ResponseBody};
use application::post::{create, read}; // ?? New!
use domain::models::{Post, NewPost}; // ?? New!
use rocket::{get, post}; // ?? New!
use rocket::response::status::{NotFound, Created}; // ?? New!
use rocket::serde::json::Json;

// ...

#[post("/new_post", format = "application/json", data = "<post>")]
pub fn create_post_handler(post: Json<NewPost>) -> Created<String> {
create::create_post(post)
}

這樣,我們就可以開始實現(xiàn)create_post()函數(shù)了。

// application/src/post/mod.rs

pub mod read;
pub mod create; // ?? New!
// application/src/post/create.rs

use domain::models::{Post, NewPost};
use shared::response_models::{Response, ResponseBody};
use infrastructure::establish_connection;
use diesel::prelude::*;
use rocket::response::status::Created;
use rocket::serde::json::Json;

pub fn create_post(post: Json<NewPost>) -> Created<String> {
use domain::schema::posts;

let post = post.into_inner();

match diesel::insert_into(posts::table).values(&post).get_result::<Post>(&mut establish_connection()) {
Ok(post) => {
let response = Response { body: ResponseBody::Post(post) };
Created::new("").tagged_body(serde_json::to_string(&response).unwrap())
},
Err(err) => match err {
_ => {
panic!("Database error - {}", err);
}
}
}
}

最后一步是注冊我們的路由,以便它們可以被使用。

// api/src/bin/main.rs

#[macro_use] extern crate rocket;
use api::post_handler;

#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/api", routes![
post_handler::list_posts_handler,
post_handler::list_post_handler,
post_handler::create_post_handler, // ?? New!
])
}

現(xiàn)在,我們已經(jīng)完成了所有設(shè)置,接下來讓我們用一些數(shù)據(jù)來測試我們的API!

CRUD測試

完成了CRUD操作中的兩個之后,我們來進行一個小測試。請回到項目的根目錄并啟動應(yīng)用程序。

cargo run

一旦項目構(gòu)建完成,打開您常用的API測試工具,然后驗證路由是否正常工作,符合預(yù)期。

圖:?GET / 按預(yù)期工作
圖:?POST /new_post 按預(yù)期工作

最后兩個字母

我們需要實現(xiàn)的最后兩個操作是更新(Update)和刪除(Delete)。我們將通過發(fā)布帖子來實現(xiàn)更新,以及直接刪除帖子來實現(xiàn)刪除功能。

和之前的兩個操作一樣,我們來創(chuàng)建相應(yīng)的處理程序。

// api/src/post_handler.rs

use shared::response_models::{Response, ResponseBody};
use application::post::{create, read, publish, delete}; // ?? New!
use domain::models::{Post, NewPost};
use rocket::{get, post};
use rocket::response::status::{NotFound, Created};
use rocket::serde::json::Json;

// ...

#[get("/publish/<post_id>")]
pub fn publish_post_handler(post_id: i32) -> Result<String, NotFound<String>> {
let post = publish::publish_post(post_id)?;
let response = Response { body: ResponseBody::Post(post) };

Ok(serde_json::to_string(&response).unwrap())
}

#[get("/delete/<post_id>")]
pub fn delete_post_handler(post_id: i32) -> Result<String, NotFound<String>> {
let posts = delete::delete_post(post_id)?;
let response = Response { body: ResponseBody::Posts(posts) };

Ok(serde_json::to_string(&response).unwrap())
}

并為他們實現(xiàn)邏輯。

// application/src/post/mod.rs

pub mod create;
pub mod read;
pub mod publish; // ?? New!
pub mod delete; // ?? New!
// application/src/post/publish.rs

use domain::models::Post;
use shared::response_models::{Response, ResponseBody};
use infrastructure::establish_connection;
use rocket::response::status::NotFound;
use diesel::prelude::*;

pub fn publish_post(post_id: i32) -> Result<Post, NotFound<String>> {
use domain::schema::posts::dsl::*;

match diesel::update(posts.find(post_id)).set(published.eq(true)).get_result::<Post>(&mut establish_connection()) {
Ok(post) => Ok(post),
Err(err) => match err {
diesel::result::Error::NotFound => {
let response = Response { body: ResponseBody::Message(format!("Error publishing post with id {} - {}", post_id, err))};
return Err(NotFound(serde_json::to_string(&response).unwrap()));
},
_ => {
panic!("Database error - {}", err);
}
}
}
}
// application/src/post/delete.rs

use shared::response_models::{Response, ResponseBody};
use infrastructure::establish_connection;
use diesel::prelude::*;
use rocket::response::status::NotFound;
use domain::models::Post;

pub fn delete_post(post_id: i32) -> Result<Vec<Post>, NotFound<String>> {
use domain::schema::posts::dsl::*;
use domain::schema::posts;

let response: Response;

let num_deleted = match diesel::delete(posts.filter(id.eq(post_id))).execute(&mut establish_connection()) {
Ok(count) => count,
Err(err) => match err {
diesel::result::Error::NotFound => {
let response = Response { body: ResponseBody::Message(format!("Error deleting post with id {} - {}", post_id, err))};
return Err(NotFound(serde_json::to_string(&response).unwrap()));
},
_ => {
panic!("Database error - {}", err);
}
}
};

if num_deleted > 0 {
match posts::table.select(posts::all_columns).load::<Post>(&mut establish_connection()) {
Ok(mut posts_) => {
posts_.sort();
Ok(posts_)
},
Err(err) => match err {
_ => {
panic!("Database error - {}", err);
}
}
}
} else {
response = Response { body: ResponseBody::Message(format!("Error - no post with id {}", post_id))};
Err(NotFound(serde_json::to_string(&response).unwrap()))
}
}

最后一步,我們需要注冊新的路由。

// api/src/bin/main.rs

#[macro_use] extern crate rocket;
use api::post_handler;

#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/api", routes![
post_handler::list_posts_handler,
post_handler::list_post_handler,
post_handler::create_post_handler,
post_handler::publish_post_handler, // ?? New!
post_handler::delete_post_handler, // ?? New!
])
}

就是這樣!現(xiàn)在,您已經(jīng)成功編寫了一個功能齊全的API,它使用Rocket.rs框架,并通過Diesel.rs與PostgreSQL數(shù)據(jù)庫進行連接。此外,該應(yīng)用程序還遵循了Clean Architecture的架構(gòu)原則進行組織。

您的項目結(jié)構(gòu)現(xiàn)在應(yīng)該接近如下:

.
├── Cargo.lock
├── Cargo.toml
├── api
│ ├── Cargo.toml
│ └── src
│ ├── bin
│ │ └── main.rs
│ ├── lib.rs
│ └── post_handler.rs
├── application
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ └── post
│ ├── create.rs
│ ├── delete.rs
│ ├── mod.rs
│ ├── publish.rs
│ └── read.rs
├── domain
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ ├── models.rs
│ └── schema.rs
├── infrastructure
│ ├── Cargo.toml
│ ├── migrations
│ │ └── 2022–11–18–090125_create_posts
│ │ ├── up.sql
│ │ └── down.sql
│ └── src
│ └── lib.rs
└── shared
├── Cargo.toml
└── src
├── lib.rs
└── response_models.rs

進一步的改進

在審視整個應(yīng)用程序時,我們可以考慮以下幾點改進措施。

首先,每次使用數(shù)據(jù)庫時我們都在打開一個新的連接,這在大規(guī)模應(yīng)用中可能會導(dǎo)致資源消耗過大。為了解決這個問題,我們可以使用連接池。Rocket.rs 提供了對 R2D2 的內(nèi)置支持,R2D2 是 Rust 的一個連接池處理庫。

其次,Diesel.rs 目前并不支持異步操作——在當(dāng)前規(guī)模下這可能不是大問題,但在大型應(yīng)用中可能會成為瓶頸。截至目前,Diesel.rs 的官方團隊還沒有提供異步支持。作為替代方案,可以考慮使用第三方的 crate 來實現(xiàn)異步功能。

最后,可以考慮為 Rust API 創(chuàng)建一個前端用戶界面。您可以在項目的根目錄下,使用您選擇的前端技術(shù)棧創(chuàng)建一個名為 web_ui 的新項目。接下來,您只需分別運行前端和后端項目,并通過前端調(diào)用 Rust API。以下是一些前端實現(xiàn)的例子,供您參考和獲取靈感。

圖:我的前端 UI 實現(xiàn)

結(jié)論

哇!這是一段多么精彩的旅程啊。我們不僅學(xué)會了如何使用 Rocket.rs 和 Diesel.rs,還掌握了如何將它們結(jié)合起來在 Rust 中創(chuàng)建一個博客 API。不僅如此,我們還為這個 API 構(gòu)建了一個前端,并按照 Clean Architecture 的原則將整個項目整合到了一個文件結(jié)構(gòu)中。

我希望大家今天能收獲滿滿,親自動手嘗試一下,創(chuàng)造出屬于自己的新作品!

感謝閱讀!

原文鏈接:https://medium.com/@jeynesbrook/building-an-api-in-rust-with-rocket-rs-and-diesel-rs-clean-architecture-8f6092ee2606

#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費