


Latihan pengekodan: alat pemindahan pangkalan data dalam nodejs
Keperluan
Saya mahu mempunyai alat migrasi pangkalan data, yang mempunyai sifat berikut:
- Setiap migrasi ditulis dalam satu fail SQL, bermakna kedua-dua bahagian "atas" dan "bawah". Ini akan membolehkan Copilot mengisi migrasi balik. Dan hakikat bahawa ia adalah SQL kosong juga menjadikannya penyelesaian yang paling fleksibel dan disokong.
- Versi yang digunakan pada masa ini harus diuruskan oleh alat. Saya mahu alat itu berdikari.
- Saya mahu alat itu menyokong pangkalan data yang berbeza, seperti Postgres, MySQL, SQL Server, dsb., jadi alat itu harus boleh dilanjutkan dalam erti kata itu.
- Saya tidak mahu ia terlalu besar, jadi hanya pemacu untuk pangkalan data yang diperlukan perlu dipasang, sebaik-baiknya atas permintaan.
- Saya mahu ia menjadi sebahagian daripada ekosistem javascript kerana kebanyakan projek yang saya kerjakan adalah sebahagian daripadanya.
- Setiap penghijrahan harus dilakukan di dalam urus niaga.
pengenalan
Banyak perkara ini lahir daripada pengalaman saya dengan alat hebat yang dipanggil tern ini. Saya sedih kerana javascript tidak mempunyai yang sama! (Atau mungkin saya malas googling...). Jadi saya memutuskan ini boleh menjadi latihan pengekodan yang bagus untuk diri saya sendiri dan cerita yang mungkin menarik kepada orang lain :)
Pembangunan
Bahagian 1. Mereka bentuk alat
Jom mencuri mereka bentuk alat CLI!
- Semua migrasi akan mempunyai skema penamaan berikut:
_ .sql, di mana nombor itu akan mewakili nombor versi migrasi, contohnya, 001_initial_setup.sql. - Semua penghijrahan akan berada dalam satu dir.
- Pemandu pangkalan data akan dimuat turun atas permintaan, sama ada beberapa pakej yang diprabundel atau hanya mengeluarkan semacam npm install
.
Jadi sintaks untuk alat itu ialah seperti berikut: martlet up --database-url
Di mana "atas" harus menggunakan semua migrasi yang belum digunakan dan ke bawah harus diputar semula ke versi yang ditentukan.
Pilihan mempunyai makna dan lalai berikut:
- url pangkalan data - rentetan sambungan untuk pangkalan data, lalainya adalah untuk mencari pembolehubah env DATABASE_URL
- pemandu - pemacu pangkalan data untuk digunakan. Untuk versi pertama, saya hanya akan menyokong Postgres dengan pilihan bernama "pg".
- dir - direktori tempat migrasi berada, lalai ialah migrasi
Seperti yang anda lihat, saya telah mula memikirkan cara saya menggunakan alat tersebut sebelum menulis sebarang kod sebenar. Ini adalah amalan yang baik, ia membantu merealisasikan keperluan dan mengurangkan kitaran pembangunan.
Bahagian 2. Pelaksanaan
2.1 Pilihan penghuraian
Ok, perkara pertama dahulu! Mari buat fail index.js dan keluarkan mesej bantuan. Ia akan kelihatan seperti ini:
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();
Kini kami akan menghuraikan pilihan:
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; }
Seperti yang anda lihat, saya tidak menggunakan mana-mana perpustakaan untuk menghurai; Saya hanya mengulangi senarai hujah dan memproses setiap pilihan. Jadi, jika saya mempunyai pilihan boolean, saya akan mengalihkan indeks lelaran sebanyak 1 dan jika saya mempunyai pilihan dengan nilai, saya akan mengalihkannya sebanyak 2.
2.2 Melaksanakan penyesuai pemacu
Untuk menyokong berbilang pemacu, kita perlu mempunyai antara muka universal untuk mengakses pangkalan data; inilah rupanya:
interface Adapter { connect(url: string): Promise<void>; transact(query: (fn: (text) => Promise<ResultSet>)): Promise<ResultSet>; close(): Promise<void>; }
Saya rasa sambung dan tutup adalah fungsi yang cukup jelas, izinkan saya menerangkan kaedah transaksi. Ia harus menerima fungsi yang akan dipanggil dengan fungsi yang menerima teks pertanyaan dan mengembalikan janji dengan hasil perantaraan. Kerumitan ini diperlukan untuk mempunyai antara muka umum yang akan menyediakan keupayaan untuk menjalankan berbilang pertanyaan dalam transaksi. Ia lebih mudah untuk difahami dengan melihat contoh penggunaan.
Jadi ini adalah cara penyesuai mencari pemacu 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(); } }
Dan contoh penggunaannya mungkin:
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 Pemasangan pemandu atas permintaan
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", }); };
Kami cuba memasang pemacu dengan benang pada mulanya, tetapi kami tidak mahu menjana sebarang perbezaan dalam direktori, jadi kami mengekalkan fail yarn.lock dan package.json. Jika benang tidak tersedia, kami akan kembali kepada npm.
Apabila kami memastikan pemacu dipasang, kami boleh mencipta penyesuai dan menggunakannya:
export async function loadAdapter(driver) { await downloadDriver(driver); return import(PACKAGES[driver].split("@")[0]).then( (m) => new PGAdapter(m.default), );
2.4 Melaksanakan logik migrasi
Kami bermula dengan menyambung ke pangkalan data dan mendapatkan versi semasa:
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
Atas ialah kandungan terperinci Latihan pengekodan: alat pemindahan pangkalan data dalam nodejs. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Alat AI Hot

Undresser.AI Undress
Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover
Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool
Gambar buka pakaian secara percuma

Clothoff.io
Penyingkiran pakaian AI

Video Face Swap
Tukar muka dalam mana-mana video dengan mudah menggunakan alat tukar muka AI percuma kami!

Artikel Panas

Alat panas

Notepad++7.3.1
Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina
Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1
Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6
Alat pembangunan web visual

SublimeText3 versi Mac
Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Topik panas











Python lebih sesuai untuk pemula, dengan lengkung pembelajaran yang lancar dan sintaks ringkas; JavaScript sesuai untuk pembangunan front-end, dengan lengkung pembelajaran yang curam dan sintaks yang fleksibel. 1. Sintaks Python adalah intuitif dan sesuai untuk sains data dan pembangunan back-end. 2. JavaScript adalah fleksibel dan digunakan secara meluas dalam pengaturcaraan depan dan pelayan.

Peralihan dari C/C ke JavaScript memerlukan menyesuaikan diri dengan menaip dinamik, pengumpulan sampah dan pengaturcaraan asynchronous. 1) C/C adalah bahasa yang ditaip secara statik yang memerlukan pengurusan memori manual, manakala JavaScript ditaip secara dinamik dan pengumpulan sampah diproses secara automatik. 2) C/C perlu dikumpulkan ke dalam kod mesin, manakala JavaScript adalah bahasa yang ditafsirkan. 3) JavaScript memperkenalkan konsep seperti penutupan, rantaian prototaip dan janji, yang meningkatkan keupayaan pengaturcaraan fleksibiliti dan asynchronous.

Penggunaan utama JavaScript dalam pembangunan web termasuk interaksi klien, pengesahan bentuk dan komunikasi tak segerak. 1) kemas kini kandungan dinamik dan interaksi pengguna melalui operasi DOM; 2) pengesahan pelanggan dijalankan sebelum pengguna mengemukakan data untuk meningkatkan pengalaman pengguna; 3) Komunikasi yang tidak bersesuaian dengan pelayan dicapai melalui teknologi Ajax.

Aplikasi JavaScript di dunia nyata termasuk pembangunan depan dan back-end. 1) Memaparkan aplikasi front-end dengan membina aplikasi senarai TODO, yang melibatkan operasi DOM dan pemprosesan acara. 2) Membina Restfulapi melalui Node.js dan menyatakan untuk menunjukkan aplikasi back-end.

Memahami bagaimana enjin JavaScript berfungsi secara dalaman adalah penting kepada pemaju kerana ia membantu menulis kod yang lebih cekap dan memahami kesesakan prestasi dan strategi pengoptimuman. 1) aliran kerja enjin termasuk tiga peringkat: parsing, penyusun dan pelaksanaan; 2) Semasa proses pelaksanaan, enjin akan melakukan pengoptimuman dinamik, seperti cache dalam talian dan kelas tersembunyi; 3) Amalan terbaik termasuk mengelakkan pembolehubah global, mengoptimumkan gelung, menggunakan const dan membiarkan, dan mengelakkan penggunaan penutupan yang berlebihan.

Python dan JavaScript mempunyai kelebihan dan kekurangan mereka sendiri dari segi komuniti, perpustakaan dan sumber. 1) Komuniti Python mesra dan sesuai untuk pemula, tetapi sumber pembangunan depan tidak kaya dengan JavaScript. 2) Python berkuasa dalam bidang sains data dan perpustakaan pembelajaran mesin, sementara JavaScript lebih baik dalam perpustakaan pembangunan dan kerangka pembangunan depan. 3) Kedua -duanya mempunyai sumber pembelajaran yang kaya, tetapi Python sesuai untuk memulakan dengan dokumen rasmi, sementara JavaScript lebih baik dengan MDNWebDocs. Pilihan harus berdasarkan keperluan projek dan kepentingan peribadi.

Kedua -dua pilihan Python dan JavaScript dalam persekitaran pembangunan adalah penting. 1) Persekitaran pembangunan Python termasuk Pycharm, Jupyternotebook dan Anaconda, yang sesuai untuk sains data dan prototaip cepat. 2) Persekitaran pembangunan JavaScript termasuk node.js, vscode dan webpack, yang sesuai untuk pembangunan front-end dan back-end. Memilih alat yang betul mengikut keperluan projek dapat meningkatkan kecekapan pembangunan dan kadar kejayaan projek.

C dan C memainkan peranan penting dalam enjin JavaScript, terutamanya digunakan untuk melaksanakan jurubahasa dan penyusun JIT. 1) C digunakan untuk menghuraikan kod sumber JavaScript dan menghasilkan pokok sintaks abstrak. 2) C bertanggungjawab untuk menjana dan melaksanakan bytecode. 3) C melaksanakan pengkompil JIT, mengoptimumkan dan menyusun kod hot-spot semasa runtime, dan dengan ketara meningkatkan kecekapan pelaksanaan JavaScript.
