Cleanest option for implementing CRUD: In struct or separate function?

Hello

In my Actix-Web project I have a few structs which represent the tables in my MySQL-database. But I am wondering if it's more clean to implement the CRUD-functions in the struct itself, or to make a separate function for it.

Option 1: Function to retrieve a Order-object implemented in struct.

#[derive(Deserialize, Serialize, Clone)]
pub struct Order {
    id: u64,
    user: User,
    restaurant: Restaurant,
    address: Address,
    transaction_fee: f64,
    delivery_cost: f64,
    payment_id: String,
    paid: bool,
    order_date: Option<chrono::DateTime<chrono::Utc>>,
}

impl Order {
    pub async fn retrieve(order_id: u64, mysql: &web::Data<MySQL>) -> Option<Order> {

        let result = sqlx::query("SELECT ID, order_date, user, restaurant, address, transaction_fee, delivery_cost, payment_id, paid FROM `order` WHERE ID=?")
            .bind(order_id)
            .fetch_one(&mysql.conn)
            .await;

        match result {
            Err(e) => {
                println!("Error [in get_order(), data.rs]: {}", e);
                None
            }
            Ok(r) => Some(Order {
                id: r.try_get("ID").unwrap(),
                user: User::get(Selector::ById(r.try_get("user").unwrap()), mysql)
                    .await
                    .unwrap(),
                restaurant: get_restaurant(r.try_get("restaurant").unwrap(), mysql)
                    .await
                    .unwrap(),
                address: get_address(r.try_get("address").unwrap(), mysql)
                    .await
                    .unwrap(),
                transaction_fee: r.try_get("transaction_fee").unwrap(),
                delivery_cost: r.try_get("delivery_cost").unwrap(),
                payment_id: r.try_get("payment_id").unwrap(),
                paid: r.try_get("paid").unwrap(),
                order_date: r.try_get("order_date").unwrap(),
            }),
        }
    }

// Called like
let order = Order::retrieve(1, &mysql).await.unwrap();

Option 2: Function to retrieve a Order-object in separate module/file.

// main.rs

#[derive(Deserialize, Serialize, Clone)]
pub struct Order {
    id: u64,
    user: User,
    restaurant: Restaurant,
    address: Address,
    transaction_fee: f64,
    delivery_cost: f64,
    payment_id: String,
    paid: bool,
    order_date: Option<chrono::DateTime<chrono::Utc>>,
}
// data.rs

pub async fn get_order(order_id: u64, mysql: &web::Data<MySQL>) -> Option<Order> {

        let result = sqlx::query("SELECT ID, order_date, user, restaurant, address, transaction_fee, delivery_cost, payment_id, paid FROM `order` WHERE ID=?")
            .bind(order_id)
            .fetch_one(&mysql.conn)
            .await;

        match result {
            Err(e) => {
                println!("Error [in get_order(), data.rs]: {}", e);
                None
            }
            Ok(r) => Some(Order {
                id: r.try_get("ID").unwrap(),
                user: User::get(Selector::ById(r.try_get("user").unwrap()), mysql)
                    .await
                    .unwrap(),
                restaurant: get_restaurant(r.try_get("restaurant").unwrap(), mysql)
                    .await
                    .unwrap(),
                address: get_address(r.try_get("address").unwrap(), mysql)
                    .await
                    .unwrap(),
                transaction_fee: r.try_get("transaction_fee").unwrap(),
                delivery_cost: r.try_get("delivery_cost").unwrap(),
                payment_id: r.try_get("payment_id").unwrap(),
                paid: r.try_get("paid").unwrap(),
                order_date: r.try_get("order_date").unwrap(),
            }),
        }
    }
// Called like
let order = data::get_order(1, mysql).await.unwrap();

What is the cleanest option? And would it be different for each CRUD-method? E.g, have the create implemented in struct so I could do

Order {...}.create(mysql).

But have the retrieve implemented in a different file?

Thanks.

It doesn't matter. Both are fine. (Personally, I'd lean towards the method approach.)

That doesn't seem to make much sense – why put them in different files? They are the same kind of operation.

1 Like

Thanks a lot for your answer. But to clarify the last part of my question:
I don't mean making a duplicate function, one in the struct and one in a separate file.

But for example implement create in the struct, and retrieve in a separate file.

Because for create: Order {...}.create() looks more logical then create_order(&order).
But for retrieve: retrieve_order(id) looks more logical then Order::retrieve(id) IMHO. Should I stick to one option or can I combine them?

I realize that, my answer is unchanged. Putting creation and retrieval of the same type in different files makes no sense to me.

You should decide if you want your code to look more OO or more procedural and stick with one. Mixing the two doesn't seem clean in this situation (sometimes it does, but not here – it breaks the symmetry of the code).

1 Like