DEV Community

Werner Echezuría
Werner Echezuría

Posted on

Practical Rust Web Development - Associations

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

Top comments (7)

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
werner profile image
Werner Echezuría

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
 
lykheang profile image
MOEURN Lykheang

thank you

Collapse
 
laynath242 profile image
LayNath242

thank you again.how to upload file in rust?

Collapse
 
werner profile image
Werner Echezuría

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

I might do it in a future post.