Practical Rust Web Development - Associations
Tags: rust, actixweb, relationships, associations
Published: 2019-07-14 15:37:10

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`](https://docs.diesel.rs/diesel/associations/index.html) 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`:

```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`:

```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`:

```rust
#[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:

```rust
#[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`:

```rust
    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](https://github.com/practical-rust-web-development/mystore/tree/v6.0)