DEV Community

loading...

Practical Rust Web Development - Associations

werner profile image Werner Echezuría ・3 min read

As web developers we are used to some facilities provided by most frameworks, however, when we choose to go for a more performant perspective for our applications, we have to get out from our comfort zone.

One of those facilities are associations between models, for example, in Ruby on Rails we have belongs_to, has_many and accepts_nested_attributes_for ActiveRecord methods that provide us several resources to make our lives easier.

Diesel provides a BelongsTo macro, that somehow we can use, but we need a little bit of more efforts to make it work.

Let's say we're going to need to associate our products by users, because one user should not have access to the products from another user. Other relationship we might need is prices, we want our products to have different prices (discount, max, min, etc).

Database Migrations

We're going to create a few migrations, one for adding a user_id column to our products table to know what user owns each product, then we need another two tables, the prices that will save the name and another to associate product and prices with the corresponding amount.

migrations/2019-07-06-151805_add_user_to_products/up.sql:

ALTER TABLE products ADD COLUMN user_id INTEGER NOT NULL;
ALTER TABLE products ADD CONSTRAINT products_user_id_foreign_key 
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE; 

migrations/2019-07-07-134611_add_prices/up.sql:

CREATE TABLE prices (
  id SERIAL PRIMARY KEY,
  name VARCHAR NOT NULL,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  CHECK (name <> '')
);

CREATE TABLE prices_products (
  id SERIAL PRIMARY KEY,
  price_id INTEGER NOT NULL REFERENCES prices(id),
  product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  amount INTEGER, --representing cents
  UNIQUE (price_id, product_id)
);

ALTER TABLE products RENAME COLUMN price TO cost;

Associations

I'm going to omit several parts because are very similar to product, like prices handler and some structs, however I'm going to highlight the PriceProduct struct.

src/models/price.rs:

#[derive(Identifiable, Associations, Queryable, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[belongs_to(Price)]
#[belongs_to(Product)]
#[table_name="prices_products"]
pub struct PriceProduct {
    pub id: i32,
    pub price_id: i32,
    pub product_id: i32,
    pub user_id: i32,
    pub amount: Option<i32>
}

AS you can see is very straightforward you just need to use the belongs_to macro to the model you want to associate the struct.

Now the method that saves our PriceProduct to the database:

#[derive(Serialize, Deserialize, Clone)]
pub struct PriceProductToUpdate {
    pub price_product: NewPriceProduct,
    pub to_delete: bool
}

use diesel::PgConnection;

impl PriceProductToUpdate {
    pub fn batch_update(records: Vec<Self>, param_product_id: i32, param_user_id: i32, connection: &PgConnection) ->
        Result<Vec<PriceProduct>, diesel::result::Error> {
            use diesel::QueryDsl;
            use diesel::RunQueryDsl;
            use diesel::ExpressionMethods;
            use diesel::Connection;
            use itertools::Itertools;

            connection.transaction(|| {
                let mut records_to_keep = vec![];
                for price_product_to_update in records {

                    if price_product_to_update.to_delete &&
                        price_product_to_update.price_product.id.is_some() {

                        diesel::delete(
                                prices_products::table
                                    .filter(prices_products::user_id.eq(param_user_id))
                                    .find(price_product_to_update.price_product.id.unwrap()))
                            .execute(connection)?;
                    } else {
                        records_to_keep.push(price_product_to_update)
                    }
                }

                records_to_keep
                    .iter()
                    .map(|price_product| {

                        let new_price_product = NewPriceProduct {
                            user_id: Some(param_user_id),
                            product_id: Some(param_product_id),
                            ..price_product.clone().price_product
                        };

                        diesel::insert_into(prices_products::table)
                            .values(&new_price_product)
                            .on_conflict((prices_products::price_id, 
                                        prices_products::product_id))
                            .do_update()
                            .set(prices_products::amount.eq(new_price_product.amount))
                            .returning((prices_products::id, 
                                        prices_products::price_id,
                                        prices_products::product_id,
                                        prices_products::user_id,
                                        prices_products::amount))
                            .get_result::<PriceProduct>(connection)
                    })
                    .fold_results(vec![], |mut accum, value| {
                        accum.push(value);
                        accum
                    })
            })

        }
}

I'm using a transaction, so, a Rollback happens if something fails. We use the belonging_to method when we need to find a product to bring us the prices associated.

src/models/product.rs:

    pub fn find(product_id: &i32, param_user_id: i32, connection: &PgConnection) -> 
        Result<(Product, Vec<PriceProduct>), diesel::result::Error> {
            use diesel::QueryDsl;
            use diesel::RunQueryDsl;
            use diesel::ExpressionMethods;
            use crate::schema;
            use crate::schema::products::dsl::*;

            let product: Product =
                schema::products::table
                    .select(PRODUCT_COLUMNS)
                    .filter(user_id.eq(param_user_id))
                    .find(product_id)
                    .first(connection)?;

            let products_with_prices =
                PriceProduct::belonging_to(&product)
                    .load::<PriceProduct>(connection)?;

            Ok((product, products_with_prices))
    }

You can take a look at the source code here

Discussion (7)

pic
Editor guide
Collapse
leekheang profile image
LEE KHEANG

hello, teacher Werner I'm a newbie in rust programming.
I want rust with graphql but I don't know how to start it.
can you advise me and share some documents. thank you

Collapse
werner profile image
Werner Echezuría Author

Hi, lol, thanks for calling me teacher, :), doing a fast google search I could find this. It seems to provide a series of tools to work with GraphQL, I'm considering creating a future post that implement it.

Collapse
leekheang profile image
LEE KHEANG

thank you

Collapse
laynath242 profile image
LayNath242

thank you again.how to upload file in rust?

Collapse
werner profile image
Werner Echezuría Author

Here you can see an example of how to do that in actix web:
github.com/actix/examples/blob/mas...

Collapse
laynath242 profile image
LayNath242 • Edited

thank you.can we update from REST Api to Graphql ?

Thread Thread
werner profile image
Werner Echezuría Author

I might do it in a future post.