「労働者が自分の仕事をうまくやりたいなら、まず自分の道具を研ぎ澄まさなければなりません。」 - 孔子、「論語。陸霊公」
表紙 > プログラミング > コーディング演習: Nodejs のデータベース移行ツール

コーディング演習: Nodejs のデータベース移行ツール

2024 年 11 月 4 日に公開
ブラウズ:346

Coding exercise: database migration tool in nodejs

要件

次のプロパティを持つデータベース移行ツールが必要です:

  1. すべての移行は単一の SQL ファイルに記述されます。これは、「アップ」部分と「ダウン」部分の両方を意味します。これにより、Copilot がロールバック移行を実行できるようになります。また、これが裸の SQL であるという事実により、最も柔軟でサポートされているソリューションになります。
  2. 現在適用されているバージョンはツールで管理する必要があります。ツールを自立させたいのです。
  3. このツールで Postgres、MySQL、SQL Server などのさまざまなデータベースをサポートしたいので、その意味で拡張可能である必要があります。
  4. サイズが大きくなりすぎないようにするため、必要なデータベースのドライバーのみを、理想的にはオンデマンドでインストールする必要があります。
  5. 私が取り組んでいるほとんどのプロジェクトは JavaScript エコシステムの一部であるため、これを JavaScript エコシステムの一部にしたいと考えています。
  6. すべての移行はトランザクション内で実行する必要があります。

導入

これらのポイントの多くは、tern という素晴らしいツールを使った私の経験から生まれました。 JavaScript には同じ機能がないのが残念でした。 (あるいは、私がグーグル検索が苦手なのかもしれません...)。そこで、これは自分自身にとっては素晴らしいコーディングの練習になるし、他の人にとっては興味深いストーリーになるかもしれないと判断しました:)

発達

パート 1. ツールの設計

CLI ツールの設計を 盗み ましょう!

  1. すべての移行には次の命名スキームが適用されます: _.sql。ここで、番号は移行バージョン番号を表します (例: 001_initial_setup.sql)。
  2. すべての移行は単一のディレクトリに存在します。
  3. データベース ドライバーは、事前にバンドルされたパッケージか、ある種の npm install を発行するだけで、オンデマンドでダウンロードされます。

したがって、ツールの構文は次のようになります: martlet up --database-url --driver --dir

または martlet down .

「上」はまだ適用されていないすべての移行を適用する必要があり、「下」は指定されたバージョンにロールバックする必要があります。
オプションの意味とデフォルトは次のとおりです:

  • database-url - データベースの接続文字列。デフォルトでは環境変数 DATABASE_URL を検索します。
  • driver - 使用するデータベースドライバー。最初のバージョンでは、「pg」という名前のオプションを使用して Postgres のみをサポートします。
  • 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;
}

connect と close は非常に明白な関数だと思うので、transact メソッドについて説明しましょう。クエリ テキストを受け取り、中間結果を含む 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を使用してドライバーをインストールしようとしますが、ディレクトリ内に差分を生成したくないため、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}`);

次に、migrations ディレクトリを読み取り、バージョンごとに並べ替えます。その後、現在のバージョンよりも新しいバージョンを持つすべての移行を適用します。実際の移行を次のスニペットで示します:

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. テスト

特定のテスト フレームワークを使用せず、組み込みの Nodejs テスト機能を使用することにしました。これらには、テスト ランナーとアサーション パッケージが含まれます。

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

そしてテストを実行するには、node --test --test-concurrency=1.

を実行します。

実際、私は TDD のような方法でコードを書いていました。移行コードが手動で機能するかどうかは検証しませんでしたが、テストと一緒にコードを作成していました。だからこそ、エンドツーエンドのテストがこのツールに最適であると判断しました。
このようなアプローチの場合、テストでは空のデータベースをブートストラップし、いくつかの移行を適用し、データベースの内容が正しいことを確認してから、初期状態にロールバックしてデータベースが空であることを検証する必要があります。
データベースを実行するために、docker の優れたラッパーを提供する「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. 結論

これは、JavaScript エコシステムでのシンプルな CLI ツールの開発にどのようにアプローチするかを示す一例でした。最新の JavaScript エコシステムはかなり有料で強力であり、外部依存関係を最小限に抑えてツールを実装できたことに注意してください。オンデマンドでダウンロードされる postgres ドライバーと、テスト用の testcontainer を使用しました。このアプローチにより、開発者はアプリケーションを最も柔軟に制御できるようになります。

5. 参考文献

  • マートレット リポジトリ
  • ターン
  • postgres ドライバー
リリースステートメント この記事は次の場所に転載されています: https://dev.to/duskpoet/coding-exercise-database-migration-tool-in-nodejs-30pg?1 侵害がある場合は、[email protected] に連絡して削除してください。
最新のチュートリアル もっと>

免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。

Copyright© 2022 湘ICP备2022001581号-3