Joskus on kannattavaa rajata käyttöliittymä ja taustajärjestelmä erilleen (front ja back). Käyttöliittymässä on logiikka tiedon näyttämiseen, ja se on integroitu taustajärjestelmään rajapinnan (API) kautta. Käyttöliittymä siis kysyy informaatiota rajapinnasta ja saa vastaukseksi esimerkiksi listan asiakkaan tilauksista. Taustajärjestelmä ei muodosta käyttöliittymää, vaan on sen tukena.  

Tällä menetelmällä saavutetaan separation of concerns (SoC), eli käyttöliittymän tehtävä on näyttää informaatiota ja välittää sitä rajapinnalle, kun taas taustajärjestelmän tehtävä on tarjota, vastaanottaa, käsitellä ja tallentaa informaatiota. Sama rajapintaa voi käyttää useampi käyttöliittymä, esimerkiksi webin lisäksi myös mobiili tai desktop. 

REST-tyyli on yksinkertainen 

Jatkossa en tarkoita rajapinnoilla ohjelman sisäisiä rajapintoja (kuten luokan rajapinta) vaan ohjelmien välisiä rajapintoja. Yleisin tapa rakentaa rajapinta nykyään lienee seurata RESTtyyliä, ja RPC  tulee heti kakkosena 

Kieltämättä REST on hyvä ja yksinkertainen tyyli rakentaa rajapintoja palvelinpuolen logiikkaan, mutta puheliaisuutensa vuoksi se ei välttämättä aina ole paras vaihtoehto moderneille käyttöliittymille. Kuvitellaan websovellus, missä käsitellään asiakkaita, asiakkaiden tilauksia ja tilauksien tuotteita. Asiakaspalvelun käyttöliittymässä on näkymä, jossa listataan asiakkaat sekä näytetään yhteenveto asiakkaan tilauksista ja tilauksien tuotteista. Rajapinta on rakennettu RESTtyylillä, eli tiedot saadaan neljällä kyselyllä. 

/customers 

/customers/:customerid/orders 

/customers/:customerid/orders/:orderid/products 

Tiedon koostaminen on hankalaa REST-rajapinnasta 

Mutta entä jos asiakkaita on keskimäärin 10 ja asiakkaan tilauksia on keskimäärin 15? Kyselyjä tehdään silloin 161. 

/customers 1 kysely 

/customers/:customerid/orders 1 * 10 = 10 kyselyä 

/customers/:customerid/orders/:orderid/products 1 * 10 * 15 = 150 kyselyä 


Kutsujen korkea lukumäärä ei välttämättä ole ongelma, mutta sen lisäksi tässäkin tapauksessa hitaan verkon yli mennään edestakaisin 3 kertaa. Ensin haetaan asiakkaat, sitten asiakkaiden tilaukset ja lopuksi tilausten tuotteet. Hidas tai muuten kuormittunut verkko voi tehdä sovelluksen käyttökokemuksesta kammottavan. Usein tämänlaisia tilanteita varten rikotaan RESTtyyliä ja tehdään ad hoc endpointteja. 

/customers_orders_and_products 

Ongelmaksi koituu nyt, että ylimääräistä dataa on valtava määrä siihen nähden, että tilauksista ja tuotteista tarvittiin vain yhteenveto. Ratkaisuksi tehdään taas uusi endpoint. 

/customers_order_summary_and_product_summary 

Lopputuloksena meillä on REST-rajapinta, joka säikäyttää osan kehittäjistäkin. Jokaisen endpointin kohdalla on n määrä eri vastauskoodeja ja skeemoja. 

/customers GET 

/customers POST 

/customers/:customerid GET 

/customers/:customerid POST 

/customers/:customerid PUT 

/customers/:customerid PATCH 

/customers/:customerid DELETE 

/customers/:customerid/orders GET 

/customers/:customerid/orders POST 

/customers/:customerid/orders/:orderid GET 

/customers/:customerid/orders/:orderid POST 

/customers/:customerid/orders/:orderid PUT 
/customers/:customerid/orders/:orderid PATCH 

/customers/:customerid/orders/:orderid DELETE 

/customers/:customerid/orders/:orderid/products GET 

/customers/:customerid/orders/:orderid/products POST 

/customers/:customerid/orders/:orderid/products/:productid GET 

/customers/:customerid/orders/:orderid/products/:productid POST 

/customers/:customerid/orders/:orderid/products/:productid PUT 

/customers/:customerid/orders/:orderid/products/:productid PATCH 

/customers/:customerid/orders/:orderid/products/:productid DELETE 

/customers_orders_and_products GET 

/customers_order_summary_and_product_summary 

Ja vaikka rajapinta tarjoaa jo aivan valtava määrä toiminnallisuutta, niin useimpiin käyttötarkoituksiin kannattaa silti tehdä uusi ja optimoitu ad hoc endpoint, mitä pitää muuttaa vaatimuksien muuttuessa. Aikanaan ad hoc endpointteja voi olla enemmän kuin REST endpointtejaMiksi seurata REST-tyyliä, jos sitä kannattaa rikkoa? 

GraphQL on luonnostaan koostava 

Rajapintojen käytön ja kehityksen haasteeksi on syntynyt tiedon koostamiseen kuluva työ, jota varten voi joutua suunnittelemaan bulk/batch endpointteja. Suuri osa ohjelmistotaloista pitää tätä kuitenkin stantardina ja jatkaa REST-rajapintojen tarjoamista käyttöliittymien tarpeisiin. Onneksi kansainväliset yritykset, kuten Facebook, Airbnb, Paypal ja Twitter, ovat mukana GraphQL Foundationissa ja tukevat tapaa luoda koostavia rajapintoja. Kuvitellaan seuraavanlaiset resurssityypit: 

type OrderProduct { 

  productId: ID 

  nameString 

  … muita tietoja 

} 

 

type Order { 

  orderId: ID 

  “Tilauksen tuotteet” 

  products: [OrderProduct] 

  … muita tietoja 


 

type Customer { 

  customerId: ID 

  nameString 

  “Asiakkaan tilaukset” 

  orders: [Order] 

  … muita tietoja 

}
 

type Query { 

  “Asiakaslista” 

  customers: [Customer] 
} 

GraphQLrajapinnat eivät perustu urleihin ja verbeihin vaan skeemaan ja kyselyihin. Kyselyiden rakentamiseen hyödynnetään skeemaa, joka kuvaa tarjolla olevat kyselyt ja palautetun datan. Tämän skeeman perusteella voi yhdellä kyselyllä saada kaiken tarvittavan informaation. 

query haeAsiakkaanTilauksetJaTuotteet { 

  customers { 

    customerId 

    name 

    orders { 

      orderId 

      products { 

        productId 

        name 

      } 

    } 

  } 

} 

Yhdellä kyselyllä saadaan:  

  • lista asiakkaista 
  • jokaisesta asiakkaasta on asiakkaan id, nimi ja lista tilauksista 
  • jokaisesta tilauksesta on tilauksen id ja lista tilaustuotteista 
  • jokaisesta tilaustuotteesta on tuotteen id ja nimi 

Kyselyyn voi myös lisätä tietoa tai sitä voidaan poistaa. Lisäksi skeemaa voi rikastuttaa ja pienellä vaivalla saadaan lisää ominaisuuksia, kuten suoraan asiakkaan tai tilauslistan hakeminen sekä asiakkaan hakeminen tilaukselle: 

extend type Query { 

  “Tietty asiakas” 

  customer(customerIdInt!): Customer 

  “Tilauslista” 

  orders: [Order] 

} 

 

extend type Order { 

  “Tilauksen asiakas” 

  customerCustomer 

} 

Querytyypin ja customtyyppien laajennuksia voidaan tehdä, kunnes voidaan hakea mikä tahansa tietty resurssi, lista mistä tahansa resursseista ja voidaan kulkea asiakkaasta tuotteeseen tai tuotteesta asiakkaaseen. Entä kun tiedon hakemisen sijaan halutaan luoda/muuttaa/poistaa tietoa? 

type Mutation { 

  “Luo asiakkaita” 

  createCustomers(customerInputs: [CustomerInput]!): [Customer] 

Päivitä asiakkaita 

  updateCustomers(customerInputs: [CustomerInput]!): [Customer] 

  “Poista asiakkaita, palauttaa poistettujen asiakkaiden Id:t 

  deleteCustomers(customerIds: [Int]!): [Int] 

} 

 

input CustomerInput { 

  customerIdInt 

  … muita tietoja 

} 

Mutaatioita lisätään tarvituille luonti, muokkaus ja poistooperaatioille. Ne voivat koskea yksittäistä resurssitai kokoelmaaEntä jos tuotekohtaisten tietojen sijaan tarvitaan vain jokin aggregaatio tuotetiedoista? 

extend type Order { 

  productCountInt 

} 

Nyt koko tuotelistan sijaan voidaan pyytää pelkkä tilausten lukumäärä. “Fieldin (eli tyypissä listatun rivin) productCount lisääminen on vapaavalintaista, eli jos sitä ei tarvitarajapinnan takana olevan logiikan ei tarvitse hakea ja laskea tuotteisiin liittyviä tietoja. Tällä tavalla saadaan monipuolisempi, joustavampi ja usein suorituskykyisempi rajapinta. 

GraphQL:n suorituskykyongelmat 

Jos suorituskykyä tarvitsee optimoida, tiedon koostamisessa täytyy huomioida tiettyjä asioita. Vaikka GraphQLrajapinnalle tehdään vain yksi kysely ja sieltä saadaan kerralla kaikki, niin taustajärjestelmässä kyselyiden määrä ei ole vähentynyt. Palataan esimerkkinä AsiakkaanTilauksetJaTuotteetkyselyyn. Jos esimerkin GraphQLskeeman takana oleva logiikka hakee kaiken samassa sisäverkossa sijaitsevan REST-rajapinnan kautta, niin se hakee ensin asiakkaat, sitten jokaisen asiakkaan kohdalla tilaukset ja lopuksi jokaisen tilauksen kohdalla tuotteet. Tässä on kaksi huomioitavaa asiaa: 

  1. N+1 määrä kyselyjä eli jokaista yhtä kyselyä kohden tehdään N määrä lisää kyselyjä. 
  2. Duplikaattikyselyt. Tilauksissa voi olla samoja tuotteita, mutta jokainen haetaan silti erikseen. 

Nämä asiat vaativat vain harvoin toimenpiteitä, koska kyselyt eivät joudu kulkemaan hitaan verkon yli, vaan ne kulkevat usein jopa saman palvelimen sisällä. Tarpeeksi isolla skaalalla voidaan kuitenkin havaita mitattava suorituskykyhaitta, joka pitää ratkaista. 

Duplikaattikyselyiden osalta suurin osa GraphQLkirjastoista soveltaa deduplikoimista. Esimerkiksi NodeJS Apollo GraphQL Server REST datasource muistaa, mistä dataa on haettu, ja jos sama kysely yritetään suorittaa toista kertaa, vastaus otetaan välimuistista. 

N+1ongelmaa varten usein sovelletaan dataloadermallia. Esimerkiksi jos asiakkaan tilaukset haetaankin dataloaderin avulla, yksittäisten kyselyiden sijaan tarvittavat kyselytiedot tallennetaan listaan ja koko lista haetaan kerralla. 

Yksinkertainen vai koostava rajapinta? 

Vaikka käyttöliittymän tueksi tehtäisiin koostava rajapinta, usein resursseille silti tehdään yksinkertaiset rajapinnatTyötä voi siis olla enemmän kuin pelkän yksinkertaisen rajapinnan tekemisessä. Mahdolliset säästöt syntyvät rajapintojen yksinkertaistuessa, suorituskyvyn kasvaessa ja kehitystahdin parantuessa. 

GraphQLskeemassa on introspektioeli työkalut voivat ladata skeeman ja avustaa rajapinnan käytössä. Tämä mahdollistaa mm. automaattisen dokumentaation, kyselyn automaattisen täydentämisen ja kirjoitusvirheistä varoittamisen. 

Koostavan ja skeemapohjaisen rajapinnan käyttö voi olla helpompaa. Suorituskykyongelmat voivat myös vähentyä huomattavasti, koska usein yksi matka palvelimelle ja takaisin riittää. APIkehitystä tarvitsee myös joskus tehdä vähemmän, koska yhden skeeman saa palvelemaan paljon monipuolisemmin erilaisia tarpeita, kun taas REST-rajapintaa pitää usein muuttaa vaatimuksien muuttuessa. 

Tiedon koostaminen on tehtävä joka tapauksessa jossakin. Tehdäänkö se mieluummin käyttötapauskohtaisesti käyttäjän luona vai keskitetysti palvelimella?