Trong phần này, chúng ta sẽ tìm hiểu sâu về kiến trúc hỗ trợ hệ thống API được xây dựng với Rust, Axum, và SeaORM. Cấu trúc này nhấn mạnh khả năng mở rộng, bảo trì và hiệu suất.
Hệ thống được tổ chức thành các mô-đun riêng biệt:
application_core
: Chứa logic nghiệp vụ cốt lõi và các bộ xử lý lệnh.migrations
: Chứa các mô hình thực thể SeaORM và các bản di chuyển cơ sở dữ liệu.src
: Bao gồm điểm vào (main.rs) và lớp trình bày (các bộ xử lý HTTP Axum).my-cms/ ├── application_core/ # Core business logic and services │ └── commands/ │ └── post/ │ └── read.rs # Post read command handler ├── migrations/ # SeaORM entity definitions and migrations ├── src/ │ ├── api/ # Axum routes and controllers │ └── bin/ │ └── main.rs # Application entry point ├── Cargo.toml # Workspace configuration └── ...
application_core/commands/post/read.rs
##[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PostReadResponse { pub id: Uuid, pub title: String, pub preview_content: Option<String>, pub content: String, pub slug: String, pub thumbnail_paths: Vec<String>, pub published: bool, pub created_by: String, pub created_at: DateTimeWithTimeZone, pub last_modified_by: Option<String>, pub last_modified_at: Option<DateTimeWithTimeZone>, pub category_id: Uuid, pub row_version: i32, pub tags: Vec<tags::Model>, pub tag_names: Vec<String>, pub translations: Vec<post_translations::Model>, } impl PostReadResponse { pub fn new( post: Model, tags: Vec<tags::Model>, translations: Vec<post_translations::Model>, ) -> Self { let tag_names = tags .iter() .map(|tag| tag.name.to_owned()) .collect::<Vec<String>>(); Self { id: post.id, title: post.title, preview_content: post.preview_content, content: post.content, slug: post.slug, published: post.published, created_by: post.created_by, created_at: post.created_at, last_modified_by: post.last_modified_by, last_modified_at: post.last_modified_at, category_id: post.category_id, row_version: post.row_version, thumbnail_paths: post.thumbnail_paths, tags, tag_names, translations, } } }
#[async_trait] pub trait PostReadHandlerTrait { async fn handle_get_all_posts(&self) -> Result<Vec<PostReadResponse>, AppError>; async fn handle_get_posts_with_filtering( &self, category_type: Option<CategoryType>, published: Option<bool>, ) -> Result<Vec<PostReadResponse>, AppError>; async fn handle_get_post( &self, id: Uuid, ) -> Result<PostReadResponse, AppError>; } #[derive(Debug, Clone)] pub struct PostReadHandler { pub db: Arc<DatabaseConnection>, }
#[async_trait] impl PostReadHandlerTrait for PostReadHandler { #[instrument] async fn handle_get_all_posts(&self) -> Result<Vec<PostReadResponse>, AppError> { let db_result = Posts::find() .find_with_related(Tags) .all(self.db.as_ref()) .await .map_err(AppError::from)?; let response = db_result .into_iter() .map(|(post, tags)| { PostReadResponse::new(post, tags, vec![]) }) .collect(); Ok(response) } #[instrument] async fn handle_get_posts_with_filtering( &self, category_type: Option<CategoryType>, published: Option<bool>, ) -> Result<Vec<PostReadResponse>, AppError> { let db_result = Posts::find() .join(JoinType::LeftJoin, posts::Relation::Categories.def()) .apply_if(category_type, |query, v| { query.filter(Expr::col(categories::Column::CategoryType).eq(v.as_enum())) }) .apply_if(published, |query, v| { query.filter(Expr::col(posts::Column::Published).eq(v)) }) .find_with_related(Tags) .all(self.db.as_ref()) .await .map_err(AppError::from)?; let post_ids: Vec<Uuid> = db_result.iter().map(|(post, _)| post.id).collect(); let translations = post_translations::Entity::find() .filter(post_translations::Column::PostId.is_in(post_ids.clone())) .all(self.db.as_ref()) .await .map_err(AppError::from)?; let translations_map = translations.into_iter().fold( HashMap::new(), |mut acc: HashMap<Uuid, Vec<post_translations::Model>>, translation| { acc.entry(translation.post_id) .or_default() .push(translation); acc }, ); let response = db_result .into_iter() .map(|(post, tags)| { let post_translations = translations_map.get(&post.id).cloned().unwrap_or_default(); PostReadResponse::new(post, tags, post_translations) }) .collect(); Ok(response) } #[instrument] async fn handle_get_post(&self, id: Uuid) -> Result<PostReadResponse, AppError> { let post = Posts::find_by_id(id) .one(self.db.as_ref()) .await? .ok_or(AppError::NotFound)?; let tags = post.find_related(Tags).all(self.db.as_ref()).await?; let translations = post.find_related(post_translations::Entity) .all(self.db.as_ref()) .await?; Ok(PostReadResponse::new(post, tags, translations)) } }
🧩 Thư mục migrations
chứa các mô hình thực thể và tập lệnh di chuyển được tạo bởi SeaORM. Điều quan trọng là:
sea-orm-cli generate entity -u postgres://<your-database-url> -o migrations
🚨 Đừng dựa dẫm một cách mù quáng vào các mô hình được tạo sẵn — lược đồ của bạn có thể khác biệt đáng kể. Chỉ sử dụng điều này như một hướng dẫn cấu trúc.
Dự án này được kiến trúc sử dụng các nguyên tắc từ Clean Architecture và Command Query Responsibility Segregation (CQRS) . Những mô hình này thúc đẩy việc phân tách các mối quan tâm và tổ chức mã theo trách nhiệm.
application_core
, giúp dễ dàng viết các bài kiểm thử đơn vị.Sự phân tách rõ ràng này làm cho hệ thống dễ hiểu và phát triển hơn khi yêu cầu thay đổi.
Hiện tại, trong kiến trúc, DbConnection
đang được tiêm trực tiếp vào các handler để thực hiện các tác vụ cơ sở dữ liệu. Mặc dù cách tiếp cận này hoạt động, nhưng nó không phải là thực hành tốt nhất vì vi phạm Nguyên tắc Trách nhiệm Đơn lẻ (SRP) và làm cho các handler bị gắn chặt với tầng cơ sở dữ liệu. Để cải thiện điều này và làm cho kiến trúc trở nên mô-đun và dễ bảo trì hơn, chúng ta có thể giới thiệu Dịch vụ Truy cập Dữ liệu (DAS) .
DbConnection
, cung cấp các phương thức mà các handler có thể sử dụng để tương tác với cơ sở dữ liệu mà không cần quản lý trực tiếp kết nối.DbConnection
vào các handler, hãy tiêm DAS.pub trait DataAccessService { fn get_post_by_id(&self, id: Uuid) -> impl Future<Output = Result<PostReadResponse, AppError>>; fn get_all_posts(&self) -> impl Future<Output = Result<Vec<PostReadResponse>, AppError>>; // Add other database-related methods here }
#[derive(Debug)] pub struct PostDataAccessService { pub db: Arc<DatabaseConnection>, } impl DataAccessService for PostDataAccessService { async fn get_post_by_id(&self, id: Uuid) -> Result<PostReadResponse, AppError> { let post = Posts::find_by_id(id) .find_with_related(Tags) .one(self.db.as_ref()) .await .map_err(|e| e.into())?; // Mapping db result to response format post.map(|p_and_tags| { PostReadResponse::new( p_and_tags.0.to_owned(), p_and_tags.1.to_owned(), vec![], ) }) .ok_or(AppError::NotFound) } async fn get_all_posts(&self) -> Result<Vec<PostReadResponse>, AppError> { let db_result = Posts::find() .find_with_related(Tags) .all(self.db.as_ref()) .await .map_err(|e| e.into())?; Ok(db_result .iter() .map(|p_and_tags| { PostReadResponse::new(p_and_tags.0.to_owned(), p_and_tags.1.to_owned(), vec![]) }) .collect()) } }
#[derive(Debug)] pub struct PostReadHandler { pub data_access_service: Arc<dyn DataAccessService>, } impl PostReadHandlerTrait for PostReadHandler { #[instrument] async fn handle_get_all_posts(&self) -> Result<Vec<PostReadResponse>, AppError> { self.data_access_service.get_all_posts().await } #[instrument] async fn handle_get_post(&self, id: Uuid) -> Result<PostReadResponse, AppError> { self.data_access_service.get_post_by_id(id).await } }
=> Cải tiến này nâng cao kiến trúc bằng cách tuân theo Nguyên Tắc Trách Nhiệm Đơn (SRP) , thúc đẩy khả năng kiểm thử tốt hơn và cho phép bảo trì dễ dàng hơn cho ứng dụng.
Nếu bạn mới làm quen với Kiến Trúc Sạch hoặc CQRS , đây là một số tài liệu tham khảo hữu ích dành cho các nhà phát triển Rust:
Những tài liệu này đi sâu hơn vào các thực tiễn tốt nhất về kiến trúc và cách triển khai chúng hiệu quả trong Rust.
➡️ Xem kho mã tại đây: https://github.com/doitsu2014/my-cms
⬅️ Quay lại Xây dựng API với Rust và Axum – Phần 1: Khởi tạo & Thiết lập dự án
➡️ Tiếp tục đến Phần 3: Truy Vết & Khả Năng Quan Sát → — Làm cho API của bạn minh bạch và dễ dàng gỡ lỗi trong môi trường sản xuất.
Xuất bản vào 4/11/2025