Techblog

AlaSQL: Performanz mit Client-seitiger Datenbank

Von Nicola Zunino @NicolaZStrong auf Twitter
6. Mai 2019

AlaSQL ist eine in-memory Datenbank (imDB) die ganz auf JavaScript basiert. Im Gegensatz zu den gängigen Datenbanken, die Daten auf Festplatten speichern, benutzt eine in-memory Datenbank den Direktzugriffsspeicher (RAM). In-memory-Datenbanken punkten mit einer hohen Performance im Vergleich zu klassischen Datenbanken, da alle Daten im internen Speicher gelagert sind. Das ist ein entscheidender Faktor für Anwendungen, die auf hoher Geschwindigkeit basieren. Entsprechend werden sie häufig verwendet für Anwendungen, die zeitkritisch sind. Eine andere sehr bekannte imDB ist Redis.

Der Ursprung

AlaSQL wurde entworfen, um Daten schnell und effizient in Node.js-Kontexten zu verwalten. Seine Leichtigkeit und Performance trug zur schnellen Verbreitung in allen JavaScript-Kontexte bei, auch ohne die Anwendung spezieller Frameworks: Um AlaSQL benutzen zu können, braucht man nur einen Web-Browser mit JavaScript Unterstützung.

AlaSQL ist Open Source unter MIT-Lizenz. Es wird von von den gängigen Browsern unterstützt (Google Chrome, Mozilla Firefox, Apple Safari, Internet Explorer und Opera Browser), inklusive der mobilen Versionen der Browser auf iOS und Android.

Schwerpunkte von AlaSQL sind die Geschwindigkeit der Queries und die Möglichkeit, relationale sowie Schema-less Daten verwalten zu können. Schema-less meint nicht strukturierte Daten, also Daten, die nicht-homogene Informationen repräsentieren, die normalerweise in NoSQL-Datenbanken gespeichert werden. Noch dazu kann AlaSQL ETL-Prozesse durchführen und Daten persistieren. Nach der Datenverarbeitung und Persistierung, sind die Daten in verschiedene Formate exportierbar.

AlaSQL importieren

Es gibt verschieden Art und Weisen, AlaSQL in eine Javascript-Seite zu importieren, zum Beispiel:

  1. // CommonJS style
  2. const alasql = require('alasql');
  3.  
  4. // ES6 style
  5. import * as alasql from 'alasql';
  6.  
  7. // Global variable style
  8. window.alasql

Ein Beispiel

Um AlaSQL in einer HTML-Seite verwenden zu können, müssen wir es zuerst importieren. Wir holen uns die passende Library in CDN durch diesen Link: "https://cdn.jsdelivr.net/npm/alasql@0.4". Als Alternative können wir uns die .js-Datei unterladen und sie im unserem lokalen Projekt einbinden.

Um die Library letztendlich in unsere Website importieren, müssen wir nur den folgenden <script> Tag im <head> der Seiten hinzufügen:

  1. <script src="https://cdn.jsdelivr.net/npm/alasql@0.4"></script>

Sobald uns die Library zur Verfügung steht, können wir die Queries mit einer alasql() Zeile durchführen. Mit alasql() können wir die meisten der klassischer SQL-Befehle durchführen, unter anderen JOIN, VIEW, GROUP BY, UNION, PRIMARY KEY, ANY, ALL. Eine komplette Liste findet man in der offiziellen Doku.

Als einfaches Beispiel erstellen wir eine HTML-Seite, in der wir die AlaSQL Library importieren. Danach führen wir eine einfache SELECT Query durch. Bevor wir den zweiten Schritt machen können, müssen wir zuerst eine Datenstruktur erstellen und sie mit Daten füllen, um diese Daten danach abfragen zu können. AlaSQL erlaubt nicht nur Tabellen, sondern kann auch JSON-Daten verwalten (in reinem NoSQL-Stil). Ich werde erst mit Tabellen (relationales Verhalten) und später mit JSON-Daten (NoSQL Verhalten) arbeiten.

Als erstes erstellen wir eine Datenbank:

  1. const my_database = new alasql.Database('newDB');

Wir werden dann die my_database Variable benutzen, um eine neue Tabelle zu erstellen (CREATE TABLE), Daten der Tabelle hinzuzufügen (durch ein INSERT Statement) und als letztes die Daten abfragen:

  1. my_database .exec("CREATE TABLE people (name string, age number)");
  2. my_database .exec("INSERT INTO people VALUES ('Franz', 18),('Ida', 22)");
  3. const result = my_database .exec("SELECT * FROM people WHERE age < 20");

Der Code ist leicht zu verstehen und braucht keine Kommentare, als Antwort wird die Person, deren Name Mario ist, zurückgegeben.

Um das alles testen zu können, fügen wir in den HTML Body eine <div> hinzu, wo wir die Daten anzeigen möchten, die wir extrahieren:

  1. <div id="list_of_people"></div>

Jetzt können wir das gesamte Skript der Seite hinzufügen und es erweitern, um die <div> zu befüllen:

  1. <script src="https://cdn.jsdelivr.net/npm/alasql@0.4">
  2.     const my_database = new alasql.Database('newDB');
  3.     my_database .exec("CREATE TABLE people (name string, age number)");
  4.     my_database .exec("INSERT INTO people VALUES ('Franz', 18),('Ida', 22)");
  5.     const result = my_database .exec("SELECT * FROM people WHERE age < 20");
  6.     document.getElementById("list_of_people").innerHTML = JSON.stringify(result);
  7. </script>

Wir können ein ähnliches Verfahren wie das gezeigte verwenden, um auf eine Query JSON-Strukturen auszuführen:

  1. const people = [{ name: 'Franz', age : 18 },{ name: 'Ida', age : 22 },];
  2. const result = alasql("SELECT * FROM ? people WHERE age < 22", [people]);
  3. document.getElementById("list_of_people").innerHTML = JSON.stringify(result);

Mit der ersten Zeile haben wir ein JSON-Objekte-Array erstellt (in dieses Beispiel haben alle Objekte dieselbe Struktur, das musst aber nicht unbedingt so sein), der people heißt. Dann haben wir den Array als Daten-Struktur benutzt und sie durch Function alasql() abgefragt.

Beispiel zusammengefügt

Das komplette Beispiel, das auch eine neuere Version von AlaSQL benutzt, sieht dann so aus:

  1. <!DOCTYPE html>
  2. <html>
  3.     <head>
  4.         <title>AlaSQL</title>
  5.         <meta charset="UTF-8">
  6.         <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.         <script src="https://cdn.jsdelivr.net/npm/alasql@0.4"></script>
  8.         <script>
  9.             // QUERY ON DATABASE
  10.             const db = new alasql.Database('test');
  11.             db.exec('CREATE TABLE account (username string, password string)');
  12.             db.exec('INSERT INTO account (?,?)', ['name1', 'pwd1']);
  13.             db.exec('INSERT INTO account (?,?)', ['name2', 'pwd2']);
  14.             db.exec('SELECT * FROM account', [], function (res) {
  15.                 res.forEach(function (account) {
  16.                     console.log(account.username);
  17.                 });
  18.             });
  19.  
  20.             // QUERY ON JSON / OBJECTS
  21.             const data = [{a: 1, b: 10}, {a: 2, b: 20}, {a: 1, b: 30}];
  22.             const resData = alasql('SELECT a, SUM(b) AS b FROM ? GROUP BY a', [data]);
  23.             resData.forEach(function (item) {
  24.                 console.log(item.a + " - " + item.b);
  25.             });
  26.         </script>
  27.     </head>
  28.     <body></body>
  29. </html>

Es ist wieder erkennbar, wie leicht es ist! Dieses Beispiel fügt die beiden vorher gezeigten Teile zusammen. Man kann auf einen Blick sehen, dass die Datenbank-Instanz optional ist.

Weitere Features

Weitere fortgeschrittene SQL Features sind implementiert, zum Beispiel: ROLLUP(), CUBE(), GROUPING SETS(), CROSS APPLY, OUTER APPLY, WITH SELECT.

User-Defined Functions

Es ist möglich, eigene Funktionen zu definieren, um die Vorteile von SQL und Javascript zusammen zu nutzen. Dafür verwendet man das alasql.fn Objekt:

  1. alasql.fn.myfunction = function(a,b) {
  2.   return a * b + 1;
  3. };
  4. const res = alasql('SELECT myfunction (a,b) FROM one');

Mehr dazu im AlaSQL-Wiki auf Github hier.

Compiled Statements

Compiled Statemnts in AlaSQL bringen die selbe Vorteile die im Standard SQL Prepared Statement bringen. Da im Gegensatz zu gewöhnlichen Statements die Compiled Statements noch keine Parameterwerte erhalten, werden dem Datenbanksystem Platzhalter übergeben. Dadurch können SQL-Injections effektiv verhindert werden.

  1. const ins = alasql.compile('INSERT INTO one VALUES (?,?)');
  2. ins(1,10);
  3. ins(2,20);

Compiled Statements die wie hier im Beispiel mehrmals kurz aufeinander aufgeruft werden, können auch einen Geschwindigkeitsvorteil bringen, da das Statement schon vorübersetzt im Datenbanksystem vorliegt und nur noch mit den neuen Parametern ausgeführt werden muss. Weiteres hier.

Lesen und schreiben von Dateien

Man kann Daten aus verschiedenen Datei-Typen importieren und exportieren, etwa CSV, TAB, TXT und JSON. File Extensions sind optional.

  1. const tabFile = 'mydata.tab'; alasql.promise([
  2.           "SELECT * FROM txt('MyFile.log') WHERE [0] LIKE 'M%'", // parameter-less query
  3.           [ "SELECT * FROM tab(?) ORDER BY [1]", [tabFile] ], // [query, array of params]
  4.           "SELECT [3] AS city,[4] AS population FROM csv('./data/cities')",
  5.           "SELECT * FROM json('../config/myJsonfile')" ]).then(function(results){
  6.           console.log(results);
  7. }).catch(console.error);

In diesem Beispiel gibt es Aufrufe auf mehrere Dateien. Da die Aufrufe asynchron laufen, muss man die Queries miteinander verketten.

Performance

AlaSQL soll möglichst schnell sein, dafür gibt es einige SQL-Engine-Optimierungen, zum Beispiel:

  • Queries werden im Cache als Compiled Functions gespeichert
  • Tabellen, die in Join benutzt werden, bekommen ein Pre-Index
  • WHERE wird vor Join durchgeführt

Zweites Beispiel

Als Beispiel möchte ich eine einfache TODO-List-Anwendung realisieren, für die React benutzt wird. Hier bauen wir die React-Komponente:

  1. import React from 'react';
  2. import * as alasql from 'alasql';
  3.  
  4. class App extends React.Component {
  5.  
  6.   constructor(props) {
  7.     super(props);
  8.     this.state = { todo: [] };
  9.   }
  10.  
  11.   componentWillMount() {
  12.     alasql('CREATE TABLE todo (id INT AUTOINCREMENT PRIMARY KEY, text STRING)');
  13.   }
  14.  
  15.   addTodo() {
  16.     const { todo } = this.state;
  17.     const { inputTodo } = this.refs;
  18.     todo.push(inputTodo.value);
  19.     inputTodo.value = "";
  20.     this.setState({ todo });
  21.   }
  22.  
  23.   removeTodo(index) {
  24.     const { todo } = this.state;
  25.     todo.splice(index, 1);
  26.     this.setState({ todo });
  27.   }
  28.  
  29.   render() {
  30.     const { todo } = this.state;
  31.     return (
  32.       <main className="container">
  33.         <h1 className="mt-4">TODO List</h1>
  34.         <div className="row mt-4">
  35.           <form className="form-inline">
  36.             <div className="form-group mx-sm-3 mb-2">
  37.               <label for="inputTodo" className="sr-only">Todo</label>
  38.               <input type="text" ref="inputTodo" className="form-control" id="inputTodo" placeholder="Todo"/>
  39.             </div>
  40.             <button type="button" className="btn btn-primary mb-2" onClick={ e => this.addTodo() }>Add</button>
  41.           </form>
  42.         </div>
  43.         <div className="row">
  44.           <table className="table table-bordered">
  45.             <thead>
  46.               <tr>
  47.                 <th>TODO</th>
  48.                 <th></th>
  49.               </tr>
  50.             </thead>
  51.             <tbody>
  52.               {
  53.               !todo.length &&
  54.               <tr>
  55.                 <td colspan="2" className="text-center">
  56.                   No data available
  57.                 </td>
  58.               </tr>
  59.               }
  60.               {
  61.               todo.length > 0 && todo.map((x,i) => (
  62.               <tr>
  63.                 <td>{ x }</td>
  64.                 <td>
  65.                   <button className="btn btn-danger" onClick={ e => this.removeTodo(i) }>
  66.                     x
  67.                   </button>
  68.                 </td>
  69.               </tr>
  70.               ))
  71.               }
  72.             </tbody>
  73.           </table>
  74.         </div>
  75.       </main>
  76.       );
  77.   }
  78. }
  79.  
  80. export default App;

Die React-Komponente allein kann die einzelnen TODOs nicht persistieren. Dafür wollen wir AlaSQL integrieren.

  1. import React from 'react';
  2. import * as alasql from 'alasql';
  3.  
  4. class App extends React.Component {
  5.  
  6.   // Constructor ...
  7.  
  8.   componentWillMount() {
  9.     alasql('CREATE TABLE todo (id INT AUTOINCREMENT PRIMARY KEY, text STRING)');
  10.   }
  11.  
  12.   fetchTodos() {
  13.     const result = alasql('SELECT * FROM todo');
  14.     this.setState({ todo: result });
  15.   }
  16.  
  17.   insertTodo(text) {
  18.     alasql('INSERT INTO todo VALUES ?',
  19.       [{ id: alasql.autoval('todo', 'id', true), text }]);
  20.   }
  21.  
  22.   deleteTodo(id) {
  23.     alasql('DELETE FROM todo WHERE id = ?', id);
  24.   }
  25.  
  26.   // Lines of code ...
  27. }
  28.  
  29. export default App;

Jetzt haben wir vier Methoden, die noch total entkoppelt von der React-App sind. Die vier Methoden tun folgendes:

  1. componentWillMount ist eine React Methode die die Tabelle todo instanziiert jedes Mal, dass die Komponente geladen wird;
  2. fetchTodos lädt alle TODOs;
  3. insertTodo persistiert die TODOs in der Tabelle;
  4. deleteTodo löscht die TODOs von der Tabelle.

Mit alasql.autoval bekommt man die neue ID, die generiert wurde, da die Tabelle todo ein id Feld hat, das autoincrementable ist. Der nächste Schritt ist, die beiden Code-Teile zusammen zu bringen, so dass die React-Komponente die AlaSQL-Aufrufe benutzt:

  1. import React from 'react';
  2. import * as alasql from 'alasql';
  3.  
  4. class App extends React.Component {
  5.  
  6.   constructor(props) {
  7.     super(props);
  8.  
  9.     this.state = { todo: [] };
  10.   }
  11.  
  12.   componentDidMount() {
  13.     this.fetchTodos();
  14.   }
  15.  
  16.   fetchTodos() {
  17.     const result = alasql('SELECT * FROM todo');
  18.     this.setState({ todo: result });
  19.   }
  20.  
  21.   insertTodo(text) {
  22.     alasql('INSERT INTO todo VALUES ?',
  23.       [{ id: alasql.autoval('todo', 'id', true), text }]);
  24.   }
  25.  
  26.   deleteTodo(id) {
  27.     alasql('DELETE FROM todo WHERE id = ?', id);
  28.   }
  29.  
  30.   addTodo() {
  31.     const { inputTodo } = this.refs;
  32.     if (!inputTodo.value) return;
  33.     this.insertTodo(inputTodo.value);
  34.     this.fetchTodos();
  35.     inputTodo.value = "";
  36.   }
  37.  
  38.   removeTodo(id) {
  39.     this.deleteTodo(id);
  40.     this.fetchTodos();
  41.   }
  42.  
  43.   render() {
  44.     const { todo } = this.state;
  45.  
  46.     return (
  47.       <main className="container">
  48.         <h1 className="mt-4">TODO List</h1>
  49.         <div className="row mt-4">
  50.           <form className="form-inline">
  51.             <div className="form-group mx-sm-3 mb-2">
  52.               <label for="inputTodo" className="sr-only">Todo</label>
  53.               <input type="text" ref="inputTodo" className="form-control" id="inputTodo" placeholder="Todo"/>
  54.             </div>
  55.             <button type="button" className="btn btn-primary mb-2" onClick={ e => this.addTodo() }>Add</button>
  56.           </form>
  57.         </div>
  58.  
  59.         <div className="row">
  60.           <table className="table table-bordered">
  61.             <thead>
  62.               <tr>
  63.                 <th>ID</th>
  64.                 <th>TODO</th>
  65.                 <th></th>
  66.               </tr>
  67.             </thead>
  68.             <tbody>
  69.               {
  70.               !todo.length &&
  71.               <tr>
  72.                 <td colspan="3" className="text-center">
  73.                   No data available
  74.                 </td>
  75.               </tr>
  76.               }
  77.               {
  78.               todo.length > 0 && todo.map(x => (
  79.               <tr>
  80.                 <td>{ x.id }</td>
  81.                 <td>{ x.text }</td>
  82.                 <td>
  83.                   <button className="btn btn-danger" onClick={ e => this.removeTodo(x.id) }>
  84.                     x
  85.                   </button>
  86.                 </td>
  87.               </tr>
  88.               ))
  89.               }
  90.             </tbody>
  91.           </table>
  92.         </div>
  93.       </main>
  94.       );
  95.   }
  96. }
  97.  
  98. export default App;

Im Moment gehen alle TODOs verloren, wenn die Seite neu geladen wird. Das passiert, weil AlaSQL benutzt wird, um die Daten in einer Tabelle zu speichern. Es fehlt die Datenbank für die Persistierung der Tabelle. Dafür muss man die componentWillMount Methode ändern:

  1. componentWillMount() {
  2.     alasql(`
  3.       CREATE LOCALSTORAGE DATABASE IF NOT EXISTS todo_db;
  4.       ATTACH LOCALSTORAGE DATABASE todo_db;
  5.       USE todo_db;
  6.     `);
  7.     alasql('CREATE TABLE IF NOT EXISTS todo (id INT AUTOINCREMENT PRIMARY KEY, text STRING)');
  8. }

Durch diese Änderung erzeugt die componentWillMount Methode eine Datenbank mit Namen todo_db, falls sie nicht bereits existiert; sie wird für alle weiteren Datenbank-Geschehnisse benutzt. Selbstverständlich wird der localStorage als Datenbank-Engine verwendet.

Fazit

AlaSQL als leichte und schnelle Client-seitige Datenbank erlaubt uns die Umsetzung von Use-Cases, in denen Performanz eine Rolle spielt und in denen wir eine leichte Verwaltung von Apps und Services suchen. Die Flexibilität der Speicherstrukturen, im Sinne von relationalen sowie NoSQL-Daten, erweitert die möglichen Anwendungsszenarien und erlaubt die Gestaltung von innovativen Webapps.

Neuen Kommentar schreiben

Public Comment form

  • Zulässige HTML-Tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd><p><h1><h2><h3>

Plain text

  • Keine HTML-Tags erlaubt.
  • Internet- und E-Mail-Adressen werden automatisch umgewandelt.
  • HTML - Zeilenumbrüche und Absätze werden automatisch erzeugt.

ME Landing Page Question