ホームページ ウェブフロントエンド jsチュートリアル コーディング演習: Nodejs のデータベース移行ツール

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

Sep 25, 2024 pm 08:18 PM

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 --ドライバー --dir

;またはマートレットダウン <同じオプション>

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

  • database-url - データベースの接続文字列。デフォルトでは環境変数 DATABASE_URL を検索します。
  • driver - 使用するデータベースドライバー。最初のバージョンでは、「pg」という名前のオプションを使用して Postgres のみをサポートします。
  • dir - マイグレーションが存在するディレクトリ、デフォルトはマイグレーションです

ご覧のとおり、実際のコードを記述する前に、ツールを呼び出す方法を考えることから始めました。これは良い習慣であり、要件を実現し、開発サイクルを短縮するのに役立ちます。

パート 2. 実装

2.1 解析オプション

それでは、まず最初に! Index.js ファイルを作成し、ヘルプメッセージを出力してみましょう。次のようになります:

function printHelp() {
  console.log(
    "Usage: martlet up --driver <driver> --dir <dir> --database-url <url>",
  );
  console.log(
    "       martlet down <version> --driver <driver> --dir <dir> --database-url <url>",
  );
  console.log(
    "       <version> is a number that specifies the version to migrate down to",
  );
  console.log("Options:");
  console.log('  --driver <driver>  Driver to use, default is "pg"');
  console.log('  --dir <dir>        Directory to use, default is "migrations"');
  console.log(
    "  --database-url <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 < args.length; ) {
    switch (args[idx]) {
      case "--help":
      case "-h": {
        printHelp();
        process.exit(0);
      }
      case "--dir": {
        options.dir = args[idx + 1];
        idx += 2;
        break;
      }
      case "--driver": {
        options.driver = args[idx + 1];
        idx += 2;
        break;
      }
      case "--database-url": {
        options.databaseUrl = args[idx + 1];
        idx += 2;
        break;
      }

      default: {
        console.error(`Unknown option: ${args[idx]}`);
        printHelp();
        process.exit(1);
      }
    }
  }
  return options;
}
ログイン後にコピー

ご覧のとおり、私は解析にライブラリを使用していません。単に引数リストを反復処理して、すべてのオプションを処理するだけです。したがって、ブール値オプションがある場合は反復インデックスを 1 シフトし、値を持つオプションがある場合は 2 シフトします。

2.2 ドライバーアダプターの実装

複数のドライバーをサポートするには、データベースにアクセスするためのユニバーサル インターフェイスが必要です。以下にその様子を示します:

interface Adapter {
    connect(url: string): Promise<void>;
    transact(query: (fn: (text) => Promise<ResultSet>)): Promise<ResultSet>;
    close(): Promise<void>;
}
ログイン後にコピー

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: "postgres@3.4.4",
};

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}`);
ログイン後にコピー

Then, we read the migrations directory and sort them by version. After that, we apply every migration that has a version greater than the current one. I will just present the actual migration in the following snippet:

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}`);
});
ログイン後にコピー

The rollback migration is similar, but we sort the migrations in reverse order and apply them until we reach the desired version.

3. Testing

I decided not to use any specific testing framework but use the built-in nodejs testing capabilities. They include the test runner and the assertion package.

import { it, before, after, describe } from "node:test";
import assert from "node:assert";
ログイン後にコピー

And to execute tests I would run node --test --test-concurrency=1.

Actually, I was writing the code in a sort of TDD manner. I didn't validate that my migrations code worked by hand, but I was writing it along with tests. That's why I decided that end-to-end tests would be the best fit for this tool.
For such an approach, tests would need to bootstrap an empty database, apply some migrations, check that database contents are correct, and then roll back to the initial state and validate that the database is empty.
To run a database, I used the "testcontainers" library, which provides a nice wrapper around docker.

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();
});
ログイン後にコピー

I wrote some simple migrations and tested that they worked as expected. Here is an example of a database state validation:

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. Conclusion

This was an example of how I would approach the development of a simple CLI tool in the javascript ecosystem. I want to note that the modern javascript ecosystem is pretty charged and powerful, and I managed to implement the tool with a minimum of external dependencies. I used a postgres driver that would be downloaded on demand and testcontainers for tests. I think that approach gives developers the most flexibility and control over the application.

5. References

  • martlet repo
  • tern
  • postgres driver

以上がコーディング演習: Nodejs のデータベース移行ツールの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

Python vs. JavaScript:学習曲線と使いやすさ Python vs. JavaScript:学習曲線と使いやすさ Apr 16, 2025 am 12:12 AM

Pythonは、スムーズな学習曲線と簡潔な構文を備えた初心者により適しています。 JavaScriptは、急な学習曲線と柔軟な構文を備えたフロントエンド開発に適しています。 1。Python構文は直感的で、データサイエンスやバックエンド開発に適しています。 2。JavaScriptは柔軟で、フロントエンドおよびサーバー側のプログラミングで広く使用されています。

JavaScriptとWeb:コア機能とユースケース JavaScriptとWeb:コア機能とユースケース Apr 18, 2025 am 12:19 AM

Web開発におけるJavaScriptの主な用途には、クライアントの相互作用、フォーム検証、非同期通信が含まれます。 1)DOM操作による動的なコンテンツの更新とユーザーインタラクション。 2)ユーザーエクスペリエンスを改善するためにデータを提出する前に、クライアントの検証が実行されます。 3)サーバーとのリフレッシュレス通信は、AJAXテクノロジーを通じて達成されます。

JavaScript in Action:実際の例とプロジェクト JavaScript in Action:実際の例とプロジェクト Apr 19, 2025 am 12:13 AM

現実世界でのJavaScriptのアプリケーションには、フロントエンドとバックエンドの開発が含まれます。 1)DOM操作とイベント処理を含むTODOリストアプリケーションを構築して、フロントエンドアプリケーションを表示します。 2)node.jsを介してRestfulapiを構築し、バックエンドアプリケーションをデモンストレーションします。

JavaScriptエンジンの理解:実装の詳細 JavaScriptエンジンの理解:実装の詳細 Apr 17, 2025 am 12:05 AM

JavaScriptエンジンが内部的にどのように機能するかを理解することは、開発者にとってより効率的なコードの作成とパフォーマンスのボトルネックと最適化戦略の理解に役立つためです。 1)エンジンのワークフローには、3つの段階が含まれます。解析、コンパイル、実行。 2)実行プロセス中、エンジンはインラインキャッシュや非表示クラスなどの動的最適化を実行します。 3)ベストプラクティスには、グローバル変数の避け、ループの最適化、constとletsの使用、閉鎖の過度の使用の回避が含まれます。

Python vs. JavaScript:コミュニティ、ライブラリ、リソース Python vs. JavaScript:コミュニティ、ライブラリ、リソース Apr 15, 2025 am 12:16 AM

PythonとJavaScriptには、コミュニティ、ライブラリ、リソースの観点から、独自の利点と短所があります。 1)Pythonコミュニティはフレンドリーで初心者に適していますが、フロントエンドの開発リソースはJavaScriptほど豊富ではありません。 2)Pythonはデータサイエンスおよび機械学習ライブラリで強力ですが、JavaScriptはフロントエンド開発ライブラリとフレームワークで優れています。 3)どちらも豊富な学習リソースを持っていますが、Pythonは公式文書から始めるのに適していますが、JavaScriptはMDNWebDocsにより優れています。選択は、プロジェクトのニーズと個人的な関心に基づいている必要があります。

Python vs. JavaScript:開発環境とツール Python vs. JavaScript:開発環境とツール Apr 26, 2025 am 12:09 AM

開発環境におけるPythonとJavaScriptの両方の選択が重要です。 1)Pythonの開発環境には、Pycharm、Jupyternotebook、Anacondaが含まれます。これらは、データサイエンスと迅速なプロトタイピングに適しています。 2)JavaScriptの開発環境には、フロントエンドおよびバックエンド開発に適したnode.js、vscode、およびwebpackが含まれます。プロジェクトのニーズに応じて適切なツールを選択すると、開発効率とプロジェクトの成功率が向上する可能性があります。

JavaScript通訳者とコンパイラにおけるC/Cの役割 JavaScript通訳者とコンパイラにおけるC/Cの役割 Apr 20, 2025 am 12:01 AM

CとCは、主に通訳者とJITコンパイラを実装するために使用されるJavaScriptエンジンで重要な役割を果たします。 1)cは、JavaScriptソースコードを解析し、抽象的な構文ツリーを生成するために使用されます。 2)Cは、Bytecodeの生成と実行を担当します。 3)Cは、JITコンパイラを実装し、実行時にホットスポットコードを最適化およびコンパイルし、JavaScriptの実行効率を大幅に改善します。

Webサイトからアプリまで:JavaScriptの多様なアプリケーション Webサイトからアプリまで:JavaScriptの多様なアプリケーション Apr 22, 2025 am 12:02 AM

JavaScriptは、Webサイト、モバイルアプリケーション、デスクトップアプリケーション、サーバー側のプログラミングで広く使用されています。 1)Webサイト開発では、JavaScriptはHTMLおよびCSSと一緒にDOMを運用して、JQueryやReactなどのフレームワークをサポートします。 2)ReactNativeおよびIonicを通じて、JavaScriptはクロスプラットフォームモバイルアプリケーションを開発するために使用されます。 3)電子フレームワークにより、JavaScriptはデスクトップアプリケーションを構築できます。 4)node.jsを使用すると、JavaScriptがサーバー側で実行され、高い並行リクエストをサポートします。

See all articles