آلية العمل
يقوم التطبيق باستخدام إصدار معدّل من مكتبة SQLite حيث يحتوي على برمجية LiteSync للولوج إلى قاعدة البيانات.
تعديلات مكتبة SQLite هي تعديلات داخلية فقط، فبذلك تبقى الواجهة كما هي.
تقوم مكتبات LiteSync بالتواصل فيما بينها، متبادلة بذلك بيانات المناقلات.

النسخ
عند أوّل تشغيل، يقوم التطبيق بالاتصال بباقي العقد وتنزيل نسخة محدثة عن قاعدة البيانات
في الطوبولوجيا المركزية، تقوم العقدة الأساسية بإرسال نسخ عن قاعدة البيانات إلى بقية العقد.
بعد انتهاء التنزيل، تبدأ العقدة بالمزامنة
المزامنة
عند امتلاك جميع العقد لنفس قاعدة البيانات، تبدأ العقد بالقيام بمناقلات التبادل التي تم طلب تنفيذها أثناء وجود العقد في حالة عدم الاتصال
بعد ذلك، تدخل العقد في وضع الاتصال وحالما يبدأ تنفيذ مناقلة جديدة في عقدة ما، يتم نقل هذه المناقلة إلى باقي العقد المتصلة كيف تقوم بتنفيذها
إن لم تكن العقدة متصلة، يتم تخزين المناقلة على سجل محلي كي يتم إرساله في وقت لاحق.
هل أحتاج للقيام بتغيرات على كود تطبيقي؟
هنالك بعض الخطوات لكن بشكل أساسي، يتوجب تغيير سلسلة URI في بادئة قاعدة البيانات من هذا:
"file:/path/to/app.db"
إلى ما يماثل هذا:
"file:/path/to/app.db?node=secondary&connect=tcp://server.ip:1234"
لكن LiteSync يستخدم واجهة SQLite3 الأصلية؛ فلا حاجة لاستخدام API أخرى.
الاتصال
تملك كل عقدة خيارين:
الربط بعنوان
الاتصال بعنوان القرين
مما يمكنك من اختيار أحد الطرفين كي يتصل بالآخر. تكمن فائدة هذا في حال كان أحد الطرفين وراء موجه أو جدار ناري.
الطوبولوجيا المدعومة
الطوبولوجية النجمية المركزية

يكون ليدنا في هذه الطوبولجيا عقدة أساسية تتصل بها باقي العقد، لذلك يجب أن تكون هذه العقدة في ضع الاتصال لكي يتم التزامن بين العقد
أمثلة تكوين وفق الطوبولوجيا النجمية
يمكن للعقدة الأولية أن ترتبط بعنوان، وتقوم العقدة الثانوية بالاتصال بالعقدة الأولية
عقدة أولية:
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234"
عقدة ثانوية (على جهاز آخر):
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
كما يمكن لهذه العقدة الأولية الاتصال بعقد ثانوية أخرى
عقدة أولية
"file:/home/user/app.db?node=primary&connect=tcp://address1:port1,tcp://address2:port2"
عقد ثانوية (كل عقدة موجودة على جهاز):
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
كمان يمكن استخدام مزيج من الخيارين
عقدة أولية:
"file:/home/user/app.db?node=primary&bind=tcp://0.0.0.0:1234&connect=tcp://address1:port1"
العقدة الثانوية 1:
"file:/home/user/app.db?node=secondary&connect=tcp://server:1234"
العقدة الثانوية 2:
"file:/home/user/app.db?node=secondary&bind=tcp://0.0.0.0:1234"
حالة التزامن
يمكن التحقق من حالة التزامن باستخدام الأمر التالي:
PRAGMA sync_status
حيث يردّ بسلسلة محارف JSON.
التحقق من جهوزية قاعدة البيانات
بإمكان التطبيق تحميل نسخة جديدة عن قاعدة البيانات من عقدة أخرى في حال تشغيل التطبيق لأول مرّة على الجهاز. لكن لا يمكن الولوج إلى قاعدة البيانات حتى انتهاء هذه العملية.
يمكن الحصول على حالة التزامن و التحقق من المتحولdb_is_ready (الذي يدلّ على جهوزية قاعدة البيانات)
طالع هذه الأمثلة الأساسية
كيف أستطيع استخدامه في تطبيقاتي؟
تستطيع القيام بذلك عبر ثلاث خطوات:
1 استبدل مكتبة SQlite بتلك التي تحتوي LiteSync
2 قم بتغيير سلسلة اتصال URI
3 تحقق من جهوزية قاعدة البيانات
عند القيام بترجمة تطبيقات C وC++ تأكد من ربط تطبيقك مع مكتبة LiteSync
أما بالنسبة للغات الأخرى، تأكد من تثبيت الـwrapperالمناسب بكل لغة
نموذج لعقدة أولية
يمكن للعقدة الأولية أن تكون تطبيقاً، تماماً كتطبيق العقدة الثانوية لكن مع اختلاف سلسلة URI.
أو نقوم باستخدام تطبيق مخصص بالعقدة الأولية.
إذا أردنا استخدام تطبيق مستقل بغرض الحفاظ على عقدة قاعدة بيانات مركزية، سيكون التطبيق كالتالي:
اختر لغة -->
#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) } }
نموذج تطبيق بسيط
يكون التطبيق الذي يقوم بالكتابة إلى قاعدة بيانات محلية كالتالي:
اختر لغة -->
#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) }
الأمان
يستخدم LiteSync طريقة "السر المشترك" للتحكم في العقد التي يمكنها أن تكون جزءاً من الشبكة، وذلك عبر التشفير باستخدام مفتاح سري
من الممكن (والموصى به) تفعيل التشفير على قاعدة البيانات وعلى الاتصال بين العقد
راجع التعليمات حول التشفير
القيود الحالية
1 الدوال غير المحددة (التي تعيد قيمًا مختلفة في كل مرة يتم استدعاؤها) محظورة، مثل random() و date('now'). استخدم القيم المحددة التي تم إنشاؤها في تطبيقك
2 كلمة الأمر AUTOINCREMENT غير مدعومة - ولكنك لا تحتاج إليها! (راجع الفيديو للتفاصيل)
3 يمكن لتطبيق واحد فقط الوصول إلى قاعدة البيانات في نفس الوقت. يجب أن تستخدم كل نسخة قاعدة بيانات خاصة بها، ومن ثم سيتم تكرارها ومزامنتها باستخدام LiteSync