GraphQL — это язык запросов для чтения и манипулирования данными для API. Его приоритетом является предоставление клиентам или серверам точных данных, обеспечивая гибкий и интуитивно понятный синтаксис для описания таких данных.
По сравнению с традиционным REST API, GraphQL предоставляет систему типов для описания схем данных и, в свою очередь, дает потребителям API возможность исследовать и запрашивать необходимые данные с помощью одной конечной точки.
В этом посте мы рассмотрим создание приложения для управления проектами на Rust с использованием библиотеки Async-graphql и MongoDB. В конце этого руководства мы узнаем, как создать конечную точку GraphQL, поддерживающую чтение и манипулирование данными управления проектами, и сохранить наши данные с помощью MongoDB.
Репозиторий GitHub можно найти здесь.
Предварительные условия
Для полного понимания концепций, представленных в этом руководстве, необходим опыт работы с Rust. Опыт работы с MongoDB не является обязательным условием, но его желательно иметь.
Нам также понадобится следующее:
- Базовые знания GraphQL
- Учетная запись MongoDB для размещения базы данных. Регистрация совершенно бесплатна
Давайте писать код
Начало работы
Чтобы начать работу, нам нужно перейти в нужную директорию и выполнить в терминале следующую команду
cargo new project-mngt-rust-graphql-rocket && cd project-mngt-rust-graphql-rocket
Эта команда создает проект Rust под названием project-mngt-rust-graphql-rocket
и переходит в каталог проекта.
Далее мы устанавливаем необходимые зависимости, изменяя секцию [dependencies]
файла Cargo.toml
, как показано ниже:
//other code section goes here
[dependencies]
rocket = {version = "0.5.0-rc.2", features = ["json"]}
async-graphql = { version = "4.0", features = ["bson", "chrono"] }
async-graphql-rocket = "4.0.0"
serde = "1.0.136"
dotenv = "0.15.0"
[dependencies.mongodb]
version = "2.2.0"
default-features = false
features = ["sync"]
rocket = {version = "0.5.0-rc.2", features = ["json"]}
— это основанный на Rust фреймворк для создания веб-приложений. Здесь также указывается требуемая версия и тип функций (json).
async-graphql = { version = "4.0", features = ["bson", "chrono"] }
— серверная библиотека для построения GraphQL в Rust. Она также включает bson
и chrono
.
async-graphql-rocket = "4.0"
— это библиотека, которая помогает интегрировать async-grapql
с Rocket
.
serde = "1.0.136"
— это фреймворк для сериализации и десериализации структур данных Rust. Например, преобразование структур Rust в JSON.
dotenv = "0.15.0"
— библиотека для управления переменными окружения.
[dependencies.mongodb]
— драйвер для подключения к MongoDB. Здесь также указывается необходимая версия и тип функции (Sync API).
Нам нужно выполнить приведенную ниже команду для установки зависимостей:
cargo build
Система модулей в Rust
Модули — это как структуры папок в нашем приложении; они упрощают управление зависимостями.
Для этого нам нужно перейти в папку src
и создать папки config
, handler
и schemas
с соответствующими им файлами mod.rs
для управления видимостью.
config
предназначен для модульной организации конфигурационных файлов.
handler
предназначен для модуляции логики GraphQL.
schemas
— для модуляции схем GraphQL.
Добавление ссылки на модули
Чтобы использовать код в модулях, нам нужно объявить их как модуль и импортировать в файл main.rs
.
//add modules
mod config;
mod handler;
mod schemas;
fn main() {
println!("Hello, world!");
}
Настройка MongoDB
После этого нам нужно войти или зарегистрироваться в нашей учетной записи MongoDB. Щелкните на выпадающем меню проекта и нажмите на кнопку New Project.
Введите projectMngt
в качестве имени проекта, нажмите Next и нажмите Create Project.
Нажмите на кнопку Создать базу данных
Выберите Shared в качестве типа базы данных.
Нажмите кнопку Создать, чтобы настроить кластер. Настройка может занять некоторое время.
Далее нам нужно создать пользователя для внешнего доступа к базе данных, введя имя пользователя, пароль и нажав на кнопку Создать пользователя. Нам также нужно добавить наш IP-адрес для безопасного подключения к базе данных, нажав на кнопку Add My Current IP Address. Затем нажмите на Finish и Close, чтобы сохранить изменения.
После сохранения изменений мы должны увидеть экран Database Deployments, как показано ниже:
Подключение нашего приложения к MongoDB
После завершения конфигурации нам необходимо подключить наше приложение к созданной базе данных. Для этого нажмите на кнопку Connect
Нажмите на Connect your application, измените Driver на Rust
и Version, как показано ниже. Затем нажмите на значок копирования, чтобы скопировать строку подключения.
Настройка переменной среды
Далее мы должны изменить скопированную строку подключения с паролем пользователя, который мы создали ранее, и изменить имя базы данных. Для этого сначала нужно создать файл .env
в корневом каталоге, и в этот файл добавить фрагмент, приведенный ниже:
MONGOURI=mongodb+srv://<YOUR USERNAME HERE>:<YOUR PASSWORD HERE>@cluster0.e5akf.mongodb.net/<DATABASE NAME>?retryWrites=true&w=majority
Образец правильно заполненной строки подключения приведен ниже:
MONGOURI=mongodb+srv://malomz:malomzPassword@cluster0.e5ahghkf.mongodb.net/projectMngt?retryWrites=true&w=majority
Создание конечных точек GraphQL
После настройки нам необходимо создать схему для представления данных нашего приложения. Для этого нам нужно перейти в папку schemas
, в ней создать файл project_schema.rs
и добавить фрагмент ниже:
use async_graphql::{Enum, InputObject, SimpleObject};
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
//owner schema
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct Owner {
#[serde(skip_serializing_if = "Option::is_none")]
pub _id: Option<ObjectId>,
pub name: String,
pub email: String,
pub phone: String,
}
#[derive(InputObject)]
pub struct CreateOwner {
pub name: String,
pub email: String,
pub phone: String,
}
#[derive(InputObject)]
pub struct FetchOwner {
pub _id: String,
}
//project schema
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Enum)]
pub enum Status {
NotStarted,
InProgress,
Completed,
}
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
pub struct Project {
#[serde(skip_serializing_if = "Option::is_none")]
pub _id: Option<ObjectId>,
pub owner_id: String,
pub name: String,
pub description: String,
pub status: Status,
}
#[derive(InputObject)]
pub struct CreateProject {
pub owner_id: String,
pub name: String,
pub description: String,
pub status: Status,
}
#[derive(InputObject)]
pub struct FetchProject {
pub _id: String,
}
Приведенный выше фрагмент делает следующее:
- Импортирует необходимые зависимости
- Использует макрос
derive
для создания поддержки реализации дляOwner
,CreateOwner
,FetchOwner
,Status
,Project
,CreateProject
иFetchProject
. Сниппет также использует процедурный макрос из библиотекиserde
иasync-graphql
для сериализации/десериализации и преобразования структур Rust в схему GraphQL.
Далее мы должны зарегистрировать файл project_schema.rs
как часть модуля schemas
. Для этого откройте файл mod.rs
в папке schemas
и добавьте приведенный ниже фрагмент:
pub mod project_schema;
Логика работы с базой данных
Когда схема полностью настроена и доступна для потребления, мы можем создать логику базы данных, которая будет выполнять следующие действия:
- Создать владельца проекта
- Получить всех владельцев
- Получить одного владельца
- Создать проект
- Получить все проекты
- Получить один проект
Для этого сначала нужно перейти в папку config
, а в ней создать файл mongo.rs
и добавить фрагмент ниже:
use dotenv::dotenv;
use std::{env, io::Error};
use mongodb::{
bson::{doc, oid::ObjectId},
sync::{Client, Collection, Database},
};
use crate::schemas::project_schema::{Owner, Project};
pub struct DBMongo {
db: Database,
}
impl DBMongo {
pub fn init() -> Self {
dotenv().ok();
let uri = match env::var("MONGOURI") {
Ok(v) => v.to_string(),
Err(_) => format!("Error loading env variable"),
};
let client = Client::with_uri_str(uri).unwrap();
let db = client.database("projectMngt");
DBMongo { db }
}
fn col_helper<T>(data_source: &Self, collection_name: &str) -> Collection<T> {
data_source.db.collection(collection_name)
}
}
Приведенный выше фрагмент делает следующее:
- Импортирует необходимые зависимости
- Создает структуру
DBMongo
с полемdb
для доступа к базе данных MongoDB - Создает блок реализации, который добавляет методы к структуре
DBMongo
- Добавляет метод
init
в блок реализации для загрузки переменной окружения, создает соединение с базой данных и возвращает экземплярDBMongo
struct - Добавляет метод
col_helper
; вспомогательную функцию для создания коллекции MongoDB
Далее нам нужно добавить оставшиеся методы в реализацию DBMongo
для обслуживания операций управления проектом:
//imports goes here
pub struct DBMongo {
db: Database,
}
impl DBMongo {
pub fn init() -> Self {
//init code goes here
}
fn col_helper<T>(data_source: &Self, collection_name: &str) -> Collection<T> {
data_source.db.collection(collection_name)
}
//Owners logic
pub fn create_owner(&self, new_owner: Owner) -> Result<Owner, Error> {
let new_doc = Owner {
_id: None,
name: new_owner.name.clone(),
email: new_owner.email.clone(),
phone: new_owner.phone.clone(),
};
let col = DBMongo::col_helper::<Owner>(&self, "owner");
let data = col
.insert_one(new_doc, None)
.ok()
.expect("Error creating owner");
let new_owner = Owner {
_id: data.inserted_id.as_object_id(),
name: new_owner.name.clone(),
email: new_owner.email.clone(),
phone: new_owner.phone.clone(),
};
Ok(new_owner)
}
pub fn get_owners(&self) -> Result<Vec<Owner>, Error> {
let col = DBMongo::col_helper::<Owner>(&self, "owner");
let cursors = col
.find(None, None)
.ok()
.expect("Error getting list of owners");
let owners: Vec<Owner> = cursors.map(|doc| doc.unwrap()).collect();
Ok(owners)
}
pub fn single_owner(&self, id: &String) -> Result<Owner, Error> {
let obj_id = ObjectId::parse_str(id).unwrap();
let filter = doc! {"_id": obj_id};
let col = DBMongo::col_helper::<Owner>(&self, "owner");
let owner_detail = col
.find_one(filter, None)
.ok()
.expect("Error getting owner's detail");
Ok(owner_detail.unwrap())
}
//project logics
pub fn create_project(&self, new_project: Project) -> Result<Project, Error> {
let new_doc = Project {
_id: None,
owner_id: new_project.owner_id.clone(),
name: new_project.name.clone(),
description: new_project.description.clone(),
status: new_project.status.clone(),
};
let col = DBMongo::col_helper::<Project>(&self, "project");
let data = col
.insert_one(new_doc, None)
.ok()
.expect("Error creating project");
let new_project = Project {
_id: data.inserted_id.as_object_id(),
owner_id: new_project.owner_id.clone(),
name: new_project.name.clone(),
description: new_project.description.clone(),
status: new_project.status.clone(),
};
Ok(new_project)
}
pub fn get_projects(&self) -> Result<Vec<Project>, Error> {
let col = DBMongo::col_helper::<Project>(&self, "project");
let cursors = col
.find(None, None)
.ok()
.expect("Error getting list of projects");
let projects: Vec<Project> = cursors.map(|doc| doc.unwrap()).collect();
Ok(projects)
}
pub fn single_project(&self, id: &String) -> Result<Project, Error> {
let obj_id = ObjectId::parse_str(id).unwrap();
let filter = doc! {"_id": obj_id};
let col = DBMongo::col_helper::<Project>(&self, "project");
let project_detail = col
.find_one(filter, None)
.ok()
.expect("Error getting project's detail");
Ok(project_detail.unwrap())
}
}
Приведенный выше фрагмент делает следующее:
- Добавляет метод
create_owner
, который принимает в качестве параметровself
иnew_owner
и возвращает созданного владельца или ошибку. Внутри метода мы создали новый документ, используя структуруOwner
. Затем мы использовали методcol_helper
для создания новой коллекции и обратились к функцииinsert_one
для создания нового владельца и обработки ошибок. Наконец, мы вернули информацию о созданном владельце. - Добавляет метод
get_owners
, который принимает в качестве параметровself
и возвращает список владельцев или ошибку. Внутри метода мы используем методcol_helper
для создания новой коллекции и обращаемся к функцииfind
без фильтра, чтобы она могла найти все документы в базе данных и вернуть список оптимальным образом, используя методmap()
для цикла по списку владельцев и обработки ошибок. - Добавляет метод
single_owner
, который принимает в качестве параметровself
иid
и возвращает данные о владельце или ошибку. Внутри метода мы преобразовалиid
вObjectId
и использовали его в качестве фильтра для получения соответствующего документа. Затем мы используем методcol_helper
для создания новой коллекции и обращаемся к функцииfind_one
из коллекции для получения сведений о владельце и обработки ошибок. - Добавляет метод
create_project
, который принимает в качестве параметровself
иnew_project
и возвращает созданный проект или ошибку. Внутри метода мы создали новый документ, используя структуруProject
. Затем мы использовали методcol_helper
для создания новой коллекции и обратились к функцииinsert_one
для создания нового проекта и обработки ошибок. Наконец, мы вернули информацию о созданном проекте - Добавляет метод
get_projects
, который принимает в качестве параметровself
и возвращает список проектов или ошибку. Внутри метода мы используем методcol_helper
для создания новой коллекции и обращаемся к функцииfind
без фильтра, чтобы она могла найти все документы в базе данных и оптимально вернуть список, используя методmap()
для циклического просмотра списка проектов и обработки ошибок. - Добавляет метод
single_project
, который принимает в качестве параметровself
иid
и возвращает деталь проекта или ошибку. Внутри метода мы преобразовалиid
вObjectId
и использовали его в качестве фильтра для получения соответствующего документа. Затем мы используем методcol_helper
для создания новой коллекции и обращения к функцииfind_one
из коллекции для получения подробной информации о проекте и обработки ошибок.
Наконец, мы должны зарегистрировать файл mongo.rs
как часть модуля config
. Для этого откройте файл mod.rs
в папке config
и добавьте приведенный ниже фрагмент:
pub mod mongo;
Обработчики GraphQL
Разобравшись с логикой базы данных, мы можем начать использовать их для создания обработчиков GraphQL. Для этого сначала нужно перейти в папку handler
, а в ней создать файл graphql_handler.rs
и добавить в него приведенный ниже фрагмент:
use crate::{
config::mongo::DBMongo,
schemas::project_schema::{
CreateOwner, CreateProject, FetchOwner, FetchProject, Owner, Project,
},
};
use async_graphql::{Context, EmptySubscription, FieldResult, Object, Schema};
pub struct Query;
#[Object(extends)]
impl Query {
//owners query
async fn owner(&self, ctx: &Context<'_>, input: FetchOwner) -> FieldResult<Owner> {
let db = &ctx.data_unchecked::<DBMongo>();
let owner = db.single_owner(&input._id).unwrap();
Ok(owner)
}
async fn get_owners(&self, ctx: &Context<'_>) -> FieldResult<Vec<Owner>> {
let db = &ctx.data_unchecked::<DBMongo>();
let owners = db.get_owners().unwrap();
Ok(owners)
}
//projects query
async fn project(&self, ctx: &Context<'_>, input: FetchProject) -> FieldResult<Project> {
let db = &ctx.data_unchecked::<DBMongo>();
let project = db.single_project(&input._id).unwrap();
Ok(project)
}
async fn get_projects(&self, ctx: &Context<'_>) -> FieldResult<Vec<Project>> {
let db = &ctx.data_unchecked::<DBMongo>();
let projects = db.get_projects().unwrap();
Ok(projects)
}
}
pub struct Mutation;
#[Object]
impl Mutation {
//owner mutation
async fn create_owner(&self, ctx: &Context<'_>, input: CreateOwner) -> FieldResult<Owner> {
let db = &ctx.data_unchecked::<DBMongo>();
let new_owner = Owner {
_id: None,
email: input.email,
name: input.name,
phone: input.phone,
};
let owner = db.create_owner(new_owner).unwrap();
Ok(owner)
}
async fn create_project(
&self,
ctx: &Context<'_>,
input: CreateProject,
) -> FieldResult<Project> {
let db = &ctx.data_unchecked::<DBMongo>();
let new_project = Project {
_id: None,
owner_id: input.owner_id,
name: input.name,
description: input.description,
status: input.status,
};
let project = db.create_project(new_project).unwrap();
Ok(project)
}
}
pub type ProjectSchema = Schema<Query, Mutation, EmptySubscription>;
Приведенный выше фрагмент делает следующее:
- Импортирует необходимые зависимости
- Создает
Query
struct с методами реализации, связанными с запросом к базе данных, используя соответствующие методы из логики базы данных - Создает
Mutation
struct с методами реализации, связанными с модификацией базы данных, используя соответствующие методы из логики базы данных. - Создает тип
ProjectSchema
для построения того, как наш GraphQL используетQuery
struct,Mutation
struct иEmptySubscription
, поскольку у нас нет никаких подписок.
Создание сервера GraphQL
Наконец, мы можем начать создавать наш GraphQL-сервер, интегрируя ProjectSchema
и MongoDB с Actix web. Для этого нам нужно перейти к файлу main.rs
и изменить его, как показано ниже:
mod config;
mod handler;
mod schemas;
//add
use async_graphql::{
http::{playground_source, GraphQLPlaygroundConfig},
EmptySubscription, Schema,
};
use async_graphql_rocket::{GraphQLQuery, GraphQLRequest, GraphQLResponse};
use config::mongo::DBMongo;
use handler::graphql_handler::{Mutation, ProjectSchema, Query};
use rocket::{response::content, routes, State};
#[rocket::get("/graphql?<query..>")]
async fn graphql_query(schema: &State<ProjectSchema>, query: GraphQLQuery) -> GraphQLResponse {
query.execute(schema).await
}
#[rocket::post("/graphql", data = "<request>", format = "application/json")]
async fn graphql_mutation(
schema: &State<ProjectSchema>,
request: GraphQLRequest,
) -> GraphQLResponse {
request.execute(schema).await
}
#[rocket::get("/")]
async fn graphql_playground() -> content::RawHtml<String> {
content::RawHtml(playground_source(GraphQLPlaygroundConfig::new("/graphql")))
}
#[rocket::launch]
fn rocket() -> _ {
let db = DBMongo::init();
let schema = Schema::build(Query, Mutation, EmptySubscription)
.data(db)
.finish();
rocket::build().manage(schema).mount(
"/",
routes![graphql_query, graphql_mutation, graphql_playground],
)
}
Приведенный выше фрагмент делает следующее:
- Импортирует необходимые зависимости
- Создает функцию
graphql_query
с процедурным макросомget
для указания маршрута GraphQL и использует типProjectSchema
для выполнения методов, связанных с запросом к базе данных - Создает функцию
graphql_mutation
с процедурным макросомpost
для указания маршрута GraphQL и использует типProjectSchema
для выполнения методов, связанных с модификацией базы данных - Создает функцию
graphql_playground
для создания GraphiQL; площадки GraphQL, к которой мы можем получить доступ через браузер. - Использует макрос
#[rocket::launch]
для запуска функцииrocket
, которая генерирует точку входа приложения и запускает сервер. Функцияrocket
также делает следующее:- Создает переменную
db
для установления соединения с MongoDB путем вызова методаinit()
и использует ее для построения данных GraphQL. - Создает приложение с помощью функции
build
, добавляетschema
в state, и настраивает маршрут для включенияgraphql_query
,graphql_mutation
, иgraphql_playground
.
- Создает переменную
После этого мы можем протестировать наше приложение, выполнив приведенную ниже команду в терминале.
cargo run
Затем перейдите по адресу http://127.0.0.1:8000
в веб-браузере.
Заключение
В этой статье мы рассмотрели, как модульно оформить приложение Rust, создать сервер GraphQL и сохранить наши данные с помощью MongoDB.
Эти ресурсы могут быть полезны:
- Async-GraphQL
- Rocket
- MongoDB Rust Driver
- Построение REST API с помощью Rust и MongoDB
- Интеграция Async-GraphQL Rocket
- Serde (библиотека сериализации и десериализации)