การใช้ Repository Pattern ในภาษา Rust ร่วมกับ MongoDB ช่วยให้คุณสามารถแยกตรรกะการเข้าถึงฐานข้อมูล (Data Access Logic) ออกจากตรรกะทางธุรกิจ (Business Logic) ได้อย่างชัดเจน ทำให้โค้ดสะอาด ทดสอบง่าย และดูแลรักษาง่ายครับ
นี่คือตัวอย่างโครงสร้างโปรเจคและโค้ดแบบสมบูรณ์ที่พร้อมนำไปใช้งาน:
📂 โครงสร้างโปรเจค (Project Structure)
แนะนำให้จัดโครงสร้างแยกตามหน้าที่ (Separation of Concerns) ดังนี้ครับ:
my_mongodb_app/
├── Cargo.toml
└── src/
├── main.rs # จุดเริ่มต้นแอปพลิเคชันและการเชื่อมต่อ DB
├── models.rs # โครงสร้างข้อมูล (Data Models / Entities)
└── repository.rs # ตรรกะ Repository (Interface และ Implementation)
📦 1. สั่งพึ่งพาในระบบ (Cargo.toml)
เพิ่มไลบรารีที่จำเป็นสำหรับ Async, MongoDB และ Serialization:
[package]
name = "my_mongodb_app"
version = "0.1.0"
edition = "2021"
[dependencies]
mongodb = "3.1.0" # แนะนำให้ใช้เวอร์ชันล่าสุด
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
futures = "0.3"
📝 2. นิยามโครงสร้างข้อมูล (src/models.rs)
สร้าง Entity ที่ต้องการจัดเก็บในฐานข้อมูล (ในตัวอย่างนี้คือ User)
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct User {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
pub name: String,
pub email: String,
pub age: i32,
}
🛠️ 3. สร้าง Repository Pattern (src/repository.rs)
เราจะสร้าง Trait เพื่อเป็น Interface และสร้าง Struct เพื่อ Implement การเชื่อมต่อเข้ากับ MongoDB Collection ครับ
use crate::models::User;
use futures::stream::TryStreamExt;
use mongodb::bson::{doc, oid::ObjectId};
use mongodb::error::Error;
use mongodb::{Collection, Database};
// 1. นิยาม Interface (Trait) เพื่อให้ง่ายต่อการทำ Mock Unit Test ในอนาคต
pub trait UserRepository {
async fn create(&self, new_user: User) -> Result<ObjectId, Error>;
async fn find_by_id(&self, id: &str) -> Result<Option<User>, Error>;
async fn find_all(&self) -> Result<Vec<User>, Error>;
async fn update(&self, id: &str, update_user: User) -> Result<u64, Error>;
async fn delete(&self, id: &str) -> Result<u64, Error>;
}
// 2. Struct ที่จะผูกกับ MongoDB
pub struct MongoUserRepository {
collection: Collection<User>,
}
impl MongoUserRepository {
pub fn new(db: &Database) -> Self {
// กำหนดชื่อ Collection ที่จะใช้งาน
let collection = db.collection::<User>("users");
MongoUserRepository { collection }
}
}
// 3. Implement ตรรกะการจัดการฐานข้อมูล
impl UserRepository for MongoUserRepository {
async fn create(&self, new_user: User) -> Result<ObjectId, Error> {
let result = self.collection.insert_one(new_user).await?;
Ok(result.inserted_id.as_object_id().unwrap())
}
async fn find_by_id(&self, id: &str) -> Result<Option<User>, Error> {
let obj_id = ObjectId::parse_str(id).map_err(|e| Error::from(std::io::Error::new(std::io::ErrorKind::InvalidInput, e)))?;
let filter = doc! { "_id": obj_id };
let user = self.collection.find_one(filter).await?;
Ok(user)
}
async fn find_all(&self) -> Result<Vec<User>, Error> {
let mut cursor = self.collection.find(doc! {}).await?;
let mut users = Vec::new();
while let Some(user) = cursor.try_next().await? {
users.push(user);
}
Ok(users)
}
async fn update(&self, id: &str, update_user: User) -> Result<u64, Error> {
let obj_id = ObjectId::parse_str(id).map_err(|e| Error::from(std::io::Error::new(std::io::ErrorKind::InvalidInput, e)))?;
let filter = doc! { "_id": obj_id };
// แปลง struct เป็น doc เพื่ออัปเดต (ยกเว้น _id)
let update_doc = doc! {
"$set": {
"name": update_user.name,
"email": update_user.email,
"age": update_user.age
}
};
let result = self.collection.update_one(filter, update_doc).await?;
Ok(result.modified_count)
}
async fn delete(&self, id: &str) -> Result<u64, Error> {
let obj_id = ObjectId::parse_str(id).map_err(|e| Error::from(std::io::Error::new(std::io::ErrorKind::InvalidInput, e)))?;
let filter = doc! { "_id": obj_id };
let result = self.collection.delete_one(filter).await?;
Ok(result.deleted_count)
}
}
🚀 4. นำไปใช้งานจริง (src/main.rs)
โค้ดสำหรับเชื่อมต่อฐานข้อมูลและเรียกใช้งาน Repository:
mod models;
mod repository;
use models::User;
use mongodb::{options::ClientOptions, Client};
use repository::{MongoUserRepository, UserRepository};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// 1. เชื่อมต่อ MongoDB (แนะนำให้เปลี่ยน URI ตามที่ใช้งานจริง)
let mongo_uri = "mongodb://localhost:27017";
let client_options = ClientOptions::parse(mongo_uri).await?;
let client = Client::with_options(client_options)?;
let db = client.database("my_rust_db");
println!("✅ เชื่อมต่อ MongoDB สำเร็จ!");
// 2. สร้าง Instance ของ Repository
let user_repo = MongoUserRepository::new(&db);
// 3. ทดสอบการใช้งาน: สร้างข้อมูล (Create)
let new_user = User {
id: None,
name: String::from("Somchai Dev"),
email: String::from("somchai@example.com"),
age: 30,
};
let inserted_id = user_repo.create(new_user).await?;
println!("➕ เพิ่มผู้ใช้สำเร็จ ID: {}", inserted_id);
// 4. ทดสอบการใช้งาน: ดึงข้อมูลทั้งหมด (Read All)
let all_users = user_repo.find_all().await?;
println!("📋 รายชื่อผู้ใช้ทั้งหมดในระบบ: {:?}", all_users);
// 5. ทดสอบการใช้งาน: ค้นหาด้วย ID (Read One)
let user_str_id = inserted_id.to_hex();
if let Some(user) = user_repo.find_by_id(&user_str_id).await? {
println!("🔍 ค้นพบผู้ใช้: {} (อีเมล: {})", user.name, user.email);
}
// 6. ทดสอบการใช้งาน: อัปเดตข้อมูล (Update)
let updated_user_data = User {
id: Some(inserted_id),
name: String::from("Somchai Updated"),
email: String::from("somchai.new@example.com"),
age: 31,
};
let updated_count = user_repo.update(&user_str_id, updated_user_data).await?;
println!("🔄 อัปเดตสำเร็จจำนวน: {} แถว", updated_count);
// 7. ทดสอบการใช้งาน: ลบข้อมูล (Delete)
// คลายคอมเมนต์ด้านล่างหากต้องการทดสอบการลบ
// let deleted_count = user_repo.delete(&user_str_id).await?;
// println!("❌ ลบผู้ใช้สำเร็จจำนวน: {} แถว", deleted_count);
Ok(())
}
💡 ข้อดีของโครงสร้างนี้
หากคุณกำลังจะนำโครงสร้างนี้ไปใช้กับ Web Framework (เช่น Axum หรือ Actix-web) หรืออยากทราบวิธีการเขียน Unit Test แบบ Mocking สำหรับโครงสร้างนี้ แจ้งได้เลยนะครับ ผมยินดีเพิ่มตัวอย่างโค้ดในส่วนนั้นให้ครับ!