创建资源
学习如何添加代码以向数据库插入数据。
创建产品
我们可以开始为项目创建所需的迁移。迁移名称可以随意命名,这里我们将其命名为 create_products,以添加创建和删除表所需的迁移。
注意:在按照附录一安装并配置好 Diesel 后,可以在终端中运行以下命令:
1diesel migration generate create_products
该命令将在 migrations 文件夹中生成两个文件:up.sql 和 down.sql。
我们将以下代码写入 up.sql 文件:
1CREATE TABLE products ( 2 id INTEGER PRIMARY KEY, 3 name VARCHAR NOT NULL, 4 cost DOUBLE NOT NULL, 5 active BOOLEAN NOT NULL DEFAULT 0 -- SQLite 不支持布尔类型 6)
接下来处理 down.sql 文件:
1drop table products
现在,我们可以通过在终端运行以下命令来执行迁移,从而创建 products 表:
1diesel migration run
我们在 src 文件夹中创建一个名为 models.rs 的文件,用于定义模型,其中包括 products 模型。
1use diesel::Insertable; 2use shoe_store::schema::*; 3 4#[derive(Insertable, Debug)] 5#[table_name="products"] 6pub struct NewProduct { 7 pub name: String, 8 pub cost: f64, 9 pub active: bool, 10}
该结构体将作为向数据库插入数据的模型。因此,它需要实现 Insertable trait,并且必须指定对应的表名。现在我们已经准备好编写创建产品的代码。
1use ::shoe_store::models::NewProduct; 2use diesel::sqlite::SqliteConnection; 3use diesel::result::Error; 4use diesel::RunQueryDsl; 5 6fn create_product(new_product: NewProduct, conn: &SqliteConnection) -> Result<usize, Error> { 7 use ::shoe_store::schema::products::dsl::*; 8 diesel::insert_into(products) 9 .values(new_product) 10 .execute(conn) 11}
insert_into 函数需要一个目标表,在此情况下是 products,它来自 products 模式的 DSL 模块。我们可以使用 values 方法插入数据,支持单个或多个值。在我们的场景中,需要插入一个产品,该产品将在未来的接口中使用。
该函数成功时返回插入的行数,失败时返回错误。
现在我们可以编写测试代码,验证代码是否按预期工作。
1use diesel::Connection; 2use ::shoe_store::establish_connection_test; 3 4#[test] 5fn create_product_test() { 6 let connection = establish_connection_test(); 7 connection.test_transaction::<_, Error, _>(|| { 8 let results = 9 create_product(NewProduct { 10 name: "boots".to_string(), 11 cost: 13.23, 12 active: true 13 }, &connection); 14 assert_eq!(Ok(1), results); 15 16 Ok(()) 17 }); 18}
上述代码中,我们定义了一个名为 create_product_test 的测试函数。#[test] 标注表明这是一个测试函数。
我们使用 establish_connection_test 函数连接到测试数据库。在此处,我们调用 test_transaction 函数,它非常有用,因为它不会将更改提交到数据库,从而确保每次运行测试时数据库都保持干净。
我们验证了成功插入了一条记录。
创建变体
在项目中,我们很可能需要关联关系——例如,鞋子可能有不同的尺码、款式等。
虽然可以将所有鞋类特征放在同一张表中,但这会导致一张包含大量字段的巨大表,且不符合数据库规范化原则。
我们需要创建另一张表来存储鞋类变体。
1diesel migration generate create_variants
以下是 up.sql 文件的内容:
1CREATE TABLE variants ( 2 id INTEGER PRIMARY KEY NOT NULL, 3 name VARCHAR NOT NULL 4); 5 6CREATE TABLE products_variants ( 7 id INTEGER PRIMARY KEY NOT NULL, 8 variant_id INTEGER NOT NULL, 9 product_id INTEGER NOT NULL, 10 value VARCHAR, 11 FOREIGN KEY(variant_id) REFERENCES variants(id), 12 FOREIGN KEY(product_id) REFERENCES products(id) 13);
以下是 down.sql 文件的内容:
1drop table variants; 2drop table products_variants;
上述代码中未包含索引,但应根据常见的查询需求添加索引。接下来运行迁移。
1diesel migration run 2diesel migration run --database-url='db/my_project_test.sqlite'
以下是用于创建新变体所需模型的代码。
1use diesel::{ Insertable, Identifiable, Queryable}; 2use serde::{Serialize, Deserialize}; 3use shoe_store::schema::variants; 4 5#[derive(Identifiable, Queryable, Debug, Serialize, Deserialize)] 6#[table_name = "variants"] 7pub struct Variant { 8 pub id: i32, 9 pub name: String, 10} 11 12#[derive(Insertable, Debug, Clone)] 13#[table_name="variants"] 14pub struct NewVariant { 15 pub name: String, 16} 17 18use shoe_store::schema::products_variants; 19 20#[derive(Insertable, Debug)] 21#[table_name="products_variants"] 22pub struct NewProductVariant { 23 pub product_id: i32, 24 pub variant_id: i32, 25 pub value: Option<String> 26}
我们可能还需要一些不直接对应数据库表的额外模型,用于业务逻辑。
1#[derive(Clone)] 2pub struct NewVariantValue { 3 pub variant: NewVariant, 4 pub values: Vec<Option<String>> 5} 6 7pub struct NewCompleteProduct { 8 pub product: NewProduct, 9 pub variants: Vec<NewVariantValue> 10}
接下来,我们编写创建与产品相关的变体所需的代码。
1#[macro_use] 2extern crate diesel; 3 4use anyhow::Result; 5use diesel::sqlite::SqliteConnection; 6use diesel::ExpressionMethods; 7use ::shoe_store::models::*; 8use diesel::Connection; 9use diesel::RunQueryDsl; 10use diesel::query_dsl::QueryDsl; 11 12no_arg_sql_function!(last_insert_rowid, diesel::sql_types::Integer); 13 14fn create_product(new_product: NewCompleteProduct, conn: &SqliteConnection) -> Result<i32> { 15 use ::shoe_store::schema::products::dsl::products; 16 use ::shoe_store::schema::variants::dsl::*; 17 use ::shoe_store::schema::products_variants::dsl::*; 18 19 conn.transaction(|| { 20 diesel::insert_into(products) 21 .values(new_product.product) 22 .execute(conn)?; 23 24 let last_product_id: i32 = diesel::select(last_insert_rowid).first(conn)?; 25 26 for new_variant in new_product.variants { 27 let variants_result = 28 variants 29 .filter(name.eq(&new_variant.variant.name)) 30 .limit(1) 31 .load::<Variant>(conn)?; 32 33 let last_variant_id: i32 = 34 match variants_result.first() { 35 Some(variant) => variant.id, 36 None => { 37 diesel::insert_into(variants) 38 .values(name.eq(&new_variant.variant.name)) 39 .execute(conn)?; 40 41 diesel::select(last_insert_rowid).first(conn)? 42 } 43 }; 44 45 for new_value in new_variant.values { 46 diesel::insert_into(products_variants) 47 .values( 48 ( 49 product_id.eq(last_product_id), 50 variant_id.eq(last_variant_id), 51 value.eq(new_value), 52 ) 53 ) 54 .execute(conn)?; 55 } 56 } 57 Ok(last_product_id) 58 }) 59}
在上述代码中,我们使用了宏 no_arg_sql_function!,允许我们在代码中调用 SQL 函数。由于 SQLite 尚未实现 RETURNING 语法,因此我们需要使用 last_insert_rowid 函数来获取最后插入的 ID。
注意:必须在事务中使用此函数。
我们定义了一个名为 create_product 的函数,其参数类型为 NewCompleteProduct。该函数将包含创建产品及其变体所需的所有数据。
在代码中,我们遍历新的变体,根据名称进行过滤,检查该变体是否已存在,避免重复创建,若不存在则创建它。
最后,我们建立了产品与变体之间的关联关系。
接下来,我们需要编写单元测试以验证代码是否正常工作。
1use ::shoe_store::establish_connection_test; 2use diesel::result::Error; 3 4#[test] 5fn create_product_test() { 6 let connection = establish_connection_test(); 7 connection.test_transaction::<_, Error, _>(|| { 8 create_product(NewCompleteProduct { 9 product: NewProduct { 10 name: "boots".to_string(), 11 cost: 13.23, 12 active: true 13 }, 14 variants: vec![ 15 NewVariantValue { 16 variant: NewVariant { 17 name: "size".to_string() 18 }, 19 values: vec![ 20 Some(12.to_string()), 21 Some(14.to_string()), 22 Some(16.to_string()), 23 Some(18.to_string()) 24 ] 25 } 26 ] 27 }, &connection).unwrap(); 28 29 assert_eq!( 30 serde_json::to_string(&list_products(&connection).unwrap()).unwrap(), 31 serde_json::to_string(&vec![ 32 ( 33 Product { 34 id: 1, 35 name: "boots".to_string(), 36 cost: 13.23, 37 active: true 38 }, 39 vec![ 40 ( 41 Some(12.to_string()), 42 "size".to_string() 43 ), 44 ( 45 Some(14.to_string()), 46 "size".to_string() 47 ), 48 ( 49 Some(16.to_string()), 50 "size".to_string() 51 ), 52 ( 53 Some(18.to_string()), 54 "size".to_string() 55 ) 56 ] 57 ), 58 ]).unwrap() 59 ); 60 61 Ok(()) 62 }); 63}
list_products 函数用于列出最新的产品及其变体。