Como funciona?
Sua aplicação utilizará uma versão modificada da biblioteca SQLite, contendo o código do LiteSync, para acessar o banco de dados.
As modificações na biblioteca do SQLite são internas e a interface é a mesma.
As bibliotecas do LiteSync comunicarão umas com as outras trocando dados de transação.

Replicação
Na primeira vez que a aplicação for aberta, ela conectará com outro(s) nó(s) e baixará uma cópia limpa do banco de dados.
Em uma topologia centralizada, o nó primário enviará a cópia do banco de dados para os nós secundários.
Assim que for baixado, o nó iniciará a sincronização.
Sincronização
Assim que os nós possuírem o mesmo banco de dados, eles compartilharão as transações que foram executadas quando eles estavam off-line.
Logo que estiverem em modo on-line e uma transação for executada em um nó, ela será transferida para ser executada em nós conectados.
Se um nó estiver off-line, a transação será armazenada em um log local para ser compartilhada posteriormente.
EU PRECISO ALTERAR O CÓDIGO DE MINHA APLICAÇÃO?
Exitem alguns passos, mas devemos alterar basicamente a URI de abertura do banco de dados, do cógido abaixo:
"file:/path/to/app.db"
para algo como:
"file:/path/to/app.db?node=secondary&connect=tcp://server.ip:1234"
A boa notícia é que o LiteSync utiliza a interface nativa do SQLite3. Isso significa que não precisaremos utilizar outra API.
Conexão
Cada nó tem 2 opções:
ligar com um endereço
conectar com o endereço de um ponto
Assim, você poderá escolher qual ponto se conectará a outro. Isso é útil quando um deles está atrás de um roteador ou de um firewall.
Topologias Suportadas
Centralizada, Topologia Estrela

Nessa topologia, temos um nó com o qual todos os outros nós se conectarão, então ele precisa estar on-line para que a sincronização aconteça.
Aqui estão alguns exemplos de configuração:
O nó primário pode ligar-se a um endereço e um nó secundário conectar-se a ele.
Nó primário:
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234"
Nó secundário: (em outro dispositivo)
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
O nó primário pode também conectar-se a nós secundários.
Nó primário:
"file:/home/user/app.db?node=primary&connect=tcp://address1:port1,tcp://address2:port2"
Nós secundários: (cada um em um dispositivo separado)
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
Podemos também utilizar um misto das 2 opções.
Nó primário:
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234&connect=tcp://address1:port1"
Nó secundário 1:
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
Nó secundário 2:
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
Status de Sincronização
Podemos checar o status de sincronização utilizando o seguinte comando:
PRAGMA sync_status
Ele retorna uma string JSON.
Checando se o banco de dados está pronto
Se a aplicação está sendo aberta pela primeira vez em um determinado dispositivo, ela pode baixar uma cópia do banco de dados de outro dispositivo. Até que essa ação seja realizada, não podemos acessar o banco de dados.
Podemos recuperar o status de sincronização e checar a variável db_is_ready.
Verifique os exemplos básicos de aplicações abaixo:
Como utilizar em minha aplicação?
Existem 3 passos:
1 Substitua a biblioteca SQLite com uma contendo o LiteSync
2 Altere a URI de conexão
3 Verifique pelo status ready do banco de dados
Quando compilar aplicações em C e C++, você precisa ligá-las à biblioteca LiteSync.
Para outras linguagens, você precisar ter o wrapper apropriado instalado.
Exemplo de nó primário
O nó primário pode ser uma aplicação normal, exatamente a mesma dos nós secundários, mas utilizando uma URI diferente.
Ou podemos utilizar uma aplicação dedicada ao nó primário.
Uma aplicação isolada, utilizada apenas com o propósito de manter um nó de banco de dados, seria como no exemplo a seguir:
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) } }
Exemplo básico de aplicação
Uma aplicação básica, que escreve em um banco de dados local, seria como no exemplo a seguir:
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) }
SEGURANÇA
O LiteSync utiliza o método de "segredo compartilhado" para controlar quais nós podem fazer parte da rede, através de criptografia com uma chave secreta
É possível (e recomendado) habilitar a criptografia no banco de dados e na comunicação entre os nós
Confira as instruções sobre Criptografia
LIMITAÇÕES ATUAIS
1 Funções não determinísticas (que retornam valores diferentes cada vez que são chamadas) são bloqueadas, como random() e date('now'). Use valores explícitos gerados no seu aplicativo
2 A palavra-chave AUTOINCREMENT não é suportada - mas você não precisa dela! (confira o vídeo para detalhes)
3 Apenas uma única aplicação pode acessar o banco de dados ao mesmo tempo. Cada instância deve usar seu próprio banco de dados, e então eles serão replicados e sincronizados usando LiteSync