次のプロパティを持つデータベース移行ツールが必要です:
これらのポイントの多くは、tern という素晴らしいツールを使った私の経験から生まれました。 JavaScript には同じ機能がないのが残念でした。 (あるいは、私がグーグル検索が苦手なのかもしれません...)。そこで、これは自分自身にとっては素晴らしいコーディングの練習になるし、他の人にとっては興味深いストーリーになるかもしれないと判断しました:)
CLI ツールの設計を 盗み ましょう!
したがって、ツールの構文は次のようになります: martlet up --database-url
「上」はまだ適用されていないすべての移行を適用する必要があり、「下」は指定されたバージョンにロールバックする必要があります。
オプションの意味とデフォルトは次のとおりです:
ご覧のとおり、実際のコードを記述する前に、ツールを呼び出す方法を考えることから始めました。これは良い習慣であり、要件を実現し、開発サイクルを短縮するのに役立ちます。
それでは、まず最初に! 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. 参考文献
免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。
Copyright© 2022 湘ICP备2022001581号-3