コーディング演習: Nodejs のデータベース移行ツール
要件
次の特性を持つデータベース移行ツールが必要です:
- すべての移行は単一の SQL ファイルに書かれており、これは「アップ」部分と「ダウン」部分の両方を意味します。これにより、Copilot がロールバック移行を実行できるようになります。また、これが裸の SQL であるという事実により、最も柔軟でサポートされているソリューションになります。
- 現在適用されているバージョンはツールで管理する必要があります。ツールを自立させたいです。
- このツールで Postgres、MySQL、SQL Server などのさまざまなデータベースをサポートしたいので、その意味で拡張可能である必要があります。
- サイズが大きくなりすぎないようにするため、必要なデータベースのドライバーのみを、理想的にはオンデマンドでインストールする必要があります。
- 私が取り組んでいるほとんどのプロジェクトは JavaScript エコシステムの一部であるため、これを JavaScript エコシステムの一部にしたいと考えています。
- すべての移行はトランザクション内で実行する必要があります。
導入
これらのポイントの多くは、tern と呼ばれるこの素晴らしいツールを使った私の経験から生まれました。 JavaScript には同じ機能がないのが残念でした。 (あるいは、私がグーグルするのが苦手なのかもしれません...)。そこで、これは自分自身にとっては素晴らしいコーディングの練習になるし、他の人にとっては興味深いストーリーになるかもしれないと判断しました:)
発達
パート 1. ツールの設計
CLI ツールの設計を盗みましょう!
- すべての移行には、
_ .sql という名前付けスキームが適用されます。ここで、番号は移行バージョン番号を表します (例: 001_initial_setup.sql)。 - すべての移行は単一のディレクトリに存在します。
- データベース ドライバーは、事前にバンドルされたパッケージか、ある種の npm install
を発行するだけで、オンデマンドでダウンロードされます。
ツールの構文は次のようになります。 martlet up --database-url
「上」はまだ適用されていないすべての移行を適用する必要があり、「下」は指定されたバージョンにロールバックする必要があります。
オプションの意味とデフォルトは次のとおりです:
- 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 サイトの他の関連記事を参照してください。

ホットAIツール

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

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

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

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

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

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

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

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

ホットトピック











JavaScriptは現代のWeb開発の基礎であり、その主な機能には、イベント駆動型のプログラミング、動的コンテンツ生成、非同期プログラミングが含まれます。 1)イベント駆動型プログラミングにより、Webページはユーザー操作に応じて動的に変更できます。 2)動的コンテンツ生成により、条件に応じてページコンテンツを調整できます。 3)非同期プログラミングにより、ユーザーインターフェイスがブロックされないようにします。 JavaScriptは、Webインタラクション、シングルページアプリケーション、サーバー側の開発で広く使用されており、ユーザーエクスペリエンスとクロスプラットフォーム開発の柔軟性を大幅に改善しています。

JavaScriptの最新トレンドには、TypeScriptの台頭、最新のフレームワークとライブラリの人気、WebAssemblyの適用が含まれます。将来の見通しは、より強力なタイプシステム、サーバー側のJavaScriptの開発、人工知能と機械学習の拡大、およびIoTおよびEDGEコンピューティングの可能性をカバーしています。

さまざまなJavaScriptエンジンは、各エンジンの実装原則と最適化戦略が異なるため、JavaScriptコードを解析および実行するときに異なる効果をもたらします。 1。語彙分析:ソースコードを語彙ユニットに変換します。 2。文法分析:抽象的な構文ツリーを生成します。 3。最適化とコンパイル:JITコンパイラを介してマシンコードを生成します。 4。実行:マシンコードを実行します。 V8エンジンはインスタントコンピレーションと非表示クラスを通じて最適化され、Spidermonkeyはタイプ推論システムを使用して、同じコードで異なるパフォーマンスパフォーマンスをもたらします。

JavaScriptは、現代のWeb開発のコア言語であり、その多様性と柔軟性に広く使用されています。 1)フロントエンド開発:DOM操作と最新のフレームワーク(React、Vue.JS、Angularなど)を通じて、動的なWebページとシングルページアプリケーションを構築します。 2)サーバー側の開発:node.jsは、非ブロッキングI/Oモデルを使用して、高い並行性とリアルタイムアプリケーションを処理します。 3)モバイルおよびデスクトップアプリケーション開発:クロスプラットフォーム開発は、反応および電子を通じて実現され、開発効率を向上させます。

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

この記事では、許可によって保護されたバックエンドとのフロントエンド統合を示し、next.jsを使用して機能的なedtech SaaSアプリケーションを構築します。 FrontEndはユーザーのアクセス許可を取得してUIの可視性を制御し、APIリクエストがロールベースに付着することを保証します

C/CからJavaScriptへのシフトには、動的なタイピング、ゴミ収集、非同期プログラミングへの適応が必要です。 1)C/Cは、手動メモリ管理を必要とする静的に型付けられた言語であり、JavaScriptは動的に型付けされ、ごみ収集が自動的に処理されます。 2)C/Cはマシンコードにコンパイルする必要がありますが、JavaScriptは解釈言語です。 3)JavaScriptは、閉鎖、プロトタイプチェーン、約束などの概念を導入します。これにより、柔軟性と非同期プログラミング機能が向上します。

私はあなたの日常的な技術ツールを使用して機能的なマルチテナントSaaSアプリケーション(EDTECHアプリ)を作成しましたが、あなたは同じことをすることができます。 まず、マルチテナントSaaSアプリケーションとは何ですか? マルチテナントSaaSアプリケーションを使用すると、Singの複数の顧客にサービスを提供できます
