실용적인 Rust 웹 개발 - GraphQL
GraphQL의 한 가지 장점은 유연성입니다. 하나의 쿼리에서 시간이 지남에 따라 코드를 쉽게 유지 관리하고 서버와 클라이언트 간의 더 쉬운 통신을 허용하는 데 필요한 모든 것을 얻을 수 있습니다.
Juniper은 GraphQL 서버를 생성할 수 있는 크레이트이며 우리 프로젝트에서 사용할 것입니다.
온라인 상점을 계속 진행하겠습니다. 사이트에서 판매를 제어해야 하므로 GraphQL 쿼리를 수신할 판매 모듈을 만들어 보겠습니다.
migrations/2019-07-28-191653_add_sales/up.sql
:CREATE TABLE sales (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
sale_date DATE NOT NULL,
total FLOAT NOT NULL
);
CREATE TABLE sale_products (
id SERIAL PRIMARY KEY,
product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
sale_id INTEGER NOT NULL REFERENCES sales(id) ON DELETE CASCADE,
amount FLOAT NOT NULL,
discount INTEGER NOT NULL,
tax INTEGER NOT NULL,
price INTEGER NOT NULL, --representing cents
total FLOAT NOT NULL
)
그런 다음 모든 쿼리를 수신할 엔드포인트가 필요합니다. 이것은 사후 요청이 될 것입니다.
src/graphql.rs
:
pub fn graphql(
st: web::Data<Arc<Schema>>,
data: web::Json<GraphQLRequest>,
user: LoggedUser,
pool: web::Data<PgPool>
) -> impl Future<Item = HttpResponse, Error = Error> {
web::block(move || {
let pg_pool = pool
.get()
.map_err(|e| {
serde_json::Error::custom(e)
})?;
let ctx = create_context(user.id, pg_pool);
let res = data.execute(&st, &ctx);
Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?)
})
.map_err(Error::from)
.and_then(|user| {
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(user))
})
}
src/main.rs
: HttpServer::new(
move || App::new()
.service(
web::resource("/graphql").route(web::post().to_async(graphql))
)
데이터를 읽는 응답을 출력하려면 쿼리가 필요하지만 상태를 수정해야 하는 경우 변형이 필요하므로 판매 모듈에 두 리소스를 모두 추가해 보겠습니다.
src/models/sale.rs
:use diesel::PgConnection;
use diesel::BelongingToDsl;
use diesel::sql_types;
use chrono::NaiveDate;
use juniper::{FieldResult};
use crate::schema;
use crate::schema::sales;
use crate::schema::sale_products;
use crate::db_connection::PgPooledConnection;
use crate::models::product::{ Product, PRODUCT_COLUMNS };
use crate::errors::MyStoreError;
#[derive(Identifiable, Queryable, Debug, Clone, PartialEq)]
#[table_name="sales"]
#[derive(juniper::GraphQLObject)]
#[graphql(description="Sale Bill")]
pub struct Sale {
pub id: i32,
pub user_id: i32,
pub sale_date: NaiveDate,
pub total: f64,
pub bill_number: Option<String>
}
#[derive(Insertable, Deserialize, Serialize, AsChangeset, Debug, Clone, PartialEq)]
#[table_name="sales"]
#[derive(juniper::GraphQLInputObject)]
#[graphql(description="Sale Bill")]
pub struct NewSale {
pub id: Option<i32>,
pub sale_date: Option<NaiveDate>,
pub user_id: Option<i32>,
pub total: Option<f64>,
pub bill_number: Option<String>
}
use crate::models::sale_product::{ SaleProduct, NewSaleProduct, NewSaleProducts, FullSaleProduct,FullNewSaleProduct };
#[derive(Debug, Clone)]
#[derive(juniper::GraphQLObject)]
pub struct FullSale {
pub sale: Sale,
pub sale_products: Vec<FullSaleProduct>
}
#[derive(Debug, Clone)]
#[derive(juniper::GraphQLObject)]
pub struct FullNewSale {
pub sale: NewSale,
pub sale_products: Vec<FullNewSaleProduct>
}
#[derive(Debug, Clone)]
#[derive(juniper::GraphQLObject)]
pub struct ListSale {
pub data: Vec<FullSale>
}
use std::sync::Arc;
pub struct Context {
pub user_id: i32,
pub conn: Arc<PgPooledConnection>,
}
impl juniper::Context for Context {}
pub struct Query;
type BoxedQuery<'a> =
diesel::query_builder::BoxedSelectStatement<'a, (sql_types::Integer,
sql_types::Integer,
sql_types::Date,
sql_types::Float8,
sql_types::Nullable<sql_types::Text>
),
schema::sales::table, diesel::pg::Pg>;
impl Sale {
fn searching_records<'a>(search: Option<NewSale>) -> BoxedQuery<'a> {
use diesel::QueryDsl;
use diesel::ExpressionMethods;
use crate::schema::sales::dsl::*;
let mut query = schema::sales::table.into_boxed::<diesel::pg::Pg>();
if let Some(sale) = search {
if let Some(sale_sale_date) = sale.sale_date {
query = query.filter(sale_date.eq(sale_sale_date));
}
if let Some(sale_bill_number) = sale.bill_number {
query = query.filter(bill_number.eq(sale_bill_number));
}
}
query
}
}
#[juniper::object(
Context = Context,
)]
impl Query {
fn listSale(context: &Context, search: Option<NewSale>, limit: i32)
-> FieldResult<ListSale> {
use diesel::{ QueryDsl, RunQueryDsl, ExpressionMethods, GroupedBy };
use crate::models::sale_product::SaleProduct;
let conn: &PgConnection = &context.conn;
let query = Sale::searching_records(search);
let query_sales: Vec<Sale> =
query
.filter(sales::dsl::user_id.eq(context.user_id))
.limit(limit.into())
.load::<Sale>(conn)?;
let query_products =
schema::products::table
.inner_join(schema::sale_products::table)
.select((PRODUCT_COLUMNS,
(schema::sale_products::id,
schema::sale_products::product_id,
schema::sale_products::sale_id,
schema::sale_products::amount,
schema::sale_products::discount,
schema::sale_products::tax,
schema::sale_products::price,
schema::sale_products::total)))
.load::<(Product, SaleProduct)>(conn)?;
let query_sale_products =
SaleProduct::belonging_to(&query_sales)
.inner_join(schema::products::table)
.select(((schema::sale_products::id,
schema::sale_products::product_id,
schema::sale_products::sale_id,
schema::sale_products::amount,
schema::sale_products::discount,
schema::sale_products::tax,
schema::sale_products::price,
schema::sale_products::total),
PRODUCT_COLUMNS))
.load::<(SaleProduct, Product)>(conn)?
.grouped_by(&query_sales);
let tuple_full_sale: Vec<(Sale, Vec<(SaleProduct, Product)>)> =
query_sales
.into_iter()
.zip(query_sale_products)
.collect::<Vec<(Sale, Vec<(SaleProduct, Product)>)>>();
let vec_full_sale = tuple_full_sale.iter().map (|tuple_sale| {
let full_sale_product = tuple_sale.1.iter().map(|tuple_sale_product| {
FullSaleProduct {
sale_product: tuple_sale_product.0.clone(),
product: tuple_sale_product.1.clone()
}
}).collect();
FullSale {
sale: tuple_sale.0.clone(),
sale_products: full_sale_product
}
}).collect();
Ok(ListSale { data: vec_full_sale })
}
fn sale(context: &Context, sale_id: i32) -> FieldResult<FullSale> {
use diesel::{ ExpressionMethods, QueryDsl, RunQueryDsl };
let conn: &PgConnection = &context.conn;
let sale: Sale =
schema::sales::table
.filter(sales::dsl::user_id.eq(context.user_id))
.find(sale_id)
.first::<Sale>(conn)?;
let sale_products =
SaleProduct::belonging_to(&sale)
.inner_join(schema::products::table)
.select(((schema::sale_products::id,
schema::sale_products::product_id,
schema::sale_products::sale_id,
schema::sale_products::amount,
schema::sale_products::discount,
schema::sale_products::tax,
schema::sale_products::price,
schema::sale_products::total),
PRODUCT_COLUMNS))
.load::<(SaleProduct, Product)>(conn)?
.iter()
.map(|tuple| {
FullSaleProduct {
sale_product: tuple.0.clone(),
product: tuple.1.clone()
}
})
.collect();
Ok(FullSale{ sale, sale_products })
}
}
pub struct Mutation;
#[juniper::object(
Context = Context,
)]
impl Mutation {
fn createSale(context: &Context, param_new_sale: NewSale, param_new_sale_products: NewSaleProducts)
-> FieldResult<FullSale> {
use diesel::{ RunQueryDsl, Connection, QueryDsl };
let conn: &PgConnection = &context.conn;
let new_sale = NewSale {
user_id: Some(context.user_id),
..param_new_sale
};
conn.transaction(|| {
let sale =
diesel::insert_into(schema::sales::table)
.values(new_sale)
.returning(
(
sales::dsl::id,
sales::dsl::user_id,
sales::dsl::sale_date,
sales::dsl::total,
sales::dsl::bill_number
)
)
.get_result::<Sale>(conn)?;
let sale_products: Result<Vec<FullSaleProduct>, _> =
param_new_sale_products.data.into_iter().map(|param_new_sale_product| {
let new_sale_product = NewSaleProduct {
sale_id: Some(sale.id),
..param_new_sale_product.sale_product
};
let sale_product =
diesel::insert_into(schema::sale_products::table)
.values(new_sale_product)
.returning(
(
sale_products::dsl::id,
sale_products::dsl::product_id,
sale_products::dsl::sale_id,
sale_products::dsl::amount,
sale_products::dsl::discount,
sale_products::dsl::tax,
sale_products::dsl::price,
sale_products::dsl::total
)
)
.get_result::<SaleProduct>(conn);
if let Some(param_product_id) = param_new_sale_product.sale_product.product_id {
let product =
schema::products::table
.select(PRODUCT_COLUMNS)
.find(param_product_id)
.first(conn);
Ok(
FullSaleProduct {
sale_product: sale_product?,
product: product?
}
)
} else {
Err(MyStoreError::PGConnectionError)
}
}).collect();
Ok(FullSale{ sale, sale_products: sale_products? })
})
}
fn updateSale(context: &Context, param_sale: NewSale, param_sale_products: NewSaleProducts)
-> FieldResult<FullSale> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use diesel::ExpressionMethods;
use diesel::Connection;
use crate::schema::sales::dsl;
let conn: &PgConnection = &context.conn;
let sale_id = param_sale.id.ok_or(
diesel::result::Error::QueryBuilderError("missing id".into())
)?;
conn.transaction(|| {
let sale =
diesel::update(dsl::sales
.filter(dsl::user_id.eq(context.user_id))
.find(sale_id))
.set(¶m_sale)
.get_result::<Sale>(conn)?;
let sale_products: Result<Vec<FullSaleProduct>, _> =
param_sale_products.data.into_iter().map (|param_sale_product| {
let sale_product =
diesel::update(schema::sale_products::table)
.set(¶m_sale_product.sale_product)
.get_result::<SaleProduct>(conn);
if let Some(param_product_id) = param_sale_product.sale_product.product_id {
let product =
schema::products::table
.select(PRODUCT_COLUMNS)
.find(param_product_id)
.first(conn);
Ok(
FullSaleProduct {
sale_product: sale_product?,
product: product?
}
)
} else {
Err(MyStoreError::PGConnectionError)
}
}).collect();
Ok(FullSale{ sale, sale_products: sale_products? })
})
}
fn destroySale(context: &Context, sale_id: i32)
-> FieldResult<i32> {
use diesel::QueryDsl;
use diesel::RunQueryDsl;
use diesel::ExpressionMethods;
use crate::schema::sales::dsl;
let conn: &PgConnection = &context.conn;
diesel::delete(dsl::sales.filter(dsl::user_id.eq(context.user_id)).find(sale_id))
.execute(conn)?;
Ok(sale_id)
}
}
pub type Schema = juniper::RootNode<'static, Query, Mutation>;
pub fn create_schema() -> Schema {
Schema::new(Query {}, Mutation {})
}
pub fn create_context(logged_user_id: i32, pg_pool: PgPooledConnection) -> Context {
Context { user_id: logged_user_id, conn: Arc::new(pg_pool)}
}
보시다시피 비즈니스 로직은 REST 엔드포인트와 매우 유사합니다. 변경할 수 있는 것은 데이터를 쿼리한 다음
create_schema
함수를 사용하여 스키마를 내보내는 것입니다.src/main.rs
: let schema = std::sync::Arc::new(create_schema());
HttpServer::new(
move || App::new()
.data(schema.clone())
이제 데이터를 가져오고 변형을 수행하는 방법은 무엇입니까? 그런 다음 몇 가지 테스트를 작성하고 모든 것이 예상대로 작동하는지 확인해야 합니다.
tests/sale_test.rs
:...
// create a sale:
let query =
format!(
r#"
{{
"query": "
mutation CreateSale($paramNewSale: NewSale!, $paramNewSaleProducts: NewSaleProducts!) {{
createSale(paramNewSale: $paramNewSale, paramNewSaleProducts: $paramNewSaleProducts) {{
sale {{
id
userId
saleDate
total
}}
saleProducts {{
product {{
name
}}
saleProduct {{
id
productId
amount
discount
tax
price
total
}}
}}
}}
}}
",
"variables": {{
"paramNewSale": {{
"saleDate": "{}",
"total": {}
}},
"paramNewSaleProducts": {{
"data":
[{{
"product": {{ }},
"saleProduct": {{
"amount": {},
"discount": {},
"price": {},
"productId": {},
"tax": {},
"total": {}
}}
}}]
}}
}}
}}"#,
...
// show a sale:
let query = format!(r#"
{{
"query": "
query ShowASale($saleId: Int!) {{
sale(saleId: $saleId) {{
sale {{
id
userId
saleDate
total
}}
saleProducts {{
product {{ name }}
saleProduct {{
id
productId
amount
discount
tax
price
total
}}
}}
}}
}}
",
"variables": {{
"saleId": {}
}}
}}
"#, id).replace("\n", "");
...
// update a sale
let query =
format!(
r#"
{{
"query": "
mutation UpdateSale($paramSale: NewSale!, $paramSaleProducts: NewSaleProducts!) {{
updateSale(paramSale: $paramSale, paramSaleProducts: $paramSaleProducts) {{
sale {{
id
saleDate
total
}}
saleProducts {{
product {{ name }}
saleProduct {{
id
productId
amount
discount
tax
price
total
}}
}}
}}
}}
",
"variables": {{
"paramSale": {{
"id": {},
"saleDate": "{}",
"total": {}
}},
"paramSaleProducts": {{
"data":
[{{
"product": {{}},
"saleProduct":
{{
"amount": {},
"discount": {},
"price": {},
"productId": {},
"tax": {},
"total": {}
}}
}}]
}}
}}
}}"#
...
// delete a sale:
let query = format!(r#"
{{
"query": "
mutation DestroyASale($saleId: Int!) {{
destroySale(saleId: $saleId)
}}
",
"variables": {{
"saleId": {}
}}
}}
"#, id).replace("\n", "");
...
// search for a sale with specific date:
let query = format!(r#"
{{
"query": "
query ListSale($search: NewSale!, $limit: Int!) {{
listSale(search: $search, limit: $limit) {{
data {{
sale {{
id
saleDate
total
}}
saleProducts {{
product {{
name
}}
saleProduct {{
amount
price
}}
}}
}}
}}
}}
",
"variables": {{
"search": {{
"saleDate": "2019-11-10"
}},
"limit": 10
}}
}}
"#).replace("\n", "");
전체 소스 코드here를 볼 수 있습니다.
Reference
이 문제에 관하여(실용적인 Rust 웹 개발 - GraphQL), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/werner/practical-rust-web-development-graphql-cdg텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)