我想要一个数据库迁移工具,它具有以下属性:
其中很多观点都源于我使用这个名为 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 方法。它应该接受一个函数,该函数将被一个接受查询文本并返回带有中间结果的承诺的函数调用。这种复杂性需要有一个通用接口来提供在事务内运行多个查询的能力。通过查看使用示例更容易掌握。
这就是适配器查找 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文件。如果yarn不可用,我们将回退到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. 测试
我决定不使用任何特定的测试框架,而是使用内置的nodejs测试功能。它们包括测试运行程序和断言包。
import { it, before, after, describe } from "node:test"; import assert from "node:assert";要执行测试,我将运行 node --test --test-concurrency=1。
实际上,我是以一种 TDD 的方式编写代码的。我没有手动验证我的迁移代码是否有效,但我是与测试一起编写的。这就是为什么我认为端到端测试最适合这个工具。
对于这种方法,测试需要引导一个空数据库,应用一些迁移,检查数据库内容是否正确,然后回滚到初始状态并验证数据库是否为空。
为了运行数据库,我使用了“testcontainers”库,它为 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(); });我编写了一些简单的迁移并测试了它们是否按预期工作。这是数据库状态验证的示例:
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 驱动程序和 testcontainers 进行测试。我认为这种方法为开发人员提供了对应用程序最大的灵活性和控制力。
5. 参考文献
免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。
Copyright© 2022 湘ICP备2022001581号-3