Single Page Apps, Fetch API - asynkroninen javascript, websockets
- Fetch API
- Flask ja JSON
- Flask ja XML
- Javascript, Fetch ja JSON
- Fetch ja XML
- Tilaajan lisääminen
- HTMX
- Firebase, Firestore ja javascript-sovellus
- Web Sockets
- Firestoren hierarkiat ja transaktiot
- Lisätietoa
Opetellaan käyttämään Javascriptin Fetch API -rajapintaa, jolla saadaan asynkronisesti kommunikoitua WWW-palvelimen kanssa. Myös Firestore-tietokantaan pääsee Javascriptilla käsiksi asynkronisen rajapinnan kautta. Tutustutaan Javascriptin Promiseihin. Kokeilleen myös websocketeja.
Fetch API
Tutustu Javascriptin promiseihin lukemalla Using Promises -artikkeli.
Hyödynnetään Fetch-rajapintaa lomakkeen täytön apuna. Käy läpi MDN:n Using the Fetch API -ohje. Voit myös katsoa vanhan ajax-luennon ja ajax-esimerkit, joissa käytetään jQuerya ja XMLHttpRequest-rajapintaa. Idea näissä on tismalleen sama kuin Fetch-rajapinnassa, mutta Fetch on tällä hetkellä järkevin rajapintavalinta käytettäväksi. Jos Javascript ei ole sinulle ennestään tuttu, niin kertaa TIEA2120 Web-käyttöliittymien ohjelmointi -kurssin ohjaustehtäviä
Kokeile seuraavaksi toteutettavan sovelluksen mallia. Täytä lomakkeelle tietoja. Kokeile syöttää postinumeroksi 40740 ja huomaat kuinka sovellus automaattisesti täyttää postitoimipaikan. Kokeile vaihtaa oppilaitosryhmää, niin sovellus vaihtaa automaattisesti oppilaitoslistan sisältöä. Lue tämän sivun ohjetta eteenpäin, niin toteutat itse vastaavanlaisen sovelluksen. Lähdekoodi (javascript), Lähdekoodi (flask / python), ryhmat.xml (template)
Flask ja JSON
Ensimmäisenä tarvii valmistella palvelimella toimiva ohjelma tuottamaan JSON-muotoista dataa. Luodaan flask-ohjelma, joka palauttaa listan postitoimipaikoista JSON-muodossa.
Luo ensimmäisenä uusi SQLite3-tietokanta. Käytä oppilaitos.sql-SQL-koodia.

- Luo uusi Flask-sovellus users.jyu.fi-palvelimelle samaan tapaan kuin aiemmilla viikoilla.
- Avaa tietokantayhteys edellä luomaasi tietokantatiedostoon
- Kirjoita seuraavanlainen funktio, jotka palauttaa listan kaikista postinumeroista ja postitoimipaikoista JSON-muodossa:
from flask import Flask, session, redirect, url_for, request, Response, render_template, make_response from markupsafe import escape import json @app.route('/postitoimipaikat', methods=['GET']) def postitoimipaikat(): con = tietokanta() cur = con.cursor() cur.execute(""" SELECT postinro, postitoimipaikka FROM Postitoimipaikka """) data = cur.fetchall() # muunnetaan kyselyn tulos json-muotoon. Tarvitaan oma konversiofunktio row_to_json json_data = json.dumps(data, default=row_to_json) # tehdään flaskin edellyttämä response json-datasta. HTTP-koodina käytetään 200 eli OK resp = make_response(json_data, 200) # määritetään responsessa käytetty merkistö ja mediatyyppi resp.charset = "UTF-8" resp.mimetype = "application/json" return resp # muuntaa sqlite3 row -tyyppisen objektin tavalliseksi dictiksi def row_to_json(row): d = dict() for key in row.keys(): d[key] = row[key] return dKts. malli
- Kokeile toimiiko funktiosi. Jos toimii niin tee uusi funktio (ja osoite) postitoimipaikka,
joka palauttaa vain tietyn postinron ja postitoimipaikan sen mukaan mitä annetaan sivun parametreissa.
Esim. osoite https://users.jyu.fi/~omatunnus/cgi-bin/ties4080/ohjaus5/flask.cgi/postitoimipaikka?postinro=40740 palauttaisi vain:
[{"postinro": "40740", "postitoimipaikka": "Jyväskylä"}] - Varmista, että sovellus ei kaadu vaikka postinroa ei annettaisikaan parametrina tai annetaan postinumero, jota ei löydy tietokannasta.
Flask ja XML
Monesti tiedonsiirtomuotona ei olekkaan JSON vaan XML.
Toteuta samaan tapaan kuin edellä kaikkien oppilaitosryhmien hakeminen, mutta palauta listaus sellaisessa XML-muodossa, että sitä voidaan suoraan käyttää HTML-dokumentissa. Käytä mediatyyppinä text/xml. Muodosta tarvittava XML Jinja-templaten avulla:
<?xml version="1.0" encoding="UTF-8"?>
<select id="{{name}}" name="{{name}}" xmlns="http://www.w3.org/1999/xhtml">
{% for o in ryhmat %}
{% if loop.first %}
<option selected="selected" value="{{o["ryhmaid"]}}">{{ o["nimi"] }}</option>
{% else %}
<option value="{{o["ryhmaid"]}}">{{ o["nimi"] }}</option>
{% endif %}
{% endfor %}
</select>
Kts. malli
Käyttäminen poikkeaa hieman aiemmista, koska myös nyt halutaan erikseen varmistaa mikä on merkistö ja mediatyyppi:
resp = make_response( render_template("ryhmat.xml",ryhmat=ryhmat, name="oppilaitosryhma"))
resp.charset = "UTF-8"
resp.mimetype = "text/xml"
return resp
Vaihtoehtoisia toteutustapoja
XML-dokumentin voit toteuttaa myös muilla tavoilla kuin Jinja-templatella. Jinja on kaikista epävarmin tapa tuottaa ehjiä XML-dokumentteja. DOM-rajapinta (minidom) tai ElementTree ovat parempia.
DOM-rajapinta
Saman voi toteuttaa myös minidom-kirjaston avulla jolloin templatea ei tarvita. Vrt. Javascript ja DOM. Kokeile:
from xml.dom.minidom import getDOMImplementation, parse, parseString
impl = getDOMImplementation()
# createDocument(nimiavaruus, juurielementti, dokumenttityyppi)
doc = impl.createDocument("http://www.w3.org/1999/xhtml", "select", None)
# pakko laittaa seuraava, koska jostakin syystä edellinen ei riitä
doc.documentElement.setAttribute("xmlns", "http://www.w3.org/1999/xhtml")
doc.documentElement.setAttribute("name", "ryhmat")
doc.documentElement.setAttribute("id", "ryhmat")
for ryhma in cur.fetchall():
option = doc.createElement("option");
txt = doc.createTextNode( ryhma['ryhmanimi'] );
option.appendChild(txt)
# minidom ei tue textContent-ominaisuutta
# li.textContent = ryhma['ryhmanimi']
option.setAttribute("value", str(ryhma["ryhmaid"]))
doc.documentElement.appendChild(option)
resp = make_response( doc.toxml('UTF-8') )
resp.charset = "UTF-8"
resp.mimetype = "text/xml"
return resp
ElementTree
Voit myös tutustua pythonin xml.etree.ElementTree-kirjastoon ja muodostaa sen avulla tarvittavan xml-dokumentin:
import xml.etree.ElementTree as ET
xml = """<?xml version="1.0" encoding="UTF-8"?><select id="ryhmat" name="ryhmat"></select>"""
root = ET.fromstring( xml )
root.attrib["xmlns"] = "http://www.w3.org/1999/xhtml"
for ryhma in cur.fetchall():
option = ET.SubElement(root, "option")
option.text = ryhma['ryhmanimi']
option.attrib['value'] = str(ryhma["ryhmaid"])
resp = make_response( ET.tostring( root, encoding="UTF-8", method="xml" ) )
Oppilaitokset
Toteuta samaan tapaan oppilaitosten hakeminen (vrt. edellä tehty yhden
postitoimipaikan hakeminen). Rajaa haettavia oppilaitoksia jollakin
oppilaitosryhmällä (ryhmaid). Jos ryhmää ei ole annettu, silloin hae kaikki
oppilaitokset. Palauta oppilaitokset seuraavanlaisessa XML-muodossa.
Luo XML-muoto joko DOM-rajapinnan tai ElementTreen avulla.
<oppilaitokset>
<oppilaitos id="1">Helsingin kauppakorkeakoulu</oppilaitos>
<oppilaitos id="2">Helsingin yliopisto</oppilaitos>
<oppilaitos id="3">Joensuun yliopisto</oppilaitos>
...
</oppilaitokset>
Kts. malli
Javascript, Fetch ja JSON
Ota käyttöön valmis pohja. Pura samaan kansioon flask-sovelluksesi kanssa.
Muokkaa valmista osoite.js-tiedostoa. Toteutetaan postitoimipaikan valinta postinumeron perusteella.
- Lisää postinumeron change-tapahtumaan tapahtumankäsittelijä (
hae_postitoimipaikka) - Luo hae_postitoimipaikka-funktio, jossa Fetch-rajapinnan
avulla kutsut aiemmin flaskilla tekemääsi postitoimipaikka-sivua.
function hae_postitoimipaikka() { // asetukset fetch-kutsua varten // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch let url = new URL("https://users.jyu.fi/~omatunnus/cgi-bin/ties4080/ohjaus5/flask.cgi/postitoimipaikka"); // urliin on lisättävä ?postinumero=arvo, koska kyseessä on GET-tyyppinen request // Tämä tehdään seatchparams-objektin avulla // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams // kts. https://javascriptplayground.com/url-search-params/ url.searchParams.append("postinro", document.forms[0].elements.postinumero.value); // huom! tämän on oltava asyncroninen funktio async function omafetch(url) { let response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // huom. datan sisältö ja rakenne on juuri se mitä itse on muodostettu palvelimella let data = await response.json(); try { document.forms[0].elements.postitoimipaikka.value = data[0]["postitoimipaikka"]; } catch (e) { document.forms[0].elements.postitoimipaikka.value = "tuntematon"; } } // seuraava nappaa kiinni verkkovirheet ym. // Jos annettua osoitetta ei löydy, niin se ei ole virhe omafetch(url).catch(e => { console.log('Virhe postitoimipaikan hakemisessa: ' + e.message); }); } - Kokeile toimiiko sovellus ja muuttuuko postitoimipaikan sisältö. Katso Web Developer -työkalujen Network-välilehdeltä (CTRL+SHIFT+E) mitä tapahtuu, kun vaihdat postinumerokentän sisältöä. Tarkista myös Javascript Consolen (CTRL+SHIFT+K) tulosteet.
Fetch ja XML
Toteutetaan seuraavaksi oppilaitoksen valinta oppilaitosryhmän perusteella.
- Tee Javascript-tiedostoon uusi funktio hae_ryhmat,
jossa
pyydät hae_ryhmat-sivua.
async function hae_ryhmat() { let response = await fetch("/~omatunnus/cgi-bin/ties4080/ohjaus5/flask.cgi/hae_ryhmat"); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let data = await response.text(); let parser = new window.DOMParser(); let doc = parser.parseFromString( data, "text/xml" ); // importoidaan saatu xml-dokumentti tähän dokumenttiin let select = document.importNode(doc.documentElement, 1); document.querySelector("#oppilaitosryhma").replaceWith(select); } - Kutsu hae_ryhmat-funktiota sivun
window.onload-tapahtumassa, niin oppilaitosryhmälistaus päivittyy heti sivun latauduttua.hae_ryhmat().catch(e => { console.log('Virhe ryhmien hakemisessa: ' + e.message); });
Oppilaitoksen vaihtaminen oppilaitosryhmän perusteella
- Luo Javascript-ohjelmaasi uusi funktio
hae_oppilaitokset. Lisää tämä tapahtumankäsittelijäksi oppilaitosryhma-valintalistanchange-tapahtumaan. Tämän lisäämisen voit tehdä vasta oppilaitosryhmän tietojen lataamisen ja sivulle päivittämisen jälkeen. Fetch-kutsut ovat asynkronisia.document.forms[0].elements.oppilaitosryhma.addEventListener("change", hae_oppilaitokset);Sinun pitää myös flask-sovelluksen palauttamassa select-elementissä käyttää samaa id-attribuutin arvoa (oppilaitosryhma), kuin mitä valmiissa pohjadokumentissa käytetään.
- Mieti miksi tapahtumankäsittelijää ei voi kytkeä suoraan
window.onload-funktiossa, kuten muita tarkistusfunktioita?
- Mieti miksi tapahtumankäsittelijää ei voi kytkeä suoraan
- Tee
hae_oppilaitokset-tapahtumankäsittelijässä uusi Fetch-kutsu- Laita URL:ksi
oppilaitokset. - Laita url-parametreihin
ryhmaid-attribuuttiin valittuna olevan oppilaitosryhmän arvo - Onnistuneessa tapauksessa täytä oppilaitoslista saadulla XML-datalla.
- Epäonnistuessa tulosta virheilmoitus konsoliin
- Laita URL:ksi
- Lisää vielä
hae_oppilaitokset-kutsulisaa_ryhmat-funktion loppuun. Näin saat myös ensimmäiset oppilaitosvaihtoehdot näkyviin.
Tilaajan lisääminen
- Lisää lomakkeen alapuolelle listaus kaikista tietokantaan tallennetuista tilaajista. Lataa lista Fetch-kutsulla sivun latautuessa.
- Tee lomakkeesta tavalliseen tapaan toimiva eli painiketta klikattaessa
lisätään tietokantaan uusi tilaaja. Toteuta lisäys siten, että lisäyksen tekevä
flask-funktio palauttaa tiedon lisäämisen onnistumisesta tai mahdollisista
virheistä json-muodossa. Käytä lisäämisessä POST-metodia.
- Jos kaikki onnistuu, palautettava json-tiedosto on tyhjä {}
- Tarkista, että kaikkiin kenttiin on syötetty arvo
- Virheet voit listata kenttäkohtaisesti esim.
{"kentta": "virhe", "kentta2": "virhe", ...}
- Saatuasi lomakkeen ja lisäämisen toimimaan, niin muuta lisäys tapahtumaan
Fetch-kutsulla. Päivitä onnistuneen lisäämisen jälkeen sivulla oleva
tilaajalistaus. Jos lisäys epäonnistuu, niin näytä sivulla virheilmoitukset. Jos
mahdollista, niin aseta virheilmoitukset niitä vastaavien kenttien viereen.
Tämä onnistuu helposti, jos hyödynnät fiksusti kenttien nimiä ja lisäät sopivat elementit valmiiksi virheilmoituksia varten.
Koko lomakkeen sisällön saa helposti Fetch-kutsuun:
let data = new FormData( document.forms[0] ); // nyt post-metodi niin voidaan käyttää myös bodya. Luodaan tässä tarvittava // asetusobjekti: let myInit = { method: 'POST', body: data // bodyyn määritellään käytettävä lomakedata (FormData) }; let response = await fetch("/~omatunnus/cgi-bin/ties4080/ohjaus5/flask.cgi/lisaa_tilaaja", myInit) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let data = await response.json(); lisaa_success( data ); }
Malliratkaisu async/await-versio, javascript-lähdekoodi (async/await)
Lähdekoodi (flask / python), ryhmat.xml (template)
HTMX
HTMX-kirjaston avulla pystyy toteuttamaan edellä tehdyn oppilaitossovelluksen kirjoittamatta riviäkään javascript-koodia. Kts. mallisovellus, joka toimii samaan tapaan kuin aiemmin toteutettu virheilmoituksia lukuunottamatta. Tutki erityisesti sovelluksen html-lähdekoodia. Palvelimella toimivaan sovellukseen täytyi tehdä hieman muutoksia. Kts. Lähdekoodi (flask / python), _htmx-päätteiset funktiot, ryhmat.htmx (template)
Myös virheilmoitukset olisi mahdollista saada toimimaan htmx:n avulla. Keksitkö miten?
Firebase, Firestore ja javascript-sovellus
- Project settings -sivulla voit lisätä projektiisi web-sovelluksen. Valitse Your apps-kohdasta web-sovelluksen kuvake
- Keksi joku lempinimi sovellukselle
- Älä valitse Firebase hostingia
- Valitse CDN
- Saat seuraavanlaisen valmiin koodin, jonka voit kopioida html-tiedostoon:
<script type="module"> // Import the functions you need from the SDKs you need // huom. varmista, että tässä on uusin versio eli kohta 12.0.6 voi muuttua import { initializeApp } from "https://www.gstatic.com/firebasejs/12.10.0/firebase-app.js"; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries // Your web app's Firebase configuration const firebaseConfig = { apiKey: "...", authDomain: "...", projectId: "...", storageBucket: "...", messagingSenderId: "...", appId: "..." }; // Initialize Firebase const app = initializeApp(firebaseConfig); </script> - Lisää myös seuraavat rivit:
// varmista, että kirjaston versio (12.0.6) on sama kuin edellä import { getFirestore } from 'https://www.gstatic.com/firebasejs/12.10.0/firebase-firestore.js' const db = getFirestore(app); -
Seuraava ei nyt oikein kunnolla toimi. Katso parempi ajantasaistettu ohje suoraan Googlen sivulta. Kts. Get data with Cloud Firestore. Seuraa Web modular API:n ohjeita.
Kokeile seuraavalla koodilla uusien dokumenttien luomista ja dokumenttien muuttamista. Tämä toimii Web namespaced APIn mukaan.let db = app.firestore(); db.collection("users").doc("alovelace").set({ first: "foo", last: "Lovelace", born: 1815 }) .then(() => { console.log("Document with ID: alovelace updated"); }) .catch((error) => { console.error("Error adding document: ", error); }); // Add a second document with a generated ID. db.collection("users").add({ first: "Alan", middle: "Mathison", last: "Turing", born: 1912 }) .then((docRef) => { console.log("Document written with ID: ", docRef.id); }) .catch((error) => { console.error("Error adding document: ", error); }); // tässä listauksessa eivät vielä näy edellä lisätyt, koska lisäys tapahtuu asynkronisesti db.collection("users").get().then((querySnapshot) => { querySnapshot.forEach((doc) => { console.log(`${doc.id} => ${JSON.stringify(doc.data())}`); }); }); - Käy vielä tutkimassa sovelluksesi Rules-sivua, jolla voit säätää
tietokantasi käyttöoikeuksia. Oletuksena Test-moodissa olevaan
tietokantaan on kaikilla luku- ja kirjoitusoikeus. Tuotantoversiossa
nämä pitää säätää järkevämmiksi. Lue Get started with Cloud Firestore Security Rules.
Nämä säännöt eivät koske edellä tehtyä Python-versiota vaan ainoastaan
Javascriptilla tehtyjä suoria pyyntöjä Firestore-tietokantaan.
Käytännössä Javascriptille kannattaa sallia vain lukuoperaatioita
sellaisiin tietokannan osiin, joiden sisältö on julkisesti
sovelluksessa kaikkien saatavilla. Käyttöoikeuksia tarvitsevat osat
kannattaa hoitaa palvelimella toimivan sovelluksen (flask) kautta.
Kirjautuminen vaaditaan:
Kaikki oikeudet kielletty:// Sallii kaikki oikeudet niille, jotka ovat kirjautuneet sovellukseen service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth != null; } } }
Kaikilla on luku- ja kirjoitusoikeus// Deny read/write access to all users under any conditions service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if false; } } }// Allow read/write access to all users under any conditions // Warning: **NEVER** use this rule set in production; it allows // anyone to overwrite your entire database. service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if true; } } }
Web Sockets
Tutustu web socketien käyttämiseen Flask-SocketIO (Flask) ja Socket.IO (Javascript) -kirjastojen avulla. Yksinkertaisimmillaan pääset alkuun, kun kokeilet tehdä pienen chatin. Samat asiat käydään läpi kunnolla selitettyinä Flask-SocketIO:n Gettin Started -dokumentissa. Yritä saada pieni chat-toimimaan ja kokeile sitä useammalla eri selaimella samaan aikaan.
Chatin voi rakentaa suoraan Firestoren päälle. Kaikista kätevimmin se toimisi, jos käytettäisiin suoraan Firestoren omaa javascript-rajapintaa, mutta kokeile tehdä oma web socketien päälle:
from flask import Flask, render_template, request
from flask_socketio import SocketIO, send, emit, join_room, leave_room
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore
import threading
from datetime import datetime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
socketio = SocketIO(app)
cred = credentials.Certificate('service.json') #firestoren kredentiaalit
firebase_admin.initialize_app(cred)
db = firestore.client()
# Tarvitaan säie, joka seuraa firestoren tapahtumia
# ei pitäisi olla tehokkuusongelma, koska näitä tulee vain yksi per instanssi
callback_done = threading.Event()
#seurataan firestoren muutoksia
def on_snapshot(doc_snapshot, changes, read_time):
for change in changes:
if change.type.name == 'ADDED':
# viestin timestamp täytyy muuntaa stringiksi, jotta muunnos json-muotoon onnistuu
data = change.document.to_dict()
#pätkäistään ISO-muodosta ylimääräiset pois
data['timestamp'] = data['timestamp'].isoformat().split(".")[0]
#lähetetään viesti. Oletuksena menee json-muodossa
socketio.emit("message", data, include_self=True)
callback_done.set()
#järjestetään viestit aikaleiman mukaan
messages = db.collection("chat").order_by("timestamp", direction=firestore.Query.ASCENDING)
#seurataan muutoksia
doc_watch = messages.on_snapshot(on_snapshot)
@app.route('/')
def index():
return render_template('index.html')
@socketio.on('join')
def handle_join(username):
doc_ref = db.collection('chat').document()
data = {"nick": username,
"message": "Saapuu paikalle",
data['timestamp'] = datetime.now() #kannattaisiko käyttää selaimen aikaleimaa?
}
doc_ref.set(data)
@socketio.on('message')
def handle_message(data):
doc_ref = db.collection('chat').document()
data['timestamp'] = datetime.now() #kannattaisiko käyttää selaimen aikaleimaa?
doc_ref.set(data)
@socketio.on('disconnect')
def handle_disconnect():
doc_ref = db.collection('chat').document()
data = {"nick": username, "message": "Poistui chatista"}
data['timestamp'] = datetime.now()
doc_ref.set(data)
if __name__ == '__main__':
socketio.run(app, debug=True)
Sovellukseen tarvitset myös seuraavaan html- ja javascript-koodin:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat-sovellus</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script>
window.addEventListener("load", function() {
let socket = io();
let username = prompt("Nick:"); // Kysytään käyttäjän nick chattia varten
socket.emit("join", username);
let messages = document.getElementById("messages");
// Kuunnellaan palvelimelta tulevia viestejä
socket.on("message", function(data) {
let msg = document.createElement("p");
let n = document.createElement("strong");
msg.appendChild(n);
n.textContent = data.timestamp + "<" + data.nick + "> ";
msg.appendChild( document.createTextNode(data.message) );
messages.appendChild(msg);
});
let msgInput = document.getElementById("msg");
let sendMessage = function() {
let message = {'nick': username, 'message': msgInput.value}
socket.send(message);
msgInput.value = "";
}
document.querySelector("button").addEventListener("click", sendMessage);
});
</script>
</head>
<body>
<h2>Chat-sovellus</h2>
<input id="msg" type="text" placeholder="Kirjoita viesti">
<button>Lähetä</button>
<div id="messages"></div>
</body>
</html>
Miten chat-sovellusta pitäisi tästä eteenpäin parannella? Mieti ratkaisutapoja ainakin seuraaviin ongelmiin. Osaa näistä on käsitelty Flask-SocketIO:n dokumentaatiossa.
- Pitääkö käytetty nick yhdistää kirjautuneeseen käyttäjään tai edes samaan sessioon? Nyt voi kuka tahansa vapaasti esiintyä millä nickillä tahansa
- Pitäisikö liittyneellä käyttäjälle näyttää vain ne viestit, jotka ovat tulleet hänen liittymisensä jälkeen? Entä jos hän poistuu välillä ja palaa taas myöhemmin? Mitkä viestit hänelle pitäisi näyttää?
- Voisiko chat-keskustelussa olla hierarkia eli voisi vastata johonkin tiettyyn viestiin ja tästä muodostuisi viestihierarkia
- Chat on nyt tässä sovelluksessa globaali. Miten voisit luoda erilaisia chat-huoneita?
The Async Core: Understanding Eventlet and Gevent in Flask-SocketIO
Firestoren hierarkiat ja transaktiot
- Firestoressa voi luoda hierarkisia dokumenttikokoelmia. Hierarkisuus
on tarpeen vain jos on tarpeen käyttää transaktioita. Lue Transactions and batches writes.
Esim. jos sinulla on joukkue ja joukkueella on jäseniä, niin yleensä halutaan
muuttaa / päivittää samalla kerralla joukkueen ja sen jäsenten tiedot. Tässä
tapauksessa transaktiolle olisi tarvetta, JOS jäsenet ovat omana dokumenttinaan.
Vrt. aiemmin tehtyyn relaatiotietokantaan, jossa jäsenet oli tallennettu json-muodossa eikä silloin tarvittu jäsenille erillistä taulua. Jos jäsenet olisi laitettu erilliseen tauluun, tarvittaisiin relaatiotietokannassakin transaktiota joukkueen tietoja päivitettäessä. Jäsenten ollessa samassa tietueessa joukkueen muiden tietojen kanssa ei transaktiota tarvita.
Oletuksena voit käyttää firestorea kuten käytit aiemmin datastorea eli voit tehdä rakenteen relaatiotietokannan tapaan ja käyttää yhdistäviä avainkenttiä.
Luodaan malliksi hierarkinen reseptitietokanta, joka ei ehkä ole paras esimerkki, koska tämä toimisi paremmin ilman hierarkiaa.
ruokalajit = [ {"id": 1, "nimi": "Alkuruoka", "reseptit": []}, {"id": 2, "nimi": "Pääruoka", "reseptit": [ {"nimi": "Ananas Con Carne", "kuvaus": "Tulinen", "henkilomaara": 4}, {"nimi": "Banaanicurry", "kuvaus": "Mainio kasvisruoka", "henkilomaara": 2} ]}, {"id": 3, "nimi": "Lisäkeruoka", "reseptit": []}, {"id": 4, "nimi": "Väli- tai iltapala", "reseptit": []}, {"id": 5, "nimi": "Jälkiruoka", "reseptit": []}, {"id": 6, "nimi": "Suolainen leivonnainen", "reseptit": []}, {"id": 7, "nimi": "Makea leivonnainen", "reseptit": [ {"nimi": "Tiramisu", "kuvaus": "Hieno herkku", "henkilomaara": 6}, {"nimi": "Porkkanapiirakka", "kuvaus": "Suuri herkku", "henkilomaara": 8}, {"nimi": "Rommikakku", "kuvaus": "Hyytelökakku, ei tarvi paistaa", "henkilomaara": 6}, {"nimi": "Kauralastut", "kuvaus": "Helppo ja nopea", "henkilomaara": 4} ]} ] for ruokalaji in ruokalajit: #luodaan ruokalajit käyttäen omia tunnisteita (id) rlaji = db.collection('ruokalaji').document(str( ruokalaji["id"] )) rlaji.set({ "nimi": ruokalaji["nimi"] }) reseptit = ruokalaji["reseptit"] #luodaan reseptit automaattisilla tunnisteilla # for resepti in reseptit: # db.collection("ruokalaji", rlaji.id, "reseptit").document().set({ # "nimi": resepti["nimi"], # "kuvaus": resepti["kuvaus"], # "henkilomaara": resepti["henkilomaara"] # }) #annetaan resepteillekin oma id, että on helpompi testailla #huom eri ruokalajien alla olevilla resepteillä voi olla sama id! count = 1 for resepti in reseptit: db.collection("ruokalaji", rlaji.id, "reseptit").document(str(count)).set({ "nimi": resepti["nimi"], "kuvaus": resepti["kuvaus"], "henkilomaara": resepti["henkilomaara"] }) count = count + 1Kokeile valmiissa koodissa olevia eri tapoja luoda reseptit. Tutki miten ne ilmestyvät näkyviin Firestoren ylläpitonäkymään. Poista tuplaversiot samoista resepteistä ylläpitonäkymän kautta
- Aina ei ole käytettävissä valmista yksilöivää tietoa. Firestore osaa itse automaattisesti luoda
dokumentille uniikin id:n
>
#luo uuden dokumentin, joka on vielä tyhjä doc_ref = db.collection('tyyppi').document() #tunnisteen (id) saa suoraan id-ominaisuudesta id = doc_ref.id #dokumentin sisällön voi tallentaa set-metodilla doc_ref.set( omadata ) - Listaa kaikki ylätason kokoelmat:
collections_ref = db.collections() for col in collections_ref: documents = col.stream() for doc in documents: print(f'{doc.id} => {doc.to_dict()}') -
Mitäs jos halutaan listata kaikki reseptit? Tämä on hankalampaa, koska
jokainen resepti asuu yksittäisen ruokalajin reseptit-kokoelmassa.
Yksittäiseen ruokalajiin kuuluvat reseptit voi hakea seuraavalla
tavalla:
Kaikkien reseptien hakemiseen tarvitaan collection group -kysely:#haetaan kaikki jälkiruokareseptit. Jälkiruokalajin id on 7 reseptit = db.collection('ruokalaji/7/reseptit').stream() for resepti in reseptit: print(f'{resepti.id} => {resepti.to_dict()}')reseptit = db.collection_group('reseptit') docs = reseptit.stream() for doc in docs: print(f'{doc.id} => {doc.to_dict()}')Tämä ei kuitenkaan heti onnistu vaan ohjelma kaatuu virheeseen, jossa ilmoitetaan, että kysely tarvitsee tietynlaisen indeksin. Virheilmoituksessa saa suoraan kopioitua linkin, jonka avaamalla saa luotua tarvittavan indeksin.
"The query requires a COLLECTION_GROUP_ASC index for collection reseptit and field type. You can create it here: https://console.firebase.google.com/...Indeksin luominen kestää tovin. Jos yrität kyselyä uudelleen eikä indeksointi ole vielä valmista, saat uuden virheilmoituksen:
400 The query requires a COLLECTION_GROUP_ASC index for collection reseptit and field type. That index is not ready yet. See its status here: https://console.firebase.google.com... - Yritä listata reseptit ruokalajeittain seuraavalla tavalla:
Alkuruoka Pääruoka Ananas Con Carne Banaanicurry Lisäkeruoka Väli- tai iltapala Jälkiruoka Suolainen leivonnainen Makea leivonnainen Tiramisu Porkkanapiirakka Rommikakku KauralastutMalli
ruokalajit = db.collection('ruokalaji').stream() for ruokalaji in ruokalajit: print(f'{ruokalaji.to_dict()["nimi"]}') reseptit = db.collection('ruokalaji', ruokalaji.id, 'reseptit').stream() for resepti in reseptit: print(f'\t{resepti.to_dict()["nimi"]}') - Lisätietoa löytyy seuraavista : Admin SDK Reference ja google-cloud-firestore
Lisätietoa
Vanhoja versioita:
Yksinkertaisin ajax-malli (jquery), flask-osuuden lähdekoodi
vanha malliratkaisu
Lähdekoodi (jQuery)
Käyttäjien kommentit