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)
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.
thank you
thank you again.how to upload file in rust?
Here you can see an example of how to do that in actix web:
github.com/actix/examples/blob/mas...
thank you.can we update from REST Api to Graphql ?
I might do it in a future post.