Search⌘ K
AI Features

Create Resource

Explore how to create products and their variants in a Rust web application using Diesel for database migrations and data insertion. Learn to build normalized tables, manage data relationships, and write test functions to ensure your product creation logic works correctly.

We'll cover the following...

Create a product

We can start creating the migration needed for our project. We can name it however we want. Let’s call it create_products to add the migration necessary to create and drop our table.

Note: After we install and configure Diesel according to the first Appendix, we can write the following command in a terminal window:

Shell
diesel migration generate create_products

The previous command will create two files named up.sql and down.sql inside the migrations folder.

We’ll enter the below code in the up.sql file.

MySQL
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name VARCHAR NOT NULL,
cost DOUBLE NOT NULL,
active BOOLEAN NOT NULL DEFAULT 0 --Sqlite does not have a Boolean value
)

Let’s work with the down.sql file now.

MySQL
drop table products

Now, we can run our migration to create the products table by running the following command in the terminal:

Shell
diesel migration run

We add a file called models.rs for our models in the src folder with the products model.

Rust 1.40.0
use diesel::Insertable;
use shoe_store::schema::*;
#[derive(Insertable, Debug)]
#[table_name="products"]
pub struct NewProduct {
pub name: String,
pub cost: f64,
pub active: bool,
}

This struct will be our model for inserting data in our database. Therefore, we need it to be Insertable, We also need to give it the name of our table. We’re now prepared to add the code corresponding to creating a products table.

We are now prepared to add the code corresponding to creating products.

Rust 1.40.0
use ::shoe_store::models::NewProduct;
use diesel::sqlite::SqliteConnection;
use diesel::result::Error;
use diesel::RunQueryDsl;
fn create_product(new_product: NewProduct, conn: &SqliteConnection) -> Result<usize, Error> {
use ::shoe_store::schema::products::dsl::*;
diesel::insert_into(products)
.values(new_product)
.execute(conn)
}

The insert_into function needs a target. In this case, the target is products, which comes from the DSL module from the products schema. We can use the values method to insert the data. It can be a single value or several. For our use case, we need a single product that will be used in the future endpoint.

This function will return the number of rows inserted if it is successful. Otherwise, it will return an error.

We’re now ready to write some tests and verify if our code works as expected.

Rust 1.40.0
use diesel::Connection;
use ::shoe_store::establish_connection_test;
#[test]
fn create_product_test() {
let connection = establish_connection_test();
connection.test_transaction::<_, Error, _>(|| {
let results =
create_product(NewProduct {
name: "boots".to_string(),
cost: 13.23,
active: true
}, &connection);
assert_eq!(Ok(1), results);
Ok(())
});
}

In the above code, we created a function, create_product_test, to test our product creation. The annotation #[test] tells us it’s a test function.

We’re using a function called establish_connection_test to connect to our test database. Here, we can define another function called test_transaction, which is very useful because it doesn’t commit to the database. This allows us to have a clean database every time we run our tests.

We verify that one insert was created successfully.

Create Variants

We likely need associations in our project—for example, shoes can come in different sizes, styles, and so on.

We can add all the shoe features in the same table, but that would be a massive table with many fields, and it wouldn’t be normalized.

We need to create another table to save shoe variants.

Shell
diesel migration generate create_variants

Below is how the up.sql file will look.

MySQL
CREATE TABLE variants (
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR NOT NULL
);
CREATE TABLE products_variants (
id INTEGER PRIMARY KEY NOT NULL,
variant_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
value VARCHAR,
FOREIGN KEY(variant_id) REFERENCES variants(id),
FOREIGN KEY(product_id) REFERENCES products(id)
);

Below is how the down.sql file.

MySQL
drop table variants;
drop table products_variants;

We did not include indexes in the above code widgets, but you should do so according to your frequent queries. Next, we run the migrations.

Shell
diesel migration run
diesel migration run --database-url='db/my_project_test.sqlite'

In the below code, we add the models we need to create new variants.

Rust 1.40.0
use diesel::{ Insertable, Identifiable, Queryable};
use serde::{Serialize, Deserialize};
use shoe_store::schema::variants;
#[derive(Identifiable, Queryable, Debug, Serialize, Deserialize)]
#[table_name = "variants"]
pub struct Variant {
pub id: i32,
pub name: String,
}
#[derive(Insertable, Debug, Clone)]
#[table_name="variants"]
pub struct NewVariant {
pub name: String,
}
use shoe_store::schema::products_variants;
#[derive(Insertable, Debug)]
#[table_name="products_variants"]
pub struct NewProductVariant {
pub product_id: i32,
pub variant_id: i32,
pub value: Option<String>
}

We might need additional models that have a different purpose and that aren’t connected to a table for our business logic.

Rust 1.40.0
#[derive(Clone)]
pub struct NewVariantValue {
pub variant: NewVariant,
pub values: Vec<Option<String>>
}
pub struct NewCompleteProduct {
pub product: NewProduct,
pub variants: Vec<NewVariantValue>
}

Next, we’re going to write the code needed to create variants related to our products.

Rust 1.40.0
#[macro_use]
extern crate diesel;
use anyhow::Result;
use diesel::sqlite::SqliteConnection;
use diesel::ExpressionMethods;
use ::shoe_store::models::*;
use diesel::Connection;
use diesel::RunQueryDsl;
use diesel::query_dsl::QueryDsl;
no_arg_sql_function!(last_insert_rowid, diesel::sql_types::Integer);
fn create_product(new_product: NewCompleteProduct, conn: &SqliteConnection) -> Result<i32> {
use ::shoe_store::schema::products::dsl::products;
use ::shoe_store::schema::variants::dsl::*;
use ::shoe_store::schema::products_variants::dsl::*;
conn.transaction(|| {
diesel::insert_into(products)
.values(new_product.product)
.execute(conn)?;
let last_product_id: i32 = diesel::select(last_insert_rowid).first(conn)?;
for new_variant in new_product.variants {
let variants_result =
variants
.filter(name.eq(&new_variant.variant.name))
.limit(1)
.load::<Variant>(conn)?;
let last_variant_id: i32 =
match variants_result.first() {
Some(variant) => variant.id,
None => {
diesel::insert_into(variants)
.values(name.eq(&new_variant.variant.name))
.execute(conn)?;
diesel::select(last_insert_rowid).first(conn)?
}
};
for new_value in new_variant.values {
diesel::insert_into(products_variants)
.values(
(
product_id.eq(last_product_id),
variant_id.eq(last_variant_id),
value.eq(new_value),
)
)
.execute(conn)?;
}
}
Ok(last_product_id)
})
}

In the above code, we use the macro no_arg_sql_function!, which allows us to use an SQL function in our code. In this case, SQLite does not have RETURNING for our inserts implemented. Therefore, we need a function called last_insert_rowid to get the last id inserted.

Note: We must be careful to use this function in a transaction.

We create a function called create_product that requires an argument of the NewCompleteProduct type. This function will contain the data we need to create a product and its variants.

In the code, we make a loop over the new variants and filter them by name to check if the variant was already created. We do this to avoid duplications and create it if necessary.

Finally, we create the relationships needed for the products and variants.

Next, we need to create a unit test to verify if the code is working as expected.

C++
use ::shoe_store::establish_connection_test;
use diesel::result::Error;
#[test]
fn create_product_test() {
let connection = establish_connection_test();
connection.test_transaction::<_, Error, _>(|| {
create_product(NewCompleteProduct {
product: NewProduct {
name: "boots".to_string(),
cost: 13.23,
active: true
},
variants: vec![
NewVariantValue {
variant: NewVariant {
name: "size".to_string()
},
values: vec![
Some(12.to_string()),
Some(14.to_string()),
Some(16.to_string()),
Some(18.to_string())
]
}
]
}, &connection).unwrap();
assert_eq!(
serde_json::to_string(&list_products(&connection).unwrap()).unwrap(),
serde_json::to_string(&vec![
(
Product {
id: 1,
name: "boots".to_string(),
cost: 13.23,
active: true
},
vec![
(
Some(12.to_string()),
"size".to_string()
),
(
Some(14.to_string()),
"size".to_string()
),
(
Some(16.to_string()),
"size".to_string()
),
(
Some(18.to_string()),
"size".to_string()
)
]
),
]).unwrap()
);
Ok(())
});
}

The function list_products is created to list the last products with their variants.