Bài viết Câu hỏi About RongvangIT
profile Pic
0
0

Đăng ngày:

 

Sửa ngày:

59 Lượt xem

Sử dụng sqlc trong TypeScript

PostgreSQLTypeScript

Tóm tắt

  • sqlc-gen-typescript rất tốt
  • Nếu bạn đang sử dụng TypeScript cho ứng dụng web của mình, không có lý do gì bạn không chọn sqlc
  • SQL thực sự là một ngôn ngữ chung tuyệt vời

Giới thiệu về sqlc

sqlc là một công cụ tạo mã nguồn dựa trên SQL được viết bằng Go.

$ sqlc compile

Tại sao sử dụng sqlc?

  • Cuối cùng, việc học SQL nhanh hơn so với việc học các kỹ thuật cụ thể của từng ORM
  • Không liên quan đến việc ORM có hỗ trợ mở rộng hay không
  • SQL Parser sử dụng pg_query_go, nên rất đáng tin cậy

sqlc chỉ hỗ trợ PostgreSQL?

sqlc cũng hỗ trợ MySQL và SQLite.

sqlc chỉ có thể chạy trên Go?

sqlc đã hỗ trợ việc viết plugin bằng Wasm, và gần đây đã có phiên bản TypeScript.
Hiện tại, chỉ hỗ trợ PostgreSQL và MySQL.

https://github.com/sqlc-dev/sqlc-gen-typescript

Python

Cũng có plugin sqlc cho Python.

https://github.com/sqlc-dev/sqlc-gen-python

Cloudflare D1

Mặc dù không chính thức, nhưng cũng có plugin sqlc cho Cloudflare D1.

https://github.com/orisano/sqlc-gen-ts-d1

Migrations

sqlc không hỗ trợ migrations. Bạn có thể sử dụng bất kỳ công cụ nào bạn muốn, nhưng đề xuất sử dụng sqldef.

https://github.com/sqldef/sqldef

Mã tham khảo

Đối với ví dụ hoạt động, hãy xem mã nguồn sau:
https://github.com/voluntas/sqlc-gen-ts-template

  1. Sử dụng sqlc-gen-typescript để tạo mã TypeScript trong thư mục src/gen/sqlc/pgsrc/gen/sqlc/postgres
  2. Phiên bản chính thức sử dụng postgres.js có bug, nên sử dụng bản fork của sqlc từ @orisano
    https://github.com/orisano/sqlc-gen-typescript/tree/postgres-snake
  3. Sử dụng Vitesttestcontainers cho việc kiểm thử không cần mô phỏng
  4. Đặt các file SQL của queryschema trong thư mục db
  5. Xác định Wasm của sqlc-gen-typescript cần được sử dụng trong sqlc.yaml
  6. Sử dụng driver PostgreSQL với pgpostgres
  7. Cũng có hỗ trợ GitHub Actions cho việc kiểm thử

sqlc.yaml

version: "2"
plugins:
  - name: ts
    wasm:
      url: https://github.com/orisano/sqlc-gen-typescript/releases/download/v0.1.2/plugin.wasm
      sha256: a0b9bb3a31bf51ae33b8191cc1eaee1bacef38fe0472899eb51c20fe4926430e
      # url: https://downloads.sqlc.dev/plugin/sqlc-gen-typescript_0.1.2.wasm
      # sha256: f8b59cdd78b35fae157a95c5813cb09b1ebdd9a31acf2d7015465539986ccd2b

sql:
  - schema: db/schema.sql
    queries: db/query/
    engine: postgresql
    codegen:
      - out: src/gen/sqlc/pg
        plugin: ts
        options:
          runtime: node
          driver: pg
  - schema: db/schema.sql
    queries: db/query/
    engine: postgresql
    codegen:
      - out: src/gen/sqlc/postgres
        plugin: ts
        options:
          runtime: node
          driver: postgres

test/sqlc_pg.test.ts

import { Client } from "pg";
import { GenericContainer, Wait } from "testcontainers";
import { expect, test } from "vitest";

import fs from "fs";
import {
  createAccount,
  deleteAccount,
  getAccount,
  listAccounts,
} from "../src/gen/sqlc/account_sql";

test("account", async () => {
  // PostgreSQL コンテナを起動
  const container = await new GenericContainer("postgres:latest")
    .withEnvironment({
      POSTGRES_DB: "testdb",
      POSTGRES_USER: "user",
      POSTGRES_PASSWORD: "password",
    })
    .withExposedPorts(5432)
    // TCPポートが利用可能になるまで待機
    .withWaitStrategy(Wait.forListeningPorts())
    .start();

  // postgres クライアントの設定
  const client = new Client({
    host: container.getHost(),
    port: container.getMappedPort(5432),
    database: "testdb",
    user: "user",
    password: "password",
  });
  await client.connect();

  // データベースへの ping (接続テスト)
  await client.query("SELECT 1");

  // ファイルを読み込んでSQL文を取得
  const schemaSQL = fs.readFileSync("db/schema.sql", "utf-8");

  // スキーマの初期化
  await client.query(schemaSQL);

  await createAccount(client, {
    id: "spam",
    displayName: "Egg",
    email: "[email protected]",
  });

  const account = await getAccount(client, { id: "spam" });
  expect(account).not.toBeNull();
  // ここダサい、なんかいい書き方 Vitest にありそう
  if (account) {
    expect(account.id).toBe("spam");
    expect(account.displayName).toBe("Egg");
    expect(account.email).toBe("[email protected]");
  }

  await deleteAccount(client, { id: "spam" });

  const accounts = await listAccounts(client);
  expect(accounts.length).toBe(0);

  await client.end();

  // コンテナを停止
  await container.stop();
}, 30_000);

db/query/account.sql

-- name: GetAccount :one
SELECT *
FROM account
WHERE id = @id;

-- name: ListAccounts :many
SELECT *
FROM account;

-- name: CreateAccount :exec
INSERT INTO account (id, display_name, email)
VALUES (@id, @display_name, @email);

-- name: UpdateAccountDisplayName :one
UPDATE account
SET display_name = @display_name
WHERE id = @id
RETURNING *;

-- name: DeleteAccount :exec
DELETE FROM account
WHERE id = @id;

src/gen/sqlc/pg/account_sql.ts

import { QueryArrayConfig, QueryArrayResult } from "pg";

interface Client {
  query: (config: QueryArrayConfig) => Promise<QueryArrayResult>;
}

export const getAccountQuery = `-- name: GetAccount :one
SELECT pk, id, display_name, email, created_at
FROM account
WHERE id = $1`;

export interface GetAccountArgs {
  id: string;
}

export interface GetAccountRow {
  pk: number;
  id: string;
  displayName: string;
  email: string | null;
  createdAt: Date;
}

export async function getAccount(
  client: Client,
  args: GetAccountArgs
): Promise<GetAccountRow | null> {
  const result = await client.query({
    text: getAccountQuery,
    values: [args.id],
    rowMode: "array",
  });
  if (result.rows.length !== 1) {
    return null;
  }
  const row = result.rows[0];
  return {
    pk: row[0],
    id: row[1],
    displayName: row[2],
    email: row[3],
    createdAt: row[4],
  };
}

export const listAccountsQuery = `-- name: ListAccounts :many
SELECT pk, id, display_name, email, created_at
FROM account`;

export interface ListAccountsRow {
  pk: number;
  id: string;
  displayName: string;
  email: string | null;
  createdAt: Date;
}

export async function listAccounts(client: Client): Promise<ListAccountsRow[]> {
  const result = await client.query({
    text: listAccountsQuery,
    values: [],
    rowMode: "array",
  });
  return result.rows.map((row) => {
    return {
      pk: row[0],
      id: row[1],
      displayName: row[2],
      email: row[3],
      createdAt: row[4],
    };
  });
}

export const createAccountQuery = `-- name: CreateAccount :exec
INSERT INTO account (id, display_name, email)
VALUES ($1, $2, $3)`;

export interface CreateAccountArgs {
  id: string;
  displayName: string;
  email: string | null;
}

export async function createAccount(
  client: Client,
  args: CreateAccountArgs
): Promise<void> {
  await client.query({
    text: createAccountQuery,
    values: [args.id, args.displayName, args.email],
    rowMode: "array",
  });
}

export const updateAccountDisplayNameQuery = `-- name: UpdateAccountDisplayName :one
UPDATE account
SET display_name = $1
WHERE id = $2
RETURNING pk, id, display_name, email, created_at`;

export interface UpdateAccountDisplayNameArgs {
  displayName: string;
  id: string;
}

export interface UpdateAccountDisplayNameRow {
  pk: number;
  id: string;
  displayName: string;
  email: string | null;
  createdAt: Date;
}

export async function updateAccountDisplayName(
  client: Client,
  args: UpdateAccountDisplayNameArgs
): Promise<UpdateAccountDisplayNameRow | null> {
  const result = await client.query({
    text: updateAccountDisplayNameQuery,
    values: [args.displayName, args.id],
    rowMode: "array",
  });
  if (result.rows.length !== 1) {
    return null;
  }
  const row = result.rows[0];
  return {
    pk: row[0],
    id: row[1],
    displayName: row[2],
    email: row[3],
    createdAt: row[4],
  };
}

export const deleteAccountQuery = `-- name: DeleteAccount :exec
DELETE FROM account
WHERE id = $1`;

export interface DeleteAccountArgs {
  id: string;
}

export async function deleteAccount(
  client: Client,
  args: DeleteAccountArgs
): Promise<void> {
  await client.query({
    text: deleteAccountQuery,
    values: [args.id],
    rowMode: "array",
  });
}
dev_pro_it
Đang làm IT tại Japan

Bình luận

Bài viết chưa có bình luận. Hãy trở thành người bình luận đầu tiên!
Sign up for free and join this conversation.
Sign Up
If you already have a RongvangIT account Login
Danh sách thư mục
Bắt đầu ngay với RồngVàngIT - nền tảng chia sẻ kiến thức lập trình tuyệt vời cho kỹ sư Việt Nam!

Hãy đăng nhập để sử dụng hàng loạt các chức năng tuyệt vời của RồngVàngIT !

  1. 1. Bạn sẽ nhận được các bài viết phù hợp bằng chức năng theo dõi tag và người dùng.
  2. 2. Bạn có thể đọc lại các thông tin hữu ích bằng chức năng lưu trữ nội dung.
  3. 3. Chia sẻ kiến thức, đặt câu hỏi và ghi lại quá trình trưởng thành của mình cùng RồngVàngIT !
Tạo tài khoản Đăng nhập
profile Pic