"إذا أراد العامل أن يؤدي عمله بشكل جيد، فعليه أولاً أن يشحذ أدواته." - كونفوشيوس، "مختارات كونفوشيوس. لو لينجونج"
الصفحة الأمامية > برمجة > تمرين الترميز: أداة ترحيل قاعدة البيانات في Nodejs

تمرين الترميز: أداة ترحيل قاعدة البيانات في Nodejs

تم النشر بتاريخ 2024-11-04
تصفح:860

Coding exercise: database migration tool in nodejs

متطلبات

أريد الحصول على أداة ترحيل قاعدة البيانات، التي تحتوي على الخصائص التالية:

  1. تتم كتابة كل عملية ترحيل في ملف SQL واحد، مما يعني الجزأين "أعلى" و"أسفل". سيسمح هذا لـ Copilot بملء عملية ترحيل التراجع. وحقيقة أنها SQL مجردة تجعلها أيضًا الحل الأكثر مرونة ودعمًا.
  2. يجب إدارة الإصدار المطبق حاليًا بواسطة الأداة. أريد أن تكون الأداة مكتفية ذاتيًا.
  3. أريد أن تدعم الأداة قواعد بيانات مختلفة، مثل Postgres، وMySQL، وSQL Server، وما إلى ذلك، لذا يجب أن تكون قابلة للتوسيع بهذا المعنى.
  4. لا أريد أن يكون حجمه كبيرًا، لذا يجب تثبيت برامج التشغيل لقاعدة البيانات الضرورية فقط، ويفضل عند الطلب.
  5. أريد أن يكون جزءًا من نظام جافا سكريبت البيئي نظرًا لأن معظم المشاريع التي أعمل عليها هي جزء منه.
  6. يجب تنفيذ كل عملية ترحيل داخل المعاملة.

مقدمة

نشأت الكثير من هذه النقاط من تجربتي مع هذه الأداة الرائعة التي تسمى tern. كنت حزينًا لأن جافا سكريبت ليس لديه نفس الشيء! (أو ربما أسيء استخدام Google...). لذلك قررت أن هذا يمكن أن يكون تدريبًا جيدًا على البرمجة لنفسي وقصة قد تكون مثيرة للاهتمام لشخص آخر :)

تطوير

الجزء 1. تصميم الأداة

دعونا نسرق تصميم أداة CLI!

  1. ستحتوي جميع عمليات الترحيل على نظام التسمية التالي: _.sql، حيث يمثل الرقم رقم إصدار الترحيل، على سبيل المثال، 001_initial_setup.sql.
  2. ستكون جميع عمليات الترحيل موجودة في دير واحد.
  3. سيتم تنزيل برنامج تشغيل قاعدة البيانات عند الطلب، إما بعض الحزم المجمعة مسبقًا أو مجرد إصدار نوع من تثبيت npm .

لذلك سيكون بناء جملة الأداة كما يلي: martlet up --database-url --driver --dir

أو martlet down .

حيث يجب أن يطبق "up" جميع عمليات الترحيل التي لم يتم تطبيقها بعد، ويجب أن يتراجع "down" إلى الإصدار المحدد.
الخيارات لها المعنى والافتراضيات التالية:

  • database-url - سلسلة اتصال لقاعدة البيانات، الافتراضي هو البحث عن متغير env DATABASE_URL
  • driver - برنامج تشغيل قاعدة البيانات المراد استخدامه. بالنسبة للإصدار الأول، سأدعم Postgres فقط مع خيار يسمى "pg".
  • dir - الدليل الذي توجد فيه عمليات الترحيل، الافتراضي هو عمليات الترحيل

كما ترون، لقد بدأت بمعرفة كيفية استدعاء الأداة قبل كتابة أي كود فعلي. هذه ممارسة جيدة، فهي تساعد على تحقيق المتطلبات وتقليل دورات التطوير.

الجزء 2. التنفيذ

2.1 خيارات التحليل

حسنًا، أول الأشياء أولاً! لنقم بإنشاء ملف Index.js وإخراج رسالة المساعدة. سيبدو الأمر كالتالي:

function printHelp() {
  console.log(
    "Usage: martlet up --driver  --dir  --database-url ",
  );
  console.log(
    "       martlet down  --driver  --dir  --database-url ",
  );
  console.log(
    "        is a number that specifies the version to migrate down to",
  );
  console.log("Options:");
  console.log('  --driver   Driver to use, default is "pg"');
  console.log('  --dir         Directory to use, default is "migrations"');
  console.log(
    "  --database-url  Database URL to use, default is DATABASE_URL environment variable",
  );
}

printHelp();

الآن سنقوم بتحليل الخيارات:

export function parseOptions(args) {
  const options = {
    dir: "migrations",
    driver: "pg",
    databaseUrl: process.env.DATABASE_URL,
  };
  for (let idx = 0; idx 



كما ترى، لا أستخدم أي مكتبة للتحليل؛ أنا فقط أكرر قائمة الوسائط وأعالج كل خيار. لذا، إذا كان لدي خيار منطقي، فسوف أقوم بإزاحة مؤشر التكرار بمقدار 1، وإذا كان لدي خيار ذو قيمة، فسوف أقوم بإزاحته بمقدار 2.

2.2 تنفيذ محول برنامج التشغيل

لدعم برامج تشغيل متعددة، نحتاج إلى واجهة عالمية للوصول إلى قاعدة البيانات؛ هنا كيف قد يبدو:

interface Adapter {
    connect(url: string): Promise;
    transact(query: (fn: (text) => Promise)): Promise;
    close(): Promise;
}

أعتقد أن الاتصال والإغلاق وظيفتان واضحتان جدًا، واسمحوا لي أن أشرح طريقة التعامل. يجب أن تقبل دالة سيتم استدعاؤها بوظيفة تقبل نص استعلام وترجع وعدًا بنتيجة متوسطة. يتطلب هذا التعقيد وجود واجهة عامة توفر القدرة على تشغيل استعلامات متعددة داخل المعاملة. من الأسهل فهمه من خلال النظر إلى مثال الاستخدام.

إذًا هذه هي الطريقة التي يبحث بها المحول عن برنامج تشغيل postgres:

class PGAdapter {
  constructor(driver) {
    this.driver = driver;
  }

  async connect(url) {
    this.sql = this.driver(url);
  }

  async transact(query) {
    return this.sql.begin((sql) => (
      query((text) => sql.unsafe(text))
    ));
  }

  async close() {
    await this.sql.end();
  }
}

ومثال الاستخدام يمكن أن يكون:

import postgres from "postgres";

const adapter = new PGAdapter(postgres);
await adapter.connect(url);
await adapter.transact(async (sql) => {
    const rows = await sql("SELECT * FROM table1");
    await sql(`INSERT INTO table2 (id) VALUES (${rows[0].id})`);
});

2.3 تثبيت برنامج التشغيل عند الطلب

const PACKAGES = {
  pg: "[email protected]",
};

const downloadDriver = async (driver) => {
  const pkg = PACKAGES[driver];
  if (!pkg) {
    throw new Error(`Unknown driver: ${driver}`);
  }
  try {
    await stat(join(process.cwd(), "yarn.lock"));
    const lockfile = await readFile(join(process.cwd(), "yarn.lock"));
    const packagejson = await readFile(join(process.cwd(), "package.json"));
    spawnSync("yarn", ["add", pkg], {
      stdio: "inherit",
    });
    await writeFile(join(process.cwd(), "yarn.lock"), lockfile);
    await writeFile(join(process.cwd(), "package.json"), packagejson);
    return;
  } catch {}
  spawnSync("npm", ["install", "--no-save", "--legacy-peer-deps", pkg], {
    stdio: "inherit",
  });
};

نحاول تثبيت برنامج التشغيل باستخدام الغزل في البداية، لكننا لا نريد إنشاء أي اختلافات في الدليل، لذلك نحتفظ بملفات Yarn.lock وpackage.json. إذا لم يكن الغزل متاحًا، فسنعود إلى npm.

عندما نتأكد من تثبيت برنامج التشغيل، يمكننا إنشاء محول واستخدامه:

export async function loadAdapter(driver) {
  await downloadDriver(driver);
  return import(PACKAGES[driver].split("@")[0]).then(
    (m) => new PGAdapter(m.default),
  );

2.4 تنفيذ منطق الهجرة

نبدأ بالاتصال بقاعدة البيانات والحصول على الإصدار الحالي:

await adapter.connect(options.databaseUrl);
console.log("Connected to database");

const currentVersion = await adapter.transact(async (sql) => {
    await sql(`create table if not exists schema_migrations (
      version integer primary key
    )`);
    const result = await sql(`select version from schema_migrations limit 1`);
    return result[0]?.version || 0;
});

console.log(`Current version: ${currentVersion}`);

ثم نقرأ دليل الترحيلات ونصنفها حسب الإصدار. بعد ذلك، نقوم بتطبيق كل عملية ترحيل تحتوي على إصدار أكبر من الإصدار الحالي. سأقدم فقط الترحيل الفعلي في المقتطف التالي:

await adapter.transact(async (sql) => {
    await sql(upMigration);
    await sql(
      `insert into schema_migrations (version) values (${version})`
    );
    await sql(`delete from schema_migrations where version != ${version}`);
});

تشبه عملية الترحيل إلى الحالة السابقة، ولكننا نقوم بفرز عمليات الترحيل بترتيب عكسي ونطبقها حتى نصل إلى الإصدار المطلوب.

3. الاختبار

قررت عدم استخدام أي إطار اختبار محدد ولكن استخدام إمكانات اختبار العقدة المضمنة. وهي تشمل مشغل الاختبار وحزمة التأكيد.

import { it, before, after, describe } from "node:test";
import assert from "node:assert";

ولتنفيذ الاختبارات سأقوم بتشغيل العقدة --test --test-concurrency=1.

في الواقع، كنت أكتب الكود بطريقة TDD. لم أتحقق من أن كود الترحيل الخاص بي يعمل يدويًا، لكنني كنت أكتبه مع الاختبارات. ولهذا السبب قررت أن الاختبارات الشاملة ستكون الأنسب لهذه الأداة.
لمثل هذا النهج، ستحتاج الاختبارات إلى تمهيد قاعدة بيانات فارغة، وتطبيق بعض عمليات الترحيل، والتحقق من صحة محتويات قاعدة البيانات، ثم العودة إلى الحالة الأولية والتحقق من أن قاعدة البيانات فارغة.
لتشغيل قاعدة بيانات، استخدمت مكتبة "testcontainers"، والتي توفر غلافًا لطيفًا حول عامل الإرساء.

before(async () => {
    console.log("Starting container");
    container = await new GenericContainer("postgres:16-alpine")
    .withExposedPorts(5432)
    .withEnvironment({ POSTGRES_PASSWORD: "password" })
    .start();
});

after(async () => {
    await container.stop();
});

لقد كتبت بعض عمليات الترحيل البسيطة واختبرت أنها تعمل كما هو متوقع. فيما يلي مثال للتحقق من صحة حالة قاعدة البيانات:

const sql = pg(`postgres://postgres:password@localhost:${port}/postgres`);
const result = await sql`select * from schema_migrations`;
assert.deepEqual(result, [{ version: 2 }]);
const tables =
    await sql`select table_name from information_schema.tables where table_schema = 'public'`;
assert.deepEqual(tables, [
    { table_name: "schema_migrations" },
    { table_name: "test" },
]);

4. الاستنتاج

كان هذا مثالاً لكيفية التعامل مع تطوير أداة CLI بسيطة في نظام جافا سكريبت البيئي. أريد أن أشير إلى أن نظام جافا سكريبت البيئي الحديث مشحون وقوي للغاية، وتمكنت من تنفيذ الأداة مع الحد الأدنى من التبعيات الخارجية. لقد استخدمت برنامج تشغيل postgres الذي يمكن تنزيله عند الطلب وحاويات الاختبار للاختبارات. أعتقد أن هذا الأسلوب يمنح المطورين أكبر قدر من المرونة والتحكم في التطبيق.

5. المراجع

  • الريبو مارتليت
  • خرن
  • سائق بوستجرس
بيان الافراج تم نشر هذه المقالة على: https://dev.to/duskpoet/coding-exercise-database-migration-tool-in-nodejs-30pg?1 إذا كان هناك أي انتهاك، يرجى الاتصال بـ [email protected] لحذفه
أحدث البرنامج التعليمي أكثر>

تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.

Copyright© 2022 湘ICP备2022001581号-3