TL;DR: Upsert là "có rồi thì cập nhật, chưa có thì thêm mới". Muốn làm đúng trên LarkBase cần ba thứ: một ID nghiệp vụ ổn định để so khớp, record_id của LarkBase để update đúng dòng, và payload chỉ chứa những trường đến từ nguồn — nhờ vậy các cột người dùng tự thêm tay không bị đè.
Bài toán
Bạn sync đơn hàng từ hệ thống bán hàng vào LarkBase mỗi giờ. Ba tình huống xảy ra cùng lúc:
- Đơn
8051đã có trong bảng, lần sync này đổi trạng thái từ "đang xử lý" sang "huỷ" → cần update. - Đơn
8052chưa có → cần insert. - Cột "Ghi chú CSKH" mà nhân viên tự gõ tay cho đơn
8051→ phải giữ nguyên.
Làm sai một trong ba, hậu quả thấy ngay: hoặc bảng đầy bản ghi trùng, hoặc nhân viên mất công ghi chú sau mỗi lần sync — và lần sau họ sẽ không dùng hệ thống nữa.
Mấu chốt: một cột ID định danh
Mỗi bản ghi cần một ID nghiệp vụ ổn định — mã đơn, mã khách, mã chứng từ. Một dòng một ID, không trùng.
⚠️ Warning: Không có ID ổn định thì không có upsert, chỉ có insert trùng lặp. Đừng dùng tổ hợp "tên + ngày" làm khoá — khách đổi tên một chữ là hệ thống coi như bản ghi mới.
Cần phân biệt rõ hai loại ID, đây là chỗ hay nhầm nhất:
| Loại | Ví dụ | Dùng để làm gì |
|---|---|---|
| ID nghiệp vụ | 8051 (mã đơn) |
So khớp xem bản ghi đã tồn tại chưa |
record_id |
recXXXXXXXX |
Chỉ đúng dòng cần update trong LarkBase |
Update phải đi theo record_id. Nhầm sang ID nghiệp vụ là sửa nhầm dòng khác.
Luồng upsert trong n8n
Bước 1: Đọc dữ liệu hiện có
Lấy toàn bộ bản ghi trong bảng đích. Nhớ xử lý phân trang khi đọc — đọc thiếu dòng thì bản ghi đã tồn tại sẽ bị coi là mới và bạn nhận về một đống bản ghi trùng.
Bước 2: So khớp theo ID
Dùng node Code dựng một map từ ID nghiệp vụ sang record_id, rồi chia dữ liệu nguồn thành hai nhóm: cần update và cần insert.
Bước 3: Update và insert riêng
Hai nhánh gọi hai endpoint khác nhau. Update cần record_id, insert chỉ cần fields.
Node Code cho bước so khớp:
// $('Đọc LarkBase').all() — dữ liệu đang có trong bảng
const existing = new Map();
for (const item of $('Đọc LarkBase').all()) {
const businessId = item.json.fields['Mã đơn'];
if (businessId) existing.set(String(businessId), item.json.record_id);
}
const toUpdate = [];
const toInsert = [];
for (const item of $input.all()) {
const row = item.json;
const recordId = existing.get(String(row.ma_don));
// Chỉ đưa vào payload những trường đến từ nguồn.
// Cột nào không có ở đây thì LarkBase giữ nguyên giá trị cũ.
const fields = {
'Mã đơn': String(row.ma_don),
'Trạng thái': row.trang_thai,
'Tổng tiền': Number(row.tong_tien),
};
if (recordId) {
toUpdate.push({ json: { _action: 'update', record_id: recordId, fields } });
} else {
toInsert.push({ json: { _action: 'insert', fields } });
}
}
return [...toUpdate, ...toInsert];
Code trên trả cả hai nhóm kèm trường _action — đặt node IF phía sau lọc theo _action để rẽ nhánh update / insert. Điểm quan trọng nằm ở chỗ khác: payload chỉ chứa trường đến từ nguồn.
Vì sao cột tự thêm tay không bị mất
LarkBase cập nhật theo kiểu ghi đè từng trường có trong payload, không phải thay cả dòng. Cột "Ghi chú CSKH" không nằm trong fields bạn gửi lên, nên nó không bị đụng tới.
Đây là lý do phải liệt kê tường minh từng trường thay vì quăng nguyên object nguồn lên. Quăng nguyên object là lúc nào đó nguồn thêm một trường trùng tên cột nội bộ, và ghi chú của nhân viên biến mất.
Ba chỗ hay vấp
Sai kiểu dữ liệu field. Payload phải khớp định dạng field của LarkBase. Cột số mà gửi chuỗi là lỗi ngay; cột ngày cần timestamp mili giây chứ không phải chuỗi "2026-08-18". Kiểm vài bản ghi mẫu trước khi chạy cả lô.
Đụng rate limit. Bảng vài nghìn dòng mà bắn update từng dòng liên tục sẽ bị chặn. Dùng Loop Over Items chia lô, hoặc dùng endpoint batch để gửi nhiều bản ghi trong một lần gọi.
Không có retry. Sync nửa chừng rồi rớt mạng để lại trạng thái nửa vời. Bật retry on fail trên node gọi API, và gắn error workflow để biết lần chạy nào hỏng — thay vì phát hiện qua báo cáo lệch số vào tuần sau.
Kết quả nhận được
Luồng làm đúng thì idempotent: chạy lại bao nhiêu lần cũng không sinh bản ghi trùng, trạng thái trong LarkBase luôn khớp nguồn, và phần dữ liệu người dùng tự bổ sung vẫn còn nguyên.
Đó cũng là tiêu chí để biết luồng sync đã xong hay chưa — không phải "chạy được một lần", mà là "chạy lại lần thứ mười vẫn ra đúng một kết quả".
ℹ️ Info: Trước đây tôi dựng luồng này bằng AnyCross. Từ 2026 chuyển sang n8n vì tự host được, xem log từng node khi lần lỗi, và không tính tiền theo lượt chạy — luồng sync mỗi giờ thì số lượt cộng lại rất nhanh.
Tự làm mất bao lâu (ước lượng)
| Việc | Người đã quen n8n | Người mới |
|---|---|---|
| Luồng upsert cho một bảng | nửa ngày | 2–3 ngày |
| Thêm chia lô, retry, cảnh báo | 2–3 giờ | 1 ngày |
| Sửa lại khi nguồn đổi cấu trúc | 1 giờ mỗi lần | nửa ngày mỗi lần |
Con số đáng chú ý là dòng cuối. Luồng upsert không phải làm một lần rồi thôi — nguồn dữ liệu đổi trường, nghiệp vụ đổi quy tắc, và mỗi lần như vậy có người phải sửa. Đó mới là chi phí thật của tự làm, không phải buổi đầu tiên dựng luồng.
Nếu luồng sync này là thứ kế toán hoặc vận hành dựa vào hằng ngày, cân nhắc thuê dựng kèm gói vận hành — để khi nó hỏng thì có người chịu trách nhiệm sửa, không phải bạn.
Xem dịch vụ automation →Bài viết hữu ích?
Chia sẻ để nhiều người biết đến!
>_ LLM-Friendly Copy
Copy as Markdown to use with ChatGPT, Claude, or other AI tools
