Comment ça marche?
Votre application utilisera une version modifiée de la bibliothèque SQLite qui contient le code LiteSync pour accéder à votre base de données.
Les modifications sur la bibliothèque SQLite sont internes et l'interface est la même.
Les bibliothèques LiteSync communiqueront entre elles, échangeant des données de transaction.

Réplication
La première fois que l'application est ouverte, elle se connectera à l'autre nœud(s) et téléchargera une nouvelle copie de la base de données.
Dans une topologie centralisée, le nœud principal enverra la copie de la base de données aux nœuds secondaires.
Une fois téléchargé, le nœud démarrera la synchronisation.
Synchronisation
Une fois que les nœuds ont la même base de données, ils échangent des transactions qui ont été exécutées alors qu'elles étaient hors ligne.
Après cela, ils entrent en mode en ligne et une fois qu'une nouvelle transaction est exécutée dans un nœud, elle est transférée pour être exécutée dans les nœuds connectés.
Si le nœud n'est pas en ligne, la transaction est stockée dans un journal local pour être échangée ultérieurement.
EST-CE QUE J'AI BESOIN DE MODIFIER MON CODE D'APPLICATION ?
Il y a quelques étapes mais, fondamentalement, nous devons changer la chaîne URI dans l'ouverture de la base de données à partir de ce-ci :
"file:/path/to/app.db"
à quelque chose comme cette-ci :
"file:/path/to/app.db?node=secondary&connect=tcp://server.ip:1234"
La bonne nouvelle est que LiteSync utilise l'interface native SQLite3. Cela signifie que nous n'avons pas besoin d'utiliser une autre API.
Connexion
Il y a 2 options pour chaque nœud :
lier à une adresse
connecter à l'adresse du pair
Vous pouvez donc choisir quel côté se connectera à l'autre. Ceci est utile lorsqu'un côté se trouve derrière un routeur ou un pare-feu.
Topologies prises en charge
Topologie en étoile centralisée

Dans cette topologie, nous avons un nœud auquel tous les autres nœuds seront connectés, il doit donc être en ligne pour que la synchronisation ait lieu.
Voici quelques exemples de configurations :
Le nœud principal peut se lier à une adresse et les nœuds secondaires s'y connectent.
Nœud principal :
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234"
Nœud secondaire : (dans un autre appareil)
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
Le nœud principal peut également se connecter aux nœuds secondaires.
Nœud principal :
"file:/home/user/app.db?node=primary&connect=tcp://address1:port1,tcp://address2:port2"
Nœuds secondaires : (chacun sur un appareil séparé)
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
Nous pouvons même utiliser un mélange de ces 2 options.
Nœud principal :
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234&connect=tcp://address1:port1"
Nœud secondaire 1:
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
Nœud secondaire 2:
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
État de synchronisation
Nous pouvons vérifier l'état de synchronisation à l'aide de cette commande :
PRAGMA sync_status
Il renvoie une chaîne JSON.
Vérifier si la base de données est prête
Si l'application est ouverte pour la première fois sur un appareil, elle peut télécharger une nouvelle copie de la base de données à partir d'un autre nœud. Tant que ce n'est pas fait, nous ne pouvons pas accéder à la base de données.
Nous pouvons récupérer l'état de synchronisation et vérifier la variable db_is_ready.
Consultez les exemples d'application de base ci-dessous.
Comment l'utiliser dans mon application ?
Il y a 3 étapes :
1 Remplacer la bibliothèque SQLite par celle contenant LiteSync
2 Modifier la chaîne de connexion URI
3 Vérifier l'état de la base de données prête
Lors de la compilation des applications C et C ++, vous devez lier votre application à la bibliothèque LiteSync.
Pour les autres langues, vous devez avoir le wrapper approprié installé.
Exemple de nœud principal
Le nœud principal peut être une application normale, exactement la même application que les nœuds secondaires mais en utilisant un URI différent.
Ou nous pouvons utiliser une application dédiée pour être le nœud principal.
Une application autonome de base utilisée uniquement dans le but de conserver un nœud de base de données centralisé ressemblerait à ce-ci :
Select a language -->
#include <sqlite3.h> char *uri = "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"; int main() { sqlite3 *db; sqlite3_open(uri, &db); /* open the database */ while(1) sleep(1); /* keep the app open */ }
#include <sqlite_modern_cpp.h> #include <thread> #include <chrono> #include <iostream> using namespace sqlite; int main() { try { // open the database database db("file:app.db?node=primary&bind=tcp://0.0.0.0:1234"); // keep the app open while(1) { std::this_thread::sleep_for(std::chrono::seconds(1)); } } catch (std::exception& e) { std::cerr << e.what() << std::endl; } }
import litesync as sqlite3 conn = sqlite3.connect('file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234') # keep the app open import time while True: time.sleep(60) # in seconds
const uri = 'file:app.db?node=primary&bind=tcp://0.0.0.0:1234'; const options = { verbose: console.log }; const db = require('better-sqlite3-litesync')(uri, options); // keep the app open setInterval(function(){}, 5000);
import java.sql.Connection; import java.sql.DriverManager; public class Sample { public static void main(String[] args) { String uri = "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"; Connection connection = DriverManager.getConnection("jdbc:sqlite:" + uri); // keep the app open while (true) { Thread.sleep(5000); } } }
using SQLite; public class Program { public static void Main() { // open the database var uri = "file:app.db?node=primary&bind=tcp://0.0.0.0:1234"; var db = new SQLiteConnection(uri); // keep the app open while(true) { System.Threading.Thread.Sleep(5000); } } }
Imports SQLite Public Class Program Public Shared Sub Main() ' open the database Dim db As New SQLiteConnection("file:app.db?node=primary&bind=tcp://0.0.0.0:1234") ' keep the app open Do System.Threading.Thread.Sleep(5000) Loop End Sub End Class
Option Explicit Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long) Public Sub Main() Dim URI As String Dim Conn As New ADODB.Connection ' Open the connection URI = "file:C:\app\mydb.db?node=primary&bind=tcp://0.0.0.0:1234" Conn.Open "DRIVER=SQLite3 ODBC Driver;Database=" & URI ' Keep the app open Do: Sleep(5000): Loop End Sub
<?php // with sqlite3: $db = new SQLite3("file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"); // with pdo_sqlite: $pdo = new PDO("sqlite:file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"); // keep the app open - it should not be used with apache while(1) sleep(5); ?>
use DBI; my $dbh = DBI->connect("dbi:SQLite:uri=file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234"); // keep the app open - it should not be used with apache sleep;
require 'sqlite3' db = SQLite3::Database.new "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234" # keep the app open loop do sleep(1) end
local sqlite3 = require("lsqlite3") local db = sqlite3.open('file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234') -- keep the app open local lsocket = require("lsocket") while true do lsocket.select(5000) end
package main import ( "database/sql" _ "github.com/litesync/go-sqlite3" "time" ) func main() { db, err := sql.Open("sqlite3", "file:/path/to/app.db?node=primary&bind=tcp://0.0.0.0:1234") // keep the app open for { time.Sleep(1000 * time.Millisecond) } }
Exemple d'application de base
Une application de base qui écrit dans la base de données locale ressemblerait à ceci :
Select a language -->
#include <sqlite3.h> char *uri = "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"; int main() { sqlite3 *db; /* open the database */ sqlite3_open(&db, uri); /* check if the db is ready */ while(1){ char *json_str = sqlite3_query_value_str(db, "PRAGMA sync_status", NULL); bool db_is_ready = strstr(json_str, "\"db_is_ready\": true") > 0; sqlite3_free(json_str); if (db_is_ready) break; sleep_ms(250); } /* access the database */ start_access(db); } char * sqlite3_query_value_str(sqlite3 *db, char *sql, char **ppErrMsg) { char *ptr = NULL; sqlite3_stmt *stmt; int rc; if (ppErrMsg) *ppErrMsg = NULL; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { if (ppErrMsg) *ppErrMsg = sqlite3_strdup(sqlite3_errmsg(db)); return NULL; } if (sqlite3_step(stmt) == SQLITE_ROW) { char *text = (char *)sqlite3_column_text(stmt, 0); if (text) { ptr = sqlite3_strdup(text); } } sqlite3_finalize(stmt); return ptr; }
#include <iostream> #include <sqlite_modern_cpp.h> #include <unistd.h> using namespace sqlite; using namespace std; int main() { try { // open the database database db("file:app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // wait until the database is ready while(1) { string status; db << "pragma sync_status" >> status; cout << "status : " << status << endl; if (status.find("\"db_is_ready\": true") != string::npos) break; sleep(1); } // now the application can access the database // check examples here: // https://github.com/SqliteModernCpp/sqlite_modern_cpp ... } catch (exception& e) { cout << e.what() << endl; } }
import litesync as sqlite3 import json import time conn = sqlite3.connect('file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234') # check if the db is ready while not conn.is_ready(): time.sleep(0.250) start_access(conn)
const uri = 'file:test.db?node=secondary&connect=tcp://127.0.0.1:1234'; const options = { verbose: console.log }; const db = require('better-sqlite3-litesync')(uri, options); db.on('ready', function() { // the database is ready to be accessed db.exec('CREATE TABLE IF NOT EXISTS users (name, email)'); ... });
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import org.json.*; public class Sample { public static void main(String[] args) { String uri = "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"; Connection connection = DriverManager.getConnection("jdbc:sqlite:" + uri); Statement statement = connection.createStatement(); // check if the db is ready while (true) { ResultSet rs = statement.executeQuery("PRAGMA sync_status"); rs.next(); JSONObject obj = new JSONObject(rs.getString(1)); if (obj.getBoolean("db_is_ready")) break; Thread.sleep(250); } // now we can access the db start_access(connection); } }
using SQLite; public class Program { public static void Main() { // open the database var uri = "file:app.db?node=secondary&connect=tcp://server:port"; var db = new SQLiteConnection(uri); // wait until the db is ready while (!db.IsReady()) { System.Threading.Thread.Sleep(250); } // now we can use the database db.CreateTable<TodoItem>(CreateFlags.AutoIncPK); ... } }
Imports SQLite Public Class Program Public Shared Sub Main() ' open the database Dim db As New SQLiteConnection("file:app.db?node=secondary&connect=tcp://server:port") ' wait until the db is ready While Not db.IsReady() System.Threading.Thread.Sleep(250) End While ' now we can use the database db.CreateTable(Of TodoItem)(CreateFlags.AutoIncPK) ' ... End Sub End Class
Option Explicit Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long) Public Sub Main() Dim Conn As New ADODB.Connection Dim Rst As ADODB.Recordset Dim URI As String URI = "file:C:\app\mydb.db?node=secondary&connect=tcp://myserver.ddns.net:1234" Conn.Open "DRIVER=SQLite3 ODBC Driver;Database=" & URI ' Check if the database is ready Do Set Rst = New ADODB.Recordset Rst.Open "PRAGMA sync_status", Conn, , , adCmdText If InStr(Rst!sync_status, """db_is_ready"": true") > 0 Then Exit Do Sleep 200 Loop ' Now we can access the db StartDbAccess(Conn) End Sub
<?php // with sqlite3: $db = new SQLite3("file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // with pdo_sqlite: $pdo = new PDO("sqlite:file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // check if the db is ready while(1) { $results = $db->query('PRAGMA sync_status'); $row = $results->fetchArray(); $status = json_decode($row[0], true); if ($status['db_is_ready'] == true) break; sleep(0.25); } // now we can access the db start_access($db); ?>
use DBI; use JSON qw( decode_json ); my $dbh = DBI->connect("dbi:SQLite:uri=file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234"); // check if the db is ready - it should not be used with apache while (1) { my ($result) = $dbh->selectrow_array("PRAGMA sync_status"); my $status = decode_json($result); if ($status->{'db_is_ready'}) last; sleep; } // now we can access the db ...
require 'sqlite3' require 'json' db = SQLite3::Database.new "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234" # check if the db is ready loop do result = db.get_first_value "PRAGMA sync_status" status = JSON.parse(result) break if status["db_is_ready"] == true sleep 0.25 end # now we can access the db start_access(db)
local sqlite3 = require "lsqlite3" local json = require "json" local db = sqlite3.open('file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234') -- check if the db is ready local lsocket = require("lsocket") while true do local result = db:rows("PRAGMA sync_status") local status = json:decode(result[0]) if status["db_is_ready"] == true then break end lsocket.select(250) end -- now we can access the db start_access(db)
package main import ( "database/sql" _ "github.com/litesync/go-sqlite3" "time" ) func main() { db, err := sql.Open("sqlite3", "file:/path/to/app.db?node=secondary&connect=tcp://myserver.ddns.net:1234") // wait until the db is ready for !db.IsReady() { time.Sleep(1000 * time.Millisecond) } // now we can access the db start_access(db) }
SÉCURITÉ
LiteSync utilise la méthode du "secret partagé" pour contrôler quels nœuds peuvent faire partie du réseau, via le chiffrement avec une clé secrète
Il est possible (et recommandé) d'activer le chiffrement sur la base de données et sur la communication entre les nœuds
Consultez les instructions sur le Chiffrement
LIMITATIONS ACTUELLES
1 Les fonctions non déterministes (qui renvoient des valeurs différentes à chaque fois qu'elles sont appelées) sont bloquées, comme random() et date('now'). Utilisez des valeurs explicites générées sur votre application
2 Le mot-clé AUTOINCREMENT n'est pas pris en charge - mais vous n'en avez pas besoin ! (consultez la vidéo pour plus de détails)
3 Seule une seule application peut accéder à la base de données en même temps. Chaque instance doit utiliser sa propre base de données, et elles seront ensuite répliquées et synchronisées en utilisant LiteSync