」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 編碼練習:nodejs 中的資料庫遷移工具

編碼練習:nodejs 中的資料庫遷移工具

發佈於2024-11-04
瀏覽:661

Coding exercise: database migration tool in nodejs

要求

我想要一个数据库迁移工具,它具有以下属性:

  1. 每个迁移都写在单个 SQL 文件中,意味着“向上”和“向下”部分。这将允许 Copilot 填写回滚迁移。事实上,它是一个裸 SQL,也使其成为最灵活和受支持的解决方案。
  2. 当前应用的版本应由该工具管理。我希望该工具能够自给自足。
  3. 我希望该工具支持不同的数据库,例如 Postgres、MySQL、SQL Server 等,因此从这个意义上来说它应该是可扩展的。
  4. 我不希望它太大,因此只应安装必要数据库的驱动程序,最好是按需安装。
  5. 我希望它成为 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 方法。它应该接受一个函数,该函数将被一个接受查询文本并返回带有中间结果的承诺的函数调用。这种复杂性需要有一个通用接口来提供在事务内运行多个查询的能力。通过查看使用示例更容易掌握。

这就是适配器查找 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. 参考文献

  • martlet 仓库
  • 三元
  • postgres 驱动程序
版本聲明 本文轉載於:https://dev.to/duskpoet/coding-exercise-database-migration-tool-in-nodejs-30pg?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • 如何在Java字符串中有效替換多個子字符串?
    如何在Java字符串中有效替換多個子字符串?
    在java 中有效地替換多個substring,需要在需要替換一個字符串中的多個substring的情況下,很容易求助於重複應用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
    程式設計 發佈於2025-07-01
  • 如何使用不同數量列的聯合數據庫表?
    如何使用不同數量列的聯合數據庫表?
    合併列數不同的表 當嘗試合併列數不同的數據庫表時,可能會遇到挑戰。一種直接的方法是在列數較少的表中,為缺失的列追加空值。 例如,考慮兩個表,表 A 和表 B,其中表 A 的列數多於表 B。為了合併這些表,同時處理表 B 中缺失的列,請按照以下步驟操作: 確定表 B 中缺失的列,並將它們添加到表的...
    程式設計 發佈於2025-07-01
  • 如何解決AppEngine中“無法猜測文件類型,使用application/octet-stream...”錯誤?
    如何解決AppEngine中“無法猜測文件類型,使用application/octet-stream...”錯誤?
    appEngine靜態文件mime type override ,靜態文件處理程序有時可以覆蓋正確的mime類型,在錯誤消息中導致錯誤消息:“無法猜測mimeType for for file for file for [File]。 application/application/octet...
    程式設計 發佈於2025-07-01
  • 查找當前執行JavaScript的腳本元素方法
    查找當前執行JavaScript的腳本元素方法
    如何引用當前執行腳本的腳本元素在某些方案中理解問題在某些方案中,開發人員可能需要將其他腳本動態加載其他腳本。但是,如果Head Element尚未完全渲染,則使用document.getElementsbytagname('head')[0] .appendChild(v)的常規方...
    程式設計 發佈於2025-07-01
  • 如何處理PHP文件系統功能中的UTF-8文件名?
    如何處理PHP文件系統功能中的UTF-8文件名?
    在PHP的Filesystem functions中處理UTF-8 FileNames 在使用PHP的MKDIR函數中含有UTF-8字符的文件很多flusf-8字符時,您可能會在Windows Explorer中遇到comploreer grounder grounder grounder gro...
    程式設計 發佈於2025-07-01
  • 如何在GO編譯器中自定義編譯優化?
    如何在GO編譯器中自定義編譯優化?
    在GO編譯器中自定義編譯優化 GO中的默認編譯過程遵循特定的優化策略。 However, users may need to adjust these optimizations for specific requirements.Optimization Control in Go Compi...
    程式設計 發佈於2025-07-01
  • Async Void vs. Async Task在ASP.NET中:為什麼Async Void方法有時會拋出異常?
    Async Void vs. Async Task在ASP.NET中:為什麼Async Void方法有時會拋出異常?
    在ASP.NET async void void async void void void void void的設計無需返回asynchroncon而無需返回任務對象。他們在執行過程中增加未償還操作的計數,並在完成後減少。在某些情況下,這種行為可能是有益的,例如未期望或明確預期操作結果的火災和...
    程式設計 發佈於2025-07-01
  • 如何在Chrome中居中選擇框文本?
    如何在Chrome中居中選擇框文本?
    選擇框的文本對齊:局部chrome-inly-ly-ly-lyly solument 您可能希望將文本中心集中在選擇框中,以獲取優化的原因或提高可訪問性。但是,在CSS中的選擇元素中手動添加一個文本 - 對屬性可能無法正常工作。 初始嘗試 state)</option> < o...
    程式設計 發佈於2025-07-01
  • 將圖片浮動到底部右側並環繞文字的技巧
    將圖片浮動到底部右側並環繞文字的技巧
    在Web設計中圍繞在Web設計中,有時可以將圖像浮動到頁面右下角,從而使文本圍繞它纏繞。這可以在有效地展示圖像的同時創建一個吸引人的視覺效果。 css位置在右下角,使用css float and clear properties: img { 浮點:對; ...
    程式設計 發佈於2025-07-01
  • 如何干淨地刪除匿名JavaScript事件處理程序?
    如何干淨地刪除匿名JavaScript事件處理程序?
    刪除匿名事件偵聽器將匿名事件偵聽器添加到元素中會提供靈活性和簡單性,但是當需要刪除它們時,可以構成挑戰,而無需替換元素本身就可以替換一個問題。 element? element.addeventlistener(event,function(){/在這里工作/},false); 要解決此問題,請...
    程式設計 發佈於2025-07-01
  • 如何從PHP中的數組中提取隨機元素?
    如何從PHP中的數組中提取隨機元素?
    從陣列中的隨機選擇,可以輕鬆從數組中獲取隨機項目。考慮以下數組:; 從此數組中檢索一個隨機項目,利用array_rand( array_rand()函數從數組返回一個隨機鍵。通過將$項目數組索引使用此鍵,我們可以從數組中訪問一個隨機元素。這種方法為選擇隨機項目提供了一種直接且可靠的方法。
    程式設計 發佈於2025-07-01
  • 如何使用FormData()處理多個文件上傳?
    如何使用FormData()處理多個文件上傳?
    )處理多個文件輸入時,通常需要處理多個文件上傳時,通常是必要的。 The fd.append("fileToUpload[]", files[x]); method can be used for this purpose, allowing you to send multi...
    程式設計 發佈於2025-07-01
  • 為什麼HTML無法打印頁碼及解決方案
    為什麼HTML無法打印頁碼及解決方案
    無法在html頁面上打印頁碼? @page規則在@Media內部和外部都無濟於事。 HTML:Customization:@page { margin: 10%; @top-center { font-family: sans-serif; font-weight: ...
    程式設計 發佈於2025-07-01
  • FastAPI自定義404頁面創建指南
    FastAPI自定義404頁面創建指南
    response = await call_next(request) if response.status_code == 404: return RedirectResponse("https://fastapi.tiangolo.com") else: ...
    程式設計 發佈於2025-07-01

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3