# Quản lý phê duyệt trước khi thay đổi cơ sở dữ liệu

> Truy vấn đọc chạy ngay; mỗi thay đổi chờ quản lý xem đúng câu SQL và số dòng dự kiến trên điện thoại rồi phê duyệt, sau đó chỉ chạy một lần. Cơ sở dữ liệu không rời khỏi máy chủ của bạn.

Source: https://hibira.io/vi/use-cases/database-write-approvals

Cho nhân viên, script hay trợ lý tự truy vấn cơ sở dữ liệu giúp tiết kiệm hàng giờ. Cho họ tự sửa dữ liệu mà không ai xem qua thì một yêu cầu "dọn giỏ hàng cũ" có thể xóa luôn đơn hàng tuần trước. Hibira đặt một người quản lý ở giữa: truy vấn đọc chạy ngay, còn mỗi thay đổi phải chờ quản lý xem đúng câu SQL và số dòng bị ảnh hưởng trên điện thoại rồi bấm đồng ý. Cơ sở dữ liệu và thông tin kết nối không bao giờ rời khỏi máy chủ của bạn.

## Người dùng nhận được gì

- Câu hỏi chỉ đọc có câu trả lời ngay, không phải chờ ai.
- Mỗi thay đổi có một trang phê duyệt mà quản lý mở từ thông báo đẩy hoặc mã QR: câu lệnh, số dòng dự kiến, kế hoạch thực thi và dấu vân tay, cùng nút Phê duyệt và Từ chối.
- Kết quả hiện trên màn hình người yêu cầu ngay khi quản lý quyết định, không cần tải lại. Nếu bị từ chối thì kèm lý do của quản lý.
- Bảo đảm câu lệnh được chạy đúng là câu đã được phê duyệt, và chỉ chạy một lần.

![Bảng điều khiển demo: một lệnh DELETE đang chờ quản lý phê duyệt, kèm mã QR, số dòng dự kiến và SHA-256 của câu lệnh.](https://hibira.io/portal/articles/sql-approvals-console-vi-light.webp)

## Xây dựng bằng

- **[Phê duyệt](https://hibira.io/vi/guides/approvals)**: `POST /apps/APP_ID/approvals` (hoặc `request_approval` của [MCP server](https://hibira.io/vi/mcp-server)) tạo trang phê duyệt do Hibira lưu trữ và báo quyết định trên một kênh.
- **Kênh riêng tư** cho mỗi bảng điều khiển hoặc mỗi lần chạy, ví dụ `private-sqlgate-<random>`, nơi sự kiện `approval` đến. Không cần hỏi lại liên tục.
- **Thông báo đẩy** (tùy chọn) để gửi liên kết tới điện thoại của quản lý.
- **[@hibira-io/approvals/sql](https://hibira.io/vi/guides/approvals)**: `classifySql()`, `sqlFingerprint()` và `assertApprovedSql()` cho Node.js, và các hàm tương tự cho Python.

```text
sequence: Truy vấn đọc chạy ngay; thay đổi chỉ chạy sau khi quản lý phê duyệt
Người yêu cầu | Máy chủ của bạn | *Hibira | Quản lý
1 > 2: SELECT … | chạy chỉ đọc, trả dữ liệu ngay
1 > 2: DELETE FROM carts … | chạy thử: 10 dòng
2 > 3: POST /approvals | câu lệnh, số dòng dự kiến, SHA-256
3 > 4: thông báo đẩy | liên kết tới trang phê duyệt
4 > 3: Phê duyệt
3 > 1: sự kiện approval | kênh riêng tư, không hỏi lại liên tục
1 > 2: chạy đi
2 > 3: GET /approvals/apr_… | xác nhận câu trả lời một lần
2 >> 1: đã chạy một lần: thay đổi 10 dòng
```

Thử trên [demos.hibira.io](https://demos.hibira.io/examples/nextjs-sql-approvals/): chọn một thay đổi, quét mã QR bằng điện thoại và phê duyệt.

## Bước 1: cho truy vấn đọc đi qua, chặn mọi thứ khác

`classifySql()` chỉ coi một câu là truy vấn đọc khi chắc chắn: một câu lệnh duy nhất, bắt đầu bằng `SELECT`, `WITH`, `EXPLAIN` hoặc `SHOW`, và không có phần nào sửa dữ liệu. Nhiều câu lệnh, `WITH ... DELETE`, `EXPLAIN ANALYZE`, `SELECT ... INTO`, `FOR UPDATE` và bất cứ thứ gì không phân tích chắc chắn được đều là thay đổi. Dù vậy vẫn nên chạy truy vấn đọc trong giao dịch chỉ đọc, để câu bị đánh giá sai sẽ thất bại thay vì ghi dữ liệu:

```js
import pg from 'pg';
import { classifySql } from '@hibira-io/approvals/sql';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

export async function runSql(sql, requestedBy) {
  if (classifySql(sql) === 'read') return { rows: await readOnly(sql) };
  return { approval: await askSupervisor(sql, requestedBy) };
}

async function readOnly(sql) {
  const db = await pool.connect();
  try {
    await db.query('BEGIN READ ONLY');
    const { rows } = await db.query(sql);
    await db.query('COMMIT');
    return rows.slice(0, 200);
  } catch (err) {
    await db.query('ROLLBACK');
    throw err;
  } finally {
    db.release();
  }
}
```

## Bước 2: cho quản lý thấy thay đổi sẽ làm gì

Ước tính số dòng trước khi xin phê duyệt. `EXPLAIN` không chạy câu lệnh; chạy thử trong giao dịch được hoàn tác cho số chính xác nhưng câu lệnh thực sự chạy, nên hãy đặt thời gian chờ. Rồi lấy dấu vân tay của đúng văn bản câu lệnh: phê duyệt chỉ có hiệu lực với câu này.

```js
import { sqlFingerprint } from '@hibira-io/approvals/sql';

async function preview(sql) {
  const db = await pool.connect();
  try {
    await db.query('BEGIN');
    await db.query("SET LOCAL statement_timeout = '5s'");
    const { rowCount } = await db.query(sql);
    return { rows: rowCount, fingerprint: await sqlFingerprint(sql) };
  } finally {
    await db.query('ROLLBACK');
    db.release();
  }
}
```

## Bước 3: xin phê duyệt

Tạo yêu cầu với câu lệnh và bản xem trước trong `details`, cùng kênh để báo quyết định. Giữ câu lệnh và dấu vân tay ở phía bạn, gắn với ID phê duyệt. Thứ duy nhất gửi tới Hibira là nội dung yêu cầu: không có thông tin kết nối, không có dòng dữ liệu nào.

```js
import Pusher from 'pusher';

const hibira = new Pusher({ appId: process.env.HIBIRA_APP_ID, key: process.env.HIBIRA_KEY, secret: process.env.HIBIRA_SECRET, host: 'ws.hibira.io', useTLS: true });

async function askSupervisor(sql, requestedBy) {
  const { rows, fingerprint } = await preview(sql);
  const res = await hibira.post({
    path: '/approvals',
    body: {
      title: 'Run this database change?',
      details: `${sql}\n\nEstimated rows: ${rows}\n\nSHA-256: ${fingerprint}`,
      options: ['Approve', 'Reject'],
      channel: `private-sqlgate-${requestedBy.consoleId}`,
      push_users: ['supervisor-42'],
      expires_in_minutes: 15,
      agent: 'SQL console',
    },
  });
  const approval = await res.json();
  await pool.query('INSERT INTO pending_changes (approval_id, sql, fingerprint) VALUES ($1, $2, $3)', [approval.approval_id, sql, fingerprint]);
  return { id: approval.approval_id, url: approval.url, rows };
}
```

Quản lý nhận thông báo đẩy kèm liên kết (hoặc quét mã trên màn hình người yêu cầu) và trả lời trên trang hiển thị bằng ngôn ngữ của họ.

![Trang phê duyệt trên điện thoại của quản lý: câu lệnh, số dòng dự kiến, SHA-256, Phê duyệt và Từ chối.](https://hibira.io/portal/articles/sql-approvals-page-vi.webp)

## Bước 4: chạy một lần khi có câu trả lời

Quyết định đến dưới dạng sự kiện `approval` trên kênh. Hãy coi nó như chuông cửa: đọc câu trả lời từ API một lần, kiểm tra câu lệnh đã lưu vẫn khớp dấu vân tay, rồi mới chạy. Giữ chỗ dòng đó trước thì câu lệnh chỉ chạy một lần, kể cả khi hai sự kiện hoặc hai worker đến cùng lúc:

```js
import { assertApprovedSql } from '@hibira-io/approvals/sql';

// In the requester's page (pusher-js), or a worker that keeps a connection open:
channel.bind('approval', ({ id, reason }) => {
  if (reason === 'decided') fetch(`/api/changes/${id}/run`, { method: 'POST' });
});

// POST /api/changes/:id/run on your server:
export async function runApproved(id) {
  const res = await hibira.get({ path: `/approvals/${id}` });
  const answer = await res.json();
  if (answer.status !== 'approved') return { status: answer.status, comment: answer.comment };
  const { rows } = await pool.query('UPDATE pending_changes SET ran_at = now() WHERE approval_id = $1 AND ran_at IS NULL RETURNING sql, fingerprint', [id]);
  if (!rows.length) return { status: 'already run' };
  await assertApprovedSql(rows[0].sql, rows[0].fingerprint);
  const result = await pool.query(rows[0].sql);
  return { status: 'ran', rows: result.rowCount };
}
```

![Sau khi phê duyệt: bảng điều khiển cho thấy thay đổi đã chạy một lần và số dòng đã thay đổi.](https://hibira.io/portal/articles/sql-approvals-done-vi-light.webp)

## Bạn đã kết nối cơ sở dữ liệu qua một MCP server?

Nếu nhóm của bạn làm việc với cơ sở dữ liệu từ Claude, Cursor, ChatGPT hoặc một ứng dụng MCP khác, thông qua server như DBHub, postgres-mcp hay Supabase MCP, chỉ cần đặt [mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate) phía trước server đó, không cần sửa mã. Truy vấn đọc đi qua ngay; mỗi lệnh thêm, sửa, xóa hay thay đổi cấu trúc đều dừng ở cùng trang phê duyệt, rồi chạy đúng một lần như đã được duyệt.

```sh
npx -y @hibira-io/mcp-approval-gate --label shop-production --push-interests db-approvals \
  -- npx -y @bytebase/dbhub --transport stdio --dsn "$DATABASE_URL"
```

![Phiên thực tế: yêu cầu đánh dấu đơn 1042 là đã hoàn tiền phải chờ, người quản lý duyệt lệnh UPDATE trên điện thoại, rồi thay đổi mới chạy một lần.](https://hibira.io/portal/articles/mcp-approval-gate-vi.mp4)

## Cùng một chốt chặn trong công cụ của bạn

- **Vercel AI SDK, Mastra, LangGraph (JavaScript và Python)**: [@hibira-io/approvals](https://hibira.io/vi/guides/approvals) có công thức cho từng loại: viết `needsApproval` hoặc `requireApproval` thành hàm xét theo câu lệnh, và hộp thư phê duyệt trong ứng dụng của bạn hiển thị bản xem trước dưới mỗi lệnh gọi.
- **Laravel**: [filament-approvals](https://hibira.io/vi/guides/filament-approvals) xin phê duyệt ngay trong bảng Filament; README có công cụ `RunSql` cho Laravel AI SDK.
- **[n8n](https://hibira.io/vi/guides/n8n), [Make](https://hibira.io/vi/guides/make), [Activepieces](https://hibira.io/vi/guides/activepieces) và Dify**: một bước mã phân loại câu lệnh, bước phê duyệt của Hibira hỏi quản lý và chờ, còn bước cơ sở dữ liệu chỉ nối vào nhánh phê duyệt. README của từng công cụ có quy trình đầy đủ.
- **Claude, Cursor và các ứng dụng MCP khác**: `request_approval` của [MCP server](https://hibira.io/vi/mcp-server) xin phê duyệt theo cùng cách ngay trong cuộc trò chuyện.
- **Mọi database MCP server**: [mcp-approval-gate](https://github.com/hibira-io/mcp-approval-gate) bọc DBHub, postgres-mcp, Supabase MCP và các server khác mà không cần sửa mã.
