Odczyt z plikowej bazy danych SQL po stronie przeglądarki
Dla tego postu utworzyłam hashtag crazy, bo będziemy dzisiaj robić rzeczy absurdalne. Albo absurdalnie wspaniałe. Z pewnością nie nadające się na produkcję, ale chcę, żebyście pobawili się razem ze mną :)
Long story short: istnieje biblioteka sql.js, która pozwala się "połączyć się" z bazą plikową w trybie odczytu i wywoływać normalne zapytania SQL. Wszystko w przeglądarce, bez części back-endowej.
Zaczynajmy
Za pomocą darmowego programu DB Browser for SQLite przygotujmy sobie bazę danych. Ma przyjazny interfejs dzięki któremu możemy sobie wyklikać i zapisać nową bazę danych do pliku :)
Stwórzmy tam tabelę projects z kolumnami id i name, z czego id będzie autogenerowanym kluczem (zaznacz opcje primary key oraz autogenerated). Wypełnijmy ją przykładowymi danymi.
Teraz stwórzmy front-endowy projekt :)
Zwykły plik HTML, plik JS oraz webpack.config - to podstawa naszego eksperymentu. Dla uproszczenia skorzystamy również z npm'a, ale gdy dodasz plik js do strony to również będzie działać :)
//package.json
{
"name": "sqljsexample",
"version": "0.0.0",
"description": "Example of sql.js usage",
"main": ".script.js",
"scripts": {
"webpack": "webpack --config webpack.config.js",
"server": "webpack-dev-server"
},
"author": "Solution Chaser",
"license": "ISC",
"devDependencies": {
"webpack": "^4.18.0",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.8"
},
"dependencies": {
"sql.js": "^0.5.0"
}
}
Jedyną zależnością jest wyżej wspomniana biblioteka, ale do pracy developerskich potrzebujemy także serwera webpackowego. Dlaczego? Jak powszechnie wiadomo przeglądarka nie ma dostępu do dysku, może tylko pobierać pliki po HTTP. I tak dokładnie zrobimy :)
Nie jest to problem ponieważ:
1) W wersji developerskiej wystawimy sobie plik database poprzez webpack-dev-server.
2) W wersji produkcyjnej i tak potrzeba będzie wystawić pliki statyczne, więc dodatkowy nie zrobi różnicy.
//webpack.config.js
module.exports = {
entry: './script.js',
mode: "development",
output: {
filename: 'build.js',
publicPath: '/dist/'
},
node: {
fs: 'empty'
}
}
Tutaj należy zwrócić na dwie rzeczy:
1) Zdefiniowanie publicPath w której umieścimy bazę
2) Właściwość node, w której istnieje `fs: empty`. Żeby nie przedłużać: webpack nie lubi się z sql.js, dzięki temu poprawnie wygeneruje się plik wynikowy :)
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet">
<title>Simple Vue VideoPlayer</title>
</head>
<body>
<h2>List of my projects</h2>
<ul id="projects"></ul>
<script src="/dist/build.js"></script>
</body>
</html>
Maksymalnie uprościłam plik html, mamy tutaj pustą listę z zdefiniowanym id.
Pobieranie bazy danych
Najpierw musimy pobrać bazę. Normalnie, fetchem, tylko potem zamienić odpowiedź na bufor, a potem na odpowiedni typ (Uint8Array). Wynika to z faktu, że najpierw pobieramy plik binarny i musimy pobrać cały stream (pierwsza konwersja), a potem konwertujemy go na typ, który przyjmie konstruktor biblioteki.
fetch(config.url).then((response) => {
response.arrayBuffer().then((buffer) => {
database(buffer);
})
})
Baza danych
Skoro już wywołaliśmy u góry funkcję inicjującą to trzeba ją teraz napisać :)
Najważniejszą informacją jaką trzeba przyswoić jest fakt, że baza jest przechowywana w pamięci przeglądarki i można na niej zrobić wszystko, także dodawać tabele i krotki. Niestety nie wpływa to na plik na serwerze. Jednak do tego jeszcze wrócimy.
const database = (buffer) => {
const uInt8Array = new Uint8Array(buffer);
const db = new SQL.Database(uInt8Array);
const projects = db.exec("select id, name from projects");
fillTable(projects[0].values);
}
Funkcja SQL.Database przyjmuje tablicę 8-bitowych intów lub można pozostawić konstruktor bez argumentów. Wtedy tworzymy nową, pustą bazę danych.
Po utworzeniu obiektu db
, możemy spokojnie wywoływać funkcję z zapytaniem SQL. Warto zapoznać się z nimi, bo jest exec
nie jest jedyną opcją.
Odpowiedź, która przyjdzie ma niestety przedziwny format, gdyż zawiera tablicę z zarówno konstrukcją tabeli jak i wynikami, które nie są obiektem, a... kolejną tablicą.
Poszczególny wiersz, gdy już się do niego dokopiemy, wygląda tak: ['1', 'project name']
zamiast obiektu.
Dlatego moim zdaniem używanie tego bez dodatkowych bibliotek lub ORMa jest strasznie męczące i zaraz podam wam rekomendację.
Na koniec funkcja wyświetlająca wyniki, żeby HTML nie pozostał pusty:
if (!list.length) {
return;
}
const projecstList = document.getElementById('projects');
list.forEach((row) => {
const element = document.createElement('li');
element.innerHTML = row.toString();
projecstList.appendChild(element);
});
Warto również wspomnieć również, że istnieje możliwość eksportu takiej bazy z pamięci do zmiennej. Całkowicie przeczy to postawionej tu idei, ale dałoby się ją wysłać i zapisać na serwerze przy pomocy backendowej aplikacji.
Posumowanie
Temat kompilacji z C do Javascript jest strasznie ciekawy, a sama możliwość zabawy z bazą plikową na frontcie daje mnóstwo możliwości. Niewątpliwie jest to rozwiązanie naszpikowane wadami, ale sądzę, że znajdą się przypadki, szczególnie nieprodukcyjne, gdzie może to być strzał w dziesiątkę. Ba! Może w jakimś przypadku posiadanie małych, tanich baz read-only będzie ogromną zaletą (na przykład przy testach automatycznych).
Przede wszystkich nie podchodziłabym do tego bez ORMa, osobiście korzystam z TypeORM, który posiada pełne wsparcie dla sql.js i serdecznie go polecam.
Chciałabym jeszcze zwrócić na coś uwagę: Bazy SQLite w wersji obsługiwanej przez sql.js nie są zabezpieczone hasłem ani szyfrowane, więc udostępnienie ich jest bardzo niebezpieczne.
Dziękuję za uwagę, mam nadzieje, że temat was zainteresował lub/i poszerzył horyzonty. Jak zawsze czekam na komentarze i pytania, zachęcam do udostępnienia :)
Na koniec pełny skrypt:
const SQL = require('sql.js');
window.onload = () => {
console.log('hello');
fetchDatabase();
}
const config = {
url: '/dist/database'
}
const fetchDatabase = () => {
fetch(config.url).then((response) => {
response.arrayBuffer().then((buffer) => {
database(buffer);
})
})
}
const database = (buffer) => {
const uInt8Array = new Uint8Array(buffer);
const db = new SQL.Database(uInt8Array);
const response = db.exec("select id, name from projects");
fillTable(response[0].values);
}
const fillTable = (list) => {
if (!list.length) {
return;
}
const projecstList = document.getElementById('projects');
list.forEach((row) => {
const element = document.createElement('li');
element.innerHTML = row.toString();
projecstList.appendChild(element);
});
}