Ykkösen jälkeen tulee kakkonen, ja kakkosen jälkeen tulee kolmonen
Tähän mennessä jokaisessa palautuksessa on ollut mukana jokin numeerinen id. Useimmiten se on tietokantaan määritelty erityinen kenttä, joka kasvattaa juoksevaa numerointia yhdellä aina kun uusi rivi tallennetaan. Jokainen resurssi löytyy siis kaavalla edellisen resurssin id + 1. Kehittäjän kannalta tämä on kätevää. Tietysti myös käyttäjän on näin helppoa arvata, että millä endpointilla saa seuraavan resurssin. Useimmiten sillä ei liene mitään merkitystä ja vastaesimerkkiä on vaikea keksiä, mutta koitetaan. Jospa meillä on vaikka sosiaalisen median rajapinta, jossa voidaan kysyä käyttäjän postauksia ja noista postauksista osa on meille avoimia ja osa ei. Saamme linkin yhteen postaukseen /users/1/posts/1. Oletetaan, että postaus tunnisteella 1 on meille tarkoitettu, mutta postaus tunnisteella 2 ei. Oletetaan lisäksi, että oikeuksia ei tarkisteta missään vaiheessa, joten voimme pyytää postauksen kunhan tiedämme sen tunnisteen. Näin ollen saamme suoraan sen postauksen, jota ei ollut meille tarkoitettu, kokeilemalla vain numeroita eteenpäin.
Tietysti tässä välissä meidän oikeutemme hakea kyseinen resurssi tarkistettaisiin, mutta jotkut voivat silti olla sitä mieltä, että käyttäjälle ei tulisi tarjoilla tunnistetietoa juoksevana numerointina. Tällöin voitaisiin laskea jokaiselle resurssille UUID, yksilöllinen tunniste, joka ei ole juokseva numero.
Verkkosivuja tehtäessä postauksien kohdalla haluamme kenties lyhennelmän, slugin, otsikosta. Emme niinkään tunnistetta salataksemme, kuin miellyttävämpiä osoitteita luodaksemme. Eli vaikkapa /posts/what-i-learned-on-jefferson-street/. Rajapinnan puolella slugin kautta kutsuminen ei liene niin mielekästä, vaan voisimme kenties käyttää UUID:ta, kuten /posts/vgg866x/.
Haluatko todella kaikki rivit?
Places::all() saattoi vaikuttaa hienolta ajatukselta, kun kannassa oli vasta testidatana muutama sata riviä. Muutamalla tuhannellakin rivillä se on vielä ihan käytettävissä. Mutta mitä sitten, kun rivejä on satoja tuhansia ja miljoonia? Ensinnäkin tiedon kaivaminen kannasta vie palvelimen resursseja (turhaan) ja sen siirtäminen käyttäjän iPhoneen vie datakaistaa (turhaan) ja se varmaankin tulee kaatamaan kehnosti tehdyn iPhone -applikation (so-so).
Eli haluamme varmaankin tarjota käyttäjälle mahdollisuuden rajata tulosten määrää, sekä estää käyttäjää hakemasta liian montaa tulosta kerralla.
Voimme jakaa kokoelman osiin limit ja offset -määreillä kyselyssä. Voimme lisäksi upottaa vastauksen metadataan suorat linkit seuraavaan ja/tai edelliseen resurssisettiin. Eli toteuttaa sivutus.
Verkkosivuilla sivutuksien yhteydessä on usein painikkeet numeroiduille sivuille. Tällaisten tarjoaminen vaatii kuitenkin sen, että tiedämme kuinka monta riviä kyseinen kysely tuottaa. Kymmenillä tuhansilla riveillä tämä ei ole vielä suuri ongelma, mutta SELECT COUNT(*) on kallis operaatio ja muuttuu sitä kalliimmaksi, mitä enemmän rivejä on kannassa.
Lisäksi rajapinnassa rivien kokonaismäärä saattaa muuttua kutsujen välillä. Yht'äkkiä rivi, joka ensikyselyllä sijoittuisi sivulle 1, sijoittuukin seuraavaa sivua haettaessa sivulle 2, koska rivien määrä on muuttunut välissä. Jos tiedot esitetään uusin ensin ja uusia rivejä tulee nopeaan tahtiin, niin edellisille sivuille meneminen toisi aina niitä samoja vanhoja aiemman sivun asioita uudelleen.
Ja toisaalta, onko sivutiedoilla merkitystä? Kuka katsoo Googlen haun tulosten määrää ja miettii, että 8 miljoonaa tulosta on liian vähän, haluaisin avata useampia sivuja aiheesta? Tai kuka säännöllisesti lukee hakutuloksista mitään sivua yli kahdeksan?
Jos emme laske rivien kokonaismäärää, saatamme tarjota linkin seuraavalle sivulle, jolla ei sitten olekaan mitään sisältöä. Onko se sitten todella niin paha asia, että sen vuoksi mielummin käyttäisi hitaammin toimivaa ohjelmaa?
Jos nyt joku haluaisi vaikkapa kaataa rajapintamme liian monilla pyynnöillä, se varmaankin onnistuu helpommin silloin, kun yksikin pyyntö tekee ihan julmetusti töitä.
Monta kutsua vs. isoja kutsuja
Tässä kohdassa on hyvä sanoa heti alkuun, että seuraavat asiat, jos eivät edelliset, ovat tilannekohtaisia.
Ajatellaan vaikka, että meillä on rajapinta pizzeria-hakukoneeseen. Meillä on siis ravintoloita, joilla on tietoja, kuten sijainti ja myös arvosteluja, jotka tulevat käyttäjiltä, joilla on tietoja, kuten käyttäjätunnus ja profiilikuva.
Sitten teemme iPhone -applikaation, joka näyttää kartalla 50 lähintä ravintolaa. Ongelma on nyt siinä, että haluammeko hakea ensin vain 50 ravintolaa ja sitten kunkin ravintolan arvostelut ja sitten kunkin arvostelijan profiilikuvan, vai haluammeko hakea nuo kaikki kerralla? Jos haemme kaikki kerralla ja käyttäjä haluaa vain katsoa lähimmän suosikkipizzeriansa saamat arvostelut, haimme mahdollisesti todella ison kasan dataa aivan turhaan ja käyttö oli hidasta.
Jos taas haemme kaikki tiedot yksitellen ja käyttäjä haluaa vertailla kaikkia ravintoloita keskenään, joudumme tekemään todella monta HTTP -kutsua. Ja todennäköisesti useimmat niistä aivan turhaan, jos vaikka sama käyttäjä on arvioinut useita ravintoloita. Ellemme sitten rakenna paikallista mallia käyttäjistä ja tarkista aina sen kautta, onko käyttäjän tietoja jo haettu ja päivitä mallia vasta sitten uuden käyttäjän tiedoilla. Mahdollisuuksia on, mutta mikä on se rooli, joka rajapinnan pitäisi tässä ottaa? Tarpeeksi tietoa, vai nopeasti tietoa?
Tietysti asia on ratkaistavissa tarjoamalla kaikille kaikkea, mutta kaikille kaikkea voi todella olla kaikille kaikkea. Jossain kohtaa rönsyt on leikattava jo pelkän ylläpidettävyyden vuoksi. Usein kaikille kaikkea onkin todellisuudessa ei kenellekään mitään.
Älä tee rajapintaa, jota ei voi käyttää
Tällä viikolla minun piti käyttää suuren suomalaisen ohjelmistotalon rajapintaa laittaakseni mittalaitteiden dataa kartalle. Ihan helppo homma, paitsi että rajapintaan ei päässyt käsiksi muutoin, kuin palveluntarjoajan oman portaalin kautta.
Jos haluamme käyttää rajapinnan tietoja JavaScript -ohjelmassa, kuten vaikka laittaa merkkauksia karttapohjalle, käytämme varmaankin XmlHTTPRequest:ia datan hakemiseksi. Mutta jos rajapinta sijaitsee eri palvelimella, kuin tuo karttaohjelma, niin meillä on ongelma. Ajax -kutsut vieraille palvelimille on selainmaassa iso no-no. Itse asiassa vastaus saattaa usein olla tällainen:
XMLHttpRequest cannot load http://myApiUrl/login. No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'null' is therefore not allowed access.
Tähän löytyy muutama ratkaisu. Ensimmäinen olisi tietysti kierrättää liikenne proxyn kautta. Eli tehdä palvelimelle pieni ohjelma, joka kutsuu vaikkapa cURL:n kautta tuota rajapintaa ja palauttaa sen vastauksen karttaohjelmalle. Sama tieto, mutta nyt se tulee samalta palvelimelta, joten selain hyväksyy sen. Voisiko turhempaa ylimääräistä työtä enää keksiä?
Toinen vaihtoehto on CORS (Cross-Orgin Resource Sharing). Tällöin rajapinta vastaa ylimääräisillä otsaketiedoilla (headers), että vieras tiedonpyyntö on ihan hyväksyttävää. Tämä voisi PHP -maailmassa näyttää seuraavalta:
header('Access-Control-Allow-Origin: *');
Tietoturvan puolesta tämä ei välttämättä ole ihan fiksua. Tähti tarkoittaa villikorttina kaikkien yhteyspisteiden sallimista. Sen sijaan voitaisiin sallia yhteys etukäteen sovituilta palvelimilta. Koska tämä vaihtoehto on joko turvaton tai työläs, on vielä kolmas vaihtoehto, jota voisimme tarkastella.
JSONP pyrkii ratkaisemaan ongelmaa, tosin ei aivan ongelmaton ole sekään. Pohjimmiltaan ongelma on siinä, että JavaScript -koodi ei saa kutsua vierasta palvelinta. Paitsi siinä tapauksessa, että se on script -tagissa. JSONP ei palautakaan suoraan dataa, vaan palauttaa metodikutsun, jonka parametreina on dataa. Tällöin rajapintakutsu voisi olla vaikkapa /posts/1?callback=handlePost. Eli kutsuttu yhteyspiste kutsuukin paikallista metodia ja antaa sille parametreina datan. Ja jotenkin tämä sitten onkin sallittua. Jäljelle jää toteuttaa tuo handlePost -metodi.
Jotta JSONP:tä voitaisiin käytää, pitää rajapinnan tukea sitä. Eli sen sijaan, että palauttaisimme JSON -kokoelman, jos kutsussa on mukana callback -osa, meidän tuleekin palauttaa JSON -kokoelma metodikutsun parametrina. Tässä pitää mainita sekin, että JSONP toimii vain GET -kutsuissa.
PHP -ohjelma palvelimella voisi sitten palauttaa vaikkapa seuraavaa:
echo $_GET['callback'] . '(' . $data . ')';
Ihan yksinkertaista helppoa ratkaisua ei näistä löytynyt. Kaikissa on puolensa ja useimmiten ne puolet ovat huonoja puolia.
Proxy -vaihtoehdon puolesta on kuitenkin sanottava, että se tarjoaa myös hyviä asioita. Jos meillä olisi vaikkapa pelkkä api avain, jolla tunnistaudutaan rajapintaan, niin senhän pitäisi olla sivun lähdekoodissa selaimen kautta luettavissa, jotta sitä voisi JavaScriptillä käyttää. Eli vähän sama, kun kirjoittaisi salasanansa suoraan sivulle. Proxy toimii palvelimella, joten avain on vähän paremmin piilossa. Koska proxy ja karttasovellus ovat samalla palvelimella, ei meidän tarvitse huolehtia CORSista ja toisaalta muut eivät voi JavaScriptin kautta suoraan hyödyntää proxyamme ja jos koittavat sitä hyödyntää cURL:n kautta, niin sitäkin voidaan suodattaa.
Lopuksi
Tässä kirjoituksessa kävimme läpi muutamia sellaisia asioita, jotka eivät mahtuneet ensimmäiseen kirjoitukseen. Tai olisivat toki mahtuneet, kyllä meidän tietokantaan merkkejä mahtuu, mutta ehkä on parempi pitää kirjoitukset edes jokseenkin luettavan mittaisina. Yksi asia, jota näissä kahdessa kirjoituksessa ei vielä käsitelty, mutta joka liittyy oleellisesti rajapintoihin, on REST. Rajapinnoista puhuttaessa saatetaan heittää ilmoille pahaenteinen kysymys - onko se RESTfull? Se aihe on kuitenkin sen verran laaja, että ansaitsee oman kirjoituksensa.
Lähteinä tälle kirjoitukselle toimi useita keskusteluita, blogeja, screencasteja ja Phil Sturgeon kirja Build APIS You Won't Hate.