Како направити моћне ГрапхКЛ сервере са Руст-ом

Постављање ГрапхКЛ сервера са Руст, Јунипер, Диесел и Ацтик; успут учећи о Рустовим мрежним оквирима и моћним макроима.

Изворни код: гитхуб.цом/ивилсонк/руст-грапхкл-екампле

Послуживање апликација путем ГрапхКЛ-а брзо постаје најлакши и најефикаснији начин испоруке података клијентима. Било да сте на мобилном уређају или у прегледачу, он пружа тражене податке и ништа више.

Клијентске апликације више не морају да спајају информације из засебних извора података. ГрапхКЛ сервери су задужени за интеграцију, уклањајући потребу за вишком података и повратним захтевима за подацима.

То наравно подразумева да сервер мора да обрађује податке који се прикупљају из различитих извора, као што су матичне позадинске услуге, базе података или АПИ-ји независних произвођача. Ово може захтевати много ресурса, како можемо оптимизирати за процесорско време?

Руст је чудо од језика, спајајући сирове перформансе језика ниског нивоа попут Ц са изражајношћу модерних језика. Наглашава сигурност типа и меморије, посебно када постоје потенцијалне трке података у истовременим операцијама.

Погледајмо шта иде у изградњу ГрапхКЛ сервера са Рустом. Научићемо о томе

  • Јунипер ГрапхКЛ Сервер
  • Ацтик веб фрамеворк интегрисан са Јунипер
  • Дизел за постављање СКЛ базе података
  • Корисни Руст макрои и изведене особине за рад са овим библиотекама

Имајте на уму да нећу улазити у детаље у вези са инсталирањем Руст-а или Царго-а. Овај чланак претпоставља неко прелиминарно знање о ланцу алата Руст.

Постављање ХТТП сервера

За почетак морамо да иницијализујемо наш пројекат са, cargoа затим инсталирамо зависности.

 cargo new rust-graphql-example cd rust-graphql-example 

Команда за иницијализацију покреће датотеку Царго.томл која садржи зависности од наших пројеката, као и датотеку маин.рс која има једноставан пример „Хелло Ворлд“.

 // main.rs fn main() { println!("Hello, world!"); } 

Као провера исправности, слободно трчите cargo runкако бисте извршили програм.

Инсталирање потребних библиотека у Руст значи додавање реда који садржи име библиотеке и број верзије. Ажурирајмо одељке зависности Царго.томл овако:

 # Cargo.toml [dependencies] actix-web = "1.0.0" diesel = { version = "1.0.0", features = ["postgres"] } dotenv = "0.9.0" env_logger = "0.6" futures = "0.1" juniper = "0.13.1" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" 

Овај чланак ће обухватити примену ГрапхКЛ сервера који користи Јунипер као ГрапхКЛ библиотеку и Ацтик као основни ХТТП сервер. Ацтик има веома леп АПИ и добро функционише са стабилном верзијом Руст-а.

Када се додају те линије, следећи пут када се пројекат компајлира укључиће те библиотеке. Пре него што компајлирамо, дозволите да ажурирамо маин.рс основним ХТТП сервером, рукујући индексном рутом.

 // main.rs use std::io; use actix_web::{web, App, HttpResponse, HttpServer, Responder}; fn main() -> io::Result { HttpServer::new(|| { App::new() .route("/", web::get().to(index)) }) .bind("localhost:8080")? .run() } fn index() -> impl Responder { HttpResponse::Ok().body("Hello world!") } 

Прва два реда на врху доводе модул који нам је потребан у опсег. Главна функција овде враћа io::Resultтип, који нам омогућава да упитник користимо као скраћеницу за руковање резултатима.

Усмеравање и конфигурација сервера креирају се у инстанци App, која се креира у затварању које обезбеђује конструктор ХТТП сервера.

Самом рутом управља функција индекса, чије је име произвољно. Све док се ова функција правилно примењује Responder, може се користити као параметар за ГЕТ захтев на индексној рути.

Да пишемо РЕСТ АПИ, могли бисмо да наставимо са додавањем додатних рута и повезаних руковалаца. Ускоро ћемо видети да тргујемо списком рутера за руковање објектима и њиховим односима.

Сада ћемо представити ГрапхКЛ библиотеку.

Креирање ГрапхКЛ шеме

У основи сваке ГрапхКЛ шеме налази се роот захтев. Из овог корена можемо тражити листе објеката, одређених објеката и било којих поља која они могу садржати.

Назовите ово КуериРоот, а ми ћемо га означити истим именом у нашем коду. Будући да нећемо постављати базу података или било које АПИ-је независних произвођача, тешко ћемо кодирати мало података које овде имамо.

Да бисмо овом примеру додали мало боје, шема ће приказати генеричку листу чланова.

Под срц додајте нову датотеку која се зове грапхкл_сцхема.рс заједно са следећим садржајем:

 // graphql_schema.rs use juniper::{EmptyMutation, RootNode}; struct Member { id: i32, name: String, } #[juniper::object(description = "A member of a team")] impl Member { pub fn id(&self) -> i32 { self.id } pub fn name(&self) -> &str { self.name.as_str() } } pub struct QueryRoot; #[juniper::object] impl QueryRoot { fn members() -> Vec { vec![ Member { id: 1, name: "Link".to_owned(), }, Member { id: 2, name: "Mario".to_owned(), } ] } } 

Заједно са нашим увозом, ми дефинишемо наш први ГрапхКЛ објекат у овом пројекту, члан. Они су једноставна бића, са личним именом и именом. О сложенијим пољима и обрасцима размишљаћемо касније.

Након што избацимо QueryRootтип као структуру јединице, дефинисаћемо само поље. Јунипер излаже Руст макро који се зове objectкоји нам омогућава да дефинишемо поља на различитим чворовима кроз нашу шему. За сада имамо само чвор КуериРоот, па ћемо на њему изложити поље звано чланови.

Макрои рђе често имају необичну синтаксу у поређењу са стандардним функцијама. Они не узимају само неке аргументе и дају резултат, они се проширују у много сложенији код током компајлирања.

Излагање шеме

Испод нашег макро позива за креирање поља чланова, дефинисаћемо RootNodeтип који излажемо на нашој шеми.

 // graphql_schema.rs pub type Schema = RootNode<'static, QueryRoot, EmptyMutation>; pub fn create_schema() -> Schema { Schema::new(QueryRoot {}, EmptyMutation::new()) } 

Због јаког куцања у Русту, присиљени смо да пружимо аргумент објекта мутације. Јунипер излаже EmptyMutationструктуру само за ову прилику, односно када желимо да створимо шему само за читање.

Сада када је шема припремљена, можемо да ажурирамо наш сервер на маин.рс да би управљао рутом „/ грапхкл“. Пошто је поседовање игралишта такође лепо, додаћемо руту за ГрапхиКЛ, интерактивно ГрапхКЛ игралиште.

 // main.rs #[macro_use] extern crate juniper; use std::io; use std::sync::Arc; use actix_web::{web, App, Error, HttpResponse, HttpServer}; use futures::future::Future; use juniper::http::graphiql::graphiql_source; use juniper::http::GraphQLRequest; mod graphql_schema; use crate::schema::{create_schema, Schema}; fn main() -> io::Result { let schema = std::sync::Arc::new(create_schema()); HttpServer::new(move || { App::new() .data(schema.clone()) .service(web::resource("/graphql").route(web::post().to_async(graphql))) .service(web::resource("/graphiql").route(web::get().to(graphiql))) }) .bind("localhost:8080")? .run() } 

You'll notice I've specified a number of imports that we will be using, including the schema we've just created. Also see that:

  • we call create_schema inside an Arc (atomically reference counted), to allow shared immutable state across threads (cooking with ? here I know)
  • we mark the closure inside HttpServer::new with move, indicating that the closure takes ownership of the inner variables, that is, it gains a copy of schema
  • schema is passed to the data method indicating that it is to be used inside the application as shared state between the two services

We must now implement the handlers for those two services. Starting with the "/graphql" route:

 // main.rs // fn main() ... fn graphql( st: web::Data
    
     , data: web::Json, ) -> impl Future { web::block(move || { let res = data.execute(&st, &()); Ok::(serde_json::to_string(&res)?) }) .map_err(Error::from) .and_then(|user| { Ok(HttpResponse::Ok() .content_type("application/json") .body(user)) }) } 
    

Our implementation of the "/graphql" route takes executes a GraphQL request against our schema from application state. It does this by creating a future from web::block and chaining handlers for success and error states.

Futures are analogous to Promises in JavaScript, which is enough to understand this code snippet. For a greater explanation of Futures in Rust, I recommend this article by Joe Jackson.

In order to test out our GraphQL schema, we'll also add a handler for "/graphiql".

 // main.rs // fn graphql() ... fn graphiql() -> HttpResponse { let html = graphiql_source("//localhost:8080/graphql"); HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(html) } 

This handler is much simpler, it merely returns the html of the GraphiQL interactive playground. We only need to specify which path is serving our GraphQL schema, which is "/graphql" in this case.

With cargo run and navigation to //localhost:8080/graphiql, we can try out the field we configured.

Упит за чланове у грапхикл

It does seem to take a little more effort than setting up a GraphQL server with Node.js and Apollo but the static typing of Rust combined with its incredible performance makes it a worthy trade — if you're willing to work at it.

Setting up Postgres for Real Data

If I stopped here, I wouldn't even be doing the examples in the docs much justice. A static list of two members that I wrote myself at dev time will not fly in this publication.

Installing Postgres and setting up your own database belongs in a different article, but I'll walk through how to install diesel, the popular Rust library for handling SQL databases.

See here to install Postgres locally on your machine. You can also use a different database like MySQL in case you are more familiar with it.

The diesel CLI will walk us through initializing our tables. Let's install it:

 cargo install diesel_cli --no-default-features --features postgres 

After that, we will add a connection URL to a .env file in our working directory:

 echo DATABASE_URL=postgres://localhost/rust_graphql_example > .env 

Once that's there, you can run:

 diesel setup # followed by diesel migration generate create_members 

Now you'll have a migrations folder in your directory. Within it, you'll have two SQL files: one up.sql for setting up your database, the other down.sql for tearing it down.

I will add the following to up.sql:

 CREATE TABLE teams ( id SERIAL PRIMARY KEY, name VARCHAR NOT NULL ); CREATE TABLE members ( id SERIAL PRIMARY KEY, name VARCHAR NOT NULL, knockouts INT NOT NULL DEFAULT 0, team_id INT NOT NULL, FOREIGN KEY (team_id) REFERENCES teams(id) ); INSERT INTO teams(id, name) VALUES (1, 'Heroes'); INSERT INTO members(name, knockouts, team_id) VALUES ('Link', 14, 1); INSERT INTO members(name, knockouts, team_id) VALUES ('Mario', 11, 1); INSERT INTO members(name, knockouts, team_id) VALUES ('Kirby', 8, 1); INSERT INTO teams(id, name) VALUES (2, 'Villains'); INSERT INTO members(name, knockouts, team_id) VALUES ('Ganondorf', 8, 2); INSERT INTO members(name, knockouts, team_id) VALUES ('Bowser', 11, 2); INSERT INTO members(name, knockouts, team_id) VALUES ('Mewtwo', 12, 2); 

And into down.sql I will add:

 DROP TABLE members; DROP TABLE teams; 

If you've written SQL in the past, these statements will make some sense. We are creating two tables, one to store teams and one to store members of those teams.

I am modeling this data based on Smash Bros if you have not yet noticed. It helps the learning stick.

Now to run the migrations:

 diesel migration run 

If you'd like to verify that the down.sql script works to destroy those tables, run: diesel migration redo.

Now the reason why I named the GraphQL schema file graphql_schema.rs instead of schema.rs, is because diesel overwrites that file in our src direction by default.

It keeps a Rust macro representation of our SQL tables in that file. It is not so important to know how exactly this table! macro works, but try not to edit this file — the ordering of the fields matters!

 // schema.rs (Generated by diesel cli) table! { members (id) { id -> Int4, name -> Varchar, knockouts -> Int4, team_id -> Int4, } } table! { teams (id) { id -> Int4, name -> Varchar, } } joinable!(members -> teams (team_id)); allow_tables_to_appear_in_same_query!( members, teams, ); 

Wiring up our Handlers with Diesel

In order to serve the data in our tables, we must first update our Member struct with the new fields:

 // graphql_schema.rs + #[derive(Queryable)] pub struct Member { pub id: i32, pub name: String, + pub knockouts: i32, + pub team_id: i32, } #[juniper::object(description = "A member of a team")] impl Member { pub fn id(&self) -> i32 { self.id } pub fn name(&self) -> &str { self.name.as_str() } + pub fn knockouts(&self) -> i32 { + self.knockouts + } + pub fn team_id(&self) -> i32 { + self.team_id + } } 

Note that we are also adding the Queryable derived attribute to Member. This tells Diesel everything it needs to know in order to query the right table in Postgres.

Additionally, add a Team struct:

 // graphql_schema.rs #[derive(Queryable)] pub struct Team { pub id: i32, pub name: String, } #[juniper::object(description = "A team of members")] impl Team { pub fn id(&self) -> i32 { self.id } pub fn name(&self) -> &str { self.name.as_str() } pub fn members(&self) -> Vec { vec![] } } 

In a short while, we will update the members function on Team to return a database query. But first, let us add a root call for members.

 // graphql_schema.rs + extern crate dotenv; + use std::env; + use diesel::pg::PgConnection; + use diesel::prelude::*; + use dotenv::dotenv; use juniper::{EmptyMutation, RootNode}; + use crate::schema::members; pub struct QueryRoot; + fn establish_connection() -> PgConnection { + dotenv().ok(); + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + PgConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url)) + } #[juniper::object] impl QueryRoot { fn members() -> Vec { - vec![ - Member { - id: 1, - name: "Link".to_owned(), - }, - Member { - id: 2, - name: "Mario".to_owned(), - } - ] + use crate::schema::members::dsl::*; + let connection = establish_connection(); + members + .limit(100) + .load::(&connection) + .expect("Error loading members") } } 

Very good, we have our first usage of a diesel query. After initializing a connection, we use the members dsl, which is generated from our table! macros in schema.rs, and call load, indicating that we wish to load Member objects.

Establishing a connection means connecting to our local Postgres database by using the env variable we declared earlier.

Assuming that was all input correctly, restart the server with cargo run, open GraphiQL and issue the members query, perhaps adding the two new fields.

The teams query will be very similar — the difference being we must also add a part of the query to the members function on the Team struct in order to resolve the relationship between GraphQL types.

 // graphql_schema.rs #[juniper::object] impl QueryRoot { fn members() -> Vec { use crate::schema::members::dsl::*; let connection = establish_connection(); members .limit(100) .load::(&connection) .expect("Error loading members") } + fn teams() -> Vec { + use crate::schema::teams::dsl::*; + let connection = establish_connection(); + teams + .limit(10) + .load::(&connection) + .expect("Error loading teams") + } } // ... #[juniper::object(description = "A team of members")] impl Team { pub fn id(&self) -> i32 { self.id } pub fn name(&self) -> &str { self.name.as_str() } pub fn members(&self) -> Vec { - vec![] + use crate::schema::members::dsl::*; + let connection = establish_connection(); + members + .filter(team_id.eq(self.id)) + .limit(100) + .load::(&connection) + .expect("Error loading members") } } 

When running this is GraphiQL, we get:

Сложенији упит у грапхикл-у

I really like the way this is turning out, but there is one more thing we must add in order to call this tutorial complete.

The Create Member Mutation

What good is a server if it is read-only and not writable? Well I'm sure those have their uses too, but we would like to write data to our database, how hard can it be?

First we'll create a MutationRoot struct that will eventually replace our usage of EmptyMutation. Then we will add the diesel insertion query.

 // graphql_schema.rs // ... pub struct MutationRoot; #[juniper::object] impl MutationRoot { fn create_member(data: NewMember) -> Member { let connection = establish_connection(); diesel::insert_into(members::table) .values(&data) .get_result(&connection) .expect("Error saving new post") } } #[derive(juniper::GraphQLInputObject, Insertable)] #[table_name = "members"] pub struct NewMember { pub name: String, pub knockouts: i32, pub team_id: i32, } 

As GraphQL mutations typically go, we define an input object called NewMember and make it the argument of the create_member function. Inside this function, we establish a connection and call the insert query on the members table, passing the entire input object.

It is super convenient that Rust allows us to use the same structs for GraphQL input objects as well as Diesel insertable objects.

Let me make this a little more clear, for the NewMember struct:

  • we derive juniper::GraphQLInputObject in order to create a input object for our GraphQL schema
  • we derive Insertable in order to let Diesel know that this struct is valid input for an insertion SQL statement
  • we add the table_name attribute so that Diesel knows which table to insert it in

There is a lot of magic going on here. This is what I love about Rust, it has great performance but the code has features like macros and derived traits to abstract away boilerplate and add functionality.

Finally, at the bottom of the file, add the MutationRoot to the schema:

 // graphql_schema.rs pub type Schema = RootNode; pub fn create_schema() -> Schema { Schema::new(QueryRoot {}, MutationRoot {}) } 

I hope that everything is there, we can test out all of our queries and mutations thus far now:

 # GraphiQL mutation CreateMemberMutation($data: NewMember!) { createMember(data: $data) { id name knockouts teamId } } # example query variables # { # "data": { # "name": "Samus", # "knockouts": 19, # "teamId": 1 # } # } 

If that mutation ran successfully, you can pop open a bottle of champagne as you are on your way to building performant and type-safe GraphQL Servers with Rust.

Thanks For Reading

I hope you have enjoyed this article, I also hope that it gave you some sort of inspiration for your own work.

Ако желите да наставите са следећим путом када испустим чланак из царства Руст, РеасонМЛ, ГрапхКЛ или развоја софтвера уопште, слободно ме пратите на Твиттеру, дев.то или на мојој веб локацији на ианвилсон.ио.

Изворни код је овде гитхуб.цом/ивилсонк/руст-грапхкл-екампле.

Остали уредни материјали за читање

Ево неких библиотека са којима смо овде радили. Имају сјајну документацију и водиче, па их обавезно прочитајте :)

  • Примена Руст Футурес у Токију
  • Јунипер - ГрапхКЛ сервер за рђу
  • Дизел - сигуран, проширив ОРМ и градитељ упита за рђу
  • Ацтик - Рустов моћан систем глумаца и најзабавнији веб оквир