🔥 การใช้ Repository Pattern ในภาษา Rust ร่วมกับ MongoDB

การใช้ 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:

Cargo.toml
[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)

src/models.rs
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 ครับ

src/repository.rs
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:

src/main.rs
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(())
}

💡 ข้อดีของโครงสร้างนี้

  1. Dependency Injection Easy: หากคุณใช้ Web Framework เช่น Actix-web หรือ Axum คุณสามารถส่ง Arc<dyn UserRepository> เข้าไปเป็น State ของแอปพลิเคชันได้เลย
  2. Easy Testing: คุณสามารถสร้าง MockUserRepository ขึ้นมาเพื่อทำ Unit Test ตรรกะฝั่งธุรกิจได้โดยไม่ต้องต่อฐานข้อมูลจริง
  3. Clean Code: main.rs หรือฝั่ง Router จะไม่เห็นคำสั่งที่เป็นคิวรีของ MongoDB เลย (เช่นคำสั่ง doc!) เพราะมันถูกซ่อนไว้หลัง Repository เรียบร้อยแล้วครับ

หากคุณกำลังจะนำโครงสร้างนี้ไปใช้กับ Web Framework (เช่น Axum หรือ Actix-web) หรืออยากทราบวิธีการเขียน Unit Test แบบ Mocking สำหรับโครงสร้างนี้ แจ้งได้เลยนะครับ ผมยินดีเพิ่มตัวอย่างโค้ดในส่วนนั้นให้ครับ!