Byg en CRUD API med Rust

Siden min første Node / Rust REST-sammenligning har jeg ønsket at følge op med en omfattende guide til at få enkle CRUD-operationer op og køre i Rust.

Mens Rust har sine kompleksiteter, der kommer fra Node.js, er en ting, der har holdt mig tiltrukket af sproget, dets enkelhed. Det er slankt og får en stor balance mellem sikkerhed og kognitiv intuition.

Under alle omstændigheder hjælper denne guide forhåbentlig dem, der er nye for Rust, og opmuntrer dem, der er på hegnet.

Rul ned, hvis du gerne vil se, hvordan Rust sammenligner sig med Java og Node.js

Brugte større rammer

  • Rocket - webramme til skrivning af hurtige webapplikationer
  • Serde - ramme for serialisering og deserialisering af Rustdatastrukturer
  • Dieselsikker, udvidelig ORM- og forespørgselsbygger

Opretter vores applikation i Rust

Først opretter vi et nyt Rust-projekt ved hjælp af kommandoerne nedenfor.

[sean @ lappy486: ~] $ fragt ny hero-api - bin && cd hero-api
    Oprettet binært (applikation) `hero-api`-projekt

Inden vi imidlertid går fremad, kræver Rocket, at vi bruger den natlige rustbygning. Heldigvis er der en hurtig kommando, vi kan bruge til at skifte kanal.

$ rustup standard om natten
$ rustup-opdatering && fragtopdatering

Du får noget output her angående omskifteren, men kan bekræfte, at det var vellykket ved at påkalde - omstillingsflagget på last og rustc

$ fragt - modstand && rustc - modstand
fragt 1.26.0-natlig (5f83bb404 2018-03-09)
rustc 1.26.0-nightly (55c984ee5 2018-03-16)

Tilføjelse af vores første rustafhængighed (raket)

Okay, i vores hero-api-bibliotek finder du en Cargo.toml- og src-mappe. Cargo.toml fungerer som Nodes pakke.json, som vi vil dække senere - og jeg formoder, at src er beskrivende nok :)

Vi bliver nødt til at tilføje Rocket som en afhængighed ved at redigere Cargo.toml og tilføje de følgende to afhængigheder. Der er bestemt mere at tilføje senere, men det vil i det mindste komme os i gang.

[afhængigheder]
raket = "0.3.6"
rocket_codegen = "0.3.6"

Nu kan vi redigere src / main.rs - vi vil bare låne den startkode, som Rocket.rs har leveret til os for at sikre, at alt fungerer på dette tidspunkt.

Det, jeg synes mest tiltalende om Rust (og raket), er, hvor selvbeskrivende størstedelen af ​​koden er.

  • # [Get ( "/ / ")]

Her udsætter vi et enkelt slutpunkt, der udnytter Rockets annotation, der tager to parametre og . Vi kortlægger derefter disse parametre til vores rutehåndterer og specificerer deres typer - hvis typerne er uovertruffen, vil rutehåndtereren ikke blive påkaldt, og en 404 returneres.

  • fn hej (navn: streng, alder: u8) -> streng {

Vores rutehåndterer returnerer en formateret streng, vha. Rusts format! makro, med det angivne navn og alder. Bemærk intet efterfølgende semikolon for returneringsangivelser

  • format! ("Hej, {} år gammel ved navn {}!", alder, navn)

Den vigtigste metode, der ligner andre sprog, kaldes passende main. Dette er den første funktion, der påberåbes, når vores applikation kører. Her beder vi Rocket om at starte og montere vores rutehandler ved hjælp af / hej som rodkontekst. Dette giver os en URL som: / hej / / .

fn main () {
  raket :: antændes ()
    .montering ("/ hej", ruter! [hej])
    .launch ();
}

Dine to filer skal se ud som billedet herunder. Lad os teste, at alt fungerer ved at starte vores server ved hjælp af kommandoen til brug af cargo. Dette skal starte en server på localhost: 8000, som kan ramt via browser / krøll.

Opbygning af afslappende slutpunkter vha. JSON (Serde)

Så et slutpunkt får os kun så langt. Når vi tænker på vores API, ønsker vi enkle CRUD-operationer til at styre Heldata; så lige fra toppen, ønsker vi:

  • Opret: POST / helt
  • Læs: FÅ / helte
  • Opdatering: PUT / helt /: id
  • Slet: SLET / helt /: id

Nu hvor vi har det ude af vejen - bruger vi JSON som vores primære middel til udveksling af data, så ind kommer vores næste afhængighed: Serde.

Dette kræver følgende tilføjelser til vores Cargo.toml:

Vi vil også tænke over, hvilke egenskaber en given helt skal have. Nedenfor opretter vi en enkel Hero-model inden for src / hero.rs - tilføjelse af Serialize og Deserialize-kommentarer fra Serde, så vores model kan udvindes fra og konverteres til JSON.

id er valgfrit, da forbrugere, der rammer vores CREATE-endpoint, endnu ikke har et id at sende. Men når vi henter data og leverer dem som et svar, vil noget fra vores DB have en id oprettet.

Lad os gå videre og oprette vores CRUD-slutpunkter nu ved hjælp af dummy-data for tiden:

I det ovenstående inkluderer vi vores nye afhængigheder og refererer til vores hero-type, vi trækker også Json og Value ud fra rocket_contrib; dette vil gøre det lettere at håndtere JSON-anmodninger / svar.

# [macro_use] ekstern kasse rocket_contrib;
# [macro_use] ekstern kasse serde_derive;
Brug rocket_contrib :: {Json, Value};
mod helten;
Brug helt :: {Hero};

Vi tilføjer også vores andre operationer (POST, PUT, DELETE). Dataattributten i vores ruteanotation fortæller blot, at Rocket skal forvente Body Data - derefter kortlægge kroppen til en parameter. Her siger vi, at det forventede legeme skal være i form af en helt, men indpakket i JSON.

# [post ("/", data = "")]
fn create (helt: Json ) -> Json  {

Vi skulle nu være i stand til at ramme nogen af ​​vores konfigurerede slutpunkter ved hjælp af en standard hvileklient / krølle osv.

Vedvarende på vores data via ORM (Diesel)

Jeg tror, ​​vi alle kan være enige, at det er godt at have et par webadresser, men det er ikke alt for nyttige, hvis de data, vi sender, ikke er vedvarende. Til dette bruger vi Diesel, da det i øjeblikket er et af de mest modne Rust ORM-rammer.

At gøre dette for første gang, må jeg indrømme, det er bestemt en involveret proces, men når den første bootstrap er ude af vejen, fungerer det ganske godt. Jeg har prøvet dette med den røde mysql-driver med sorte stråler, og mens den fungerer, bliver codebasen forurenet virkelig hurtig ..

Faktisk skrev Sean Griffin (forfatter af Diesel) en fantastisk artikel, der illustrerer netop dette punkt.

For at komme i gang installerer vi Diesel CLI:

$ fragt installere diesel_cli

Så fortæller vi Diesel, hvor vores database skal bo og køre opsætning:

$ eksport DATABASE_URL = mysql: // bruger: pass @ localhost / helte
$ dieselopsætning
    Oprettelse af database: helte

Derefter genererer vi en migrationsstrategi - dette giver os grundlæggende mulighed for at holde revisioner, når vores database udvikler sig over tid.

$ Dieselmigration genererer helte
    Oprettelse af migrationer / 2018-03-17-180012_heroes / up.sql
    Oprettelse af migrationer / 2018-03-17-180012_heroes / down.sql

Vi bliver nødt til at redigere disse to filer for at inkludere SQL til vores Heroes-skema

Nu kan vi køre vores migration - som vil udføre up.sql mod vores DB

[sean @ lappy486: ~ / hero-api] $ diesel migration run
    Kører migration 2018-03-17-180012_heroes

Med held og lykke er det den sidste SQL, vi bør røre ved dette projekt. Lad os gå videre og tilføje Diesel til vores afhængigheder i Cargo.toml. Vi vil også tilføje r2d2-diesel, som gør det muligt for os at styre db-forbindelsespooling.

[afhængigheder]
diesel = {version = "1.0.0", features = ["mysql"]}
diesel_codegen = {version = "*", funktioner = ["mysql"]}
r2d2 = "*"
r2d2-diesel = "*"

Vi opretter src / db.rs for at etablere vores databaseforbindelse og styre vores pool - heldigvis blev det meste af dette leveret af Rocket Connection Guard startkode

Vi bliver også nødt til at tilføje følgende tilføjelser for at binde vores Hero-model med den nye tabelinformation, vi har oprettet:

Dette gør det muligt for os at oprette et auto-genereret skema afledt af vores struktur

$ diesel-print-skema> src / schema.rs

Vi vil dog foretage en lille ændring, så vi kan udnytte den samme model til objekt, der kan forespørges og indsættes. Diesel-forfatteren udtrykte rimelig begrundelse for at opdele vores objekt i to modeller, men jeg foretrækker bekvemmeligheden og mindre kodeforurening. Så for det redigerer vi src / schema.rs

Uden ændringen ovenfor kræver vi to modeller: en til at indsætte (uden id) og en separat til at hente (med id).

Nu kan vi importere disse nye afhængigheder ved at tilføje følgende intosrc / main.rs:

# [makro_brug] ekstern kassediesel;
udvendig kasse r2d2;
ekstern kasse r2d2_diesel;
mod db;
mod skema;

Dernæst beder vi Rocket om at administrere vores db-forbindelsespool ved at tilføje følgende til vores raket :: ignite () -kæde:

.manage (db :: connect ())

Tro det ikke, nu er vi klar til at få denne ting i gang! Her afslører vi et par metoder ved at oprette en implementering til Hero inden for src / hero.rs - spar til mere sofistikeret fejlhåndtering.

Nu kan vi udnytte de nyligt oprettede metoder inden for vores rutehandlere

Bemærk, at hver metode inden for vores rutehandlere og Hero-implementering accepterer nu i det væsentlige de samme / lignende argumenter: En eller anden kombination af en id, Hero-objekt og databaseforbindelse.

For mere information om, hvordan du spørger om data, henvises til Diesel Getting Started-guiderne.

Endelig kan vi nu oprette en helt og se virkelige resultater i vores REST-klient. Hentning, opdatering og sletning af helte fungerer også som forventet.

Sæt vores API på prøve (wrk)

Nu hvor vi har et noget komplet API, er det sandsynligvis værd at benchmarke denne ting, da et af de mest spionerede aspekter af Rust er dens ydeevne på andre sprog.

Men inden jeg prøvede det, ønskede jeg at få en idé om, hvordan andre populære sprog og rammer stables op.

Til dette brugte jeg simpelthen wrk lokalt på min Macbook Pro, uden at andre brugerprogrammer eller tjenester kørte.

MacBook Pro (15-tommer, 2016)
Processor: 2,9 GHz Intel Intel Core i7
Hukommelse: 16 GB 2133 MHz LPDDR3
Opbevaring: 1 TB Flash-opbevaring

Java 1.8 (Spring Boot, Dvaletilstand)

Jeg sammensatte et hurtigt eksempel, hvor jeg udsatte den samme GET Heroes-metode, som vi oprettede i vores Rust API ovenfor ved at følge en god tutorial af Rajeev Kumar Singh (findes her)

Den anvendte eksempelkode kan findes her:

  • https://github.com/sean3z/spring-boot-hero-api-example

Resultaterne var ikke dårlige @ 4.584 anmodninger pr. Sekund

Knude 9.8 (Restify, Sequelize)

Node-teamet har foretaget betydelige forbedringer i knudepunkt 9. Nu udnytter V8s tænding og Turbofan - Nodes ydeevne er næsten tredoblet.

Den anvendte eksempelkode kan findes her:

  • https://github.com/sean3z/nodejs-hero-api-example

Resultaterne var ikke næsten det, jeg håbede @ 2.506 anmodninger pr. Sekund. Det er værd at bemærke, at brug af den rå mysql-pakke næsten fordoblet de håndterede anmodninger pr. Sekund.

Rust 1,26 (raket, diesel)

Til slut, for at teste vores Rust-server, ønsker vi at sikre, at ROCKET_ENV er indstillet til at prod, ellers kører den i udviklingsfunktion.

$ fragtopbygning - udgiv & mål cd / frigivelse /
$ sudo ROCKET_ENV = prod ./hero-api
     Rocket er lanceret fra http://0.0.0.0:80

Den anvendte eksempelkode kan findes her:

  • https://github.com/sean3z/rocket-diesel-rest-api-example

Rust overgik alvorligt begge de andre rammer

Konklusion

Noden leverede det mindste kodefodaftryk, men åbenlyst uden typesikkerhed og i den nedre ende af præstationsskalaen - skønt 2k req / sec ikke er noget at forhindre. Vi kan forbedre Nodes ydelse til prisen for vedligeholdelse af nastier-kode.

Java havde langt det største kodefodaftryk. Det tager også længst at starte med et gennemsnit på ~ 4 sekunder. Ikke rigtig meget andet at rapportere her, det var bare ikke .. sjovt at udvikle sig?

Rust er et utroligt sjovt sprog at bruge, og det er flammende hurtigt. Det siger sig selv, hastighed er ikke den eneste faktor, når man overvejer det rigtige værktøj til jobbet, men det er bestemt et vigtigt.

For at lære mere om Rust skal du tjekke “Rustprogrammeringssprog”