Kodokon kodokon.com

الاستمرارية باستخدام SQLite: طبقة الوصول إلى البيانات

استخدم better-sqlite3 مع العبارات المُحضَّرة والمعاملات ومستودع مخصص من أجل حفظ سريع وآمن.

9 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

يعمل SQLite داخل عمليتك: دون خادم، ودون شبكة، وبزمن استجابة لكل استعلام في حدود الميكروثانية. وهذا ما يجعل واجهة better-sqlite3 المتزامنة (synchronous) ليست مقبولة فحسب، بل غالبًا أسرع من مُشغِّل غير متزامن: فلا جدوى من دفع ثمن وعد (promise) ودورة في حلقة الأحداث من أجل قراءة أقصر من نبضة (tick). والمقايضة حقيقية عند الكتابة: فالاستعلام البطيء يُعطِّل الحلقة. أبقِ استعلاماتك مفهرسة وقصيرة، وسيتعامل SQLite مع عشرات الآلاف من عمليات القراءة في الثانية.

BASH
npm install better-sqlite3
وحدة أصلية (native): تُترجَم وقت التثبيت إذا لزم الأمر.

عند فتح قاعدة البيانات، هناك إعدادان يُحدِثان الفارق. يتيح وضع WAL (التسجيل المُسبق للكتابة) عمليات القراءة أثناء الكتابة بدلًا من قفل الملف بأكمله. ويُطبَّق المخطط (schema) بطريقة عديمة التأثير الجانبي (idempotent) باستخدام CREATE TABLE IF NOT EXISTS - وهو أكثر من كافٍ قبل أن تُدخِل أداة ترحيل (migration) حقيقية.

JAVASCRIPT
import Database from 'better-sqlite3';

export function createDb(path = 'app.db') {
  const db = new Database(path);
  db.pragma('journal_mode = WAL');
  db.exec(`CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL
  )`);
  return db;
}
connection.js: الفتح، والإعدادات، والمخطط عديم التأثير الجانبي.

إن طبقة الوصول إلى البيانات - المستودع (repository) - هي الوحدة الوحيدة التي تتحدث SQL. تُحضَّر الاستعلامات مرة واحدة عند الإنشاء ثم يُعاد استخدامها في كل استدعاء: فلا يُعيد المحرك تحليل نص SQL، وتمرّ القيم عبر العناصر النائبة (placeholders) ?، وليس أبدًا عبر الدمج (concatenation). إنها بالضبط الواجهة findAll وfindByEmail وinsert التي تتوقعها الخدمة من الدرس السابق.

JAVASCRIPT
export function createUserRepository(db) {
  const insertStmt = db.prepare(
    'INSERT INTO users (email, name) VALUES (?, ?)',
  );
  const byEmailStmt = db.prepare(
    'SELECT * FROM users WHERE email = ?',
  );
  const allStmt = db.prepare(
    'SELECT * FROM users ORDER BY id',
  );
  return {
    insert(user) {
      const info = insertStmt.run(user.email, user.name);
      return { id: info.lastInsertRowid, ...user };
    },
    findByEmail(email) {
      return byEmailStmt.get(email);
    },
    findAll() {
      return allStmt.all();
    },
  };
}
تُحضَّر مرة واحدة، وتُنفَّذ ألف مرة: run، وget، وall.
JAVASCRIPT
import { createDb } from './connection.js';

const db = createDb();
const insertStmt = db.prepare(
  'INSERT INTO users (email, name) VALUES (?, ?)',
);
const insertMany = db.transaction((users) => {
  for (const user of users) {
    insertStmt.run(user.email, user.name);
  }
});
insertMany([
  { email: 'ada@example.com', name: 'Ada' },
  { email: 'linus@example.com', name: 'Linus' },
]);
db.transaction: إما أن يُثبَّت كل شيء، أو يُتراجَع عن كل شيء.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. لماذا لا تُعدّ واجهة better-sqlite3 المتزامنة تناقضًا في Node؟
    • لأن Node يُشغِّل الوحدات الأصلية في خيط (thread) منفصل
    • لأن SQLite داخل العملية: فالاستعلام يكلّف أقل من التنسيق غير المتزامن الذي كان سيُتكبَّد لولا ذلك
    • لأن عمليات الكتابة تُصفّ تلقائيًا
    • لأن وضع WAL يجعل كل استعلام غير مُعطِّل
  2. ماذا تضمن العناصر النائبة ? في عبارة مُحضَّرة؟
    • تُمرَّر القيم بمعزل عن SQL ولا يمكن أبدًا تنفيذها كشفرة برمجية
    • تُشفَّر القيم قبل تخزينها
    • يُخزَّن الاستعلام تلقائيًا في ذاكرة مؤقتة على القرص
  3. ما الفائدة المزدوجة التي تجلبها db.transaction لألف عملية إدراج؟
    • ضغط البيانات وإزالة التكرار تلقائيًا
    • الذرّية (الكل أو لا شيء) ومزامنة قرص واحدة بدلًا من ألف
    • التحقق من المخطط وإنشاء الفهارس المفقودة