Hướng dẫn · 10 phút đọc

Vì sao chúng tôi xây dựng lại sổ đăng ký bản vẽ của Zunavi từ đầu

Hệ thống cũ đứng sau Zunavi theo dõi phiên bản 'mới nhất' của bản vẽ bằng cách xóa hết rồi chèn lại toàn bộ dòng dữ liệu. Đây là lý do cách làm đó thất bại, và cách PostgreSQL, Prisma cùng một cờ boolean duy nhất thay thế nó vĩnh viễn.

Vì sao chúng tôi xây dựng lại sổ đăng ký bản vẽ của Zunavi từ đầu

Câu hỏi mà ai cũng hỏi: Sao không vá hệ thống cũ?

Khi chúng tôi nói về Zunavi — nền tảng quản lý bản vẽ chúng tôi đang xây dựng cho các nhà sản xuất — câu hỏi đầu tiên từ bất kỳ ai am hiểu kỹ thuật thường giống nhau: nếu đã có một hệ thống đang hoạt động, tại sao lại xây lại từ đầu thay vì vá nó?

Câu trả lời trung thực là hệ thống mà Zunavi cuối cùng sẽ thay thế chưa bao giờ được xây dựng để có thể vá thành thứ mà các nhà sản xuất thực sự cần ngày nay. Đây là một case study về cách nợ kỹ thuật âm thầm tích tụ trong phần mềm sản xuất cho đến khi chi phí mở rộng nó vượt quá chi phí xây lại từ đầu.

Hệ thống cũ thực sự trông như thế nào

Backend cũ chạy trên MS SQL Server với TypeORM làm tầng ORM. Riêng điều đó không có gì bất thường. Điều bất thường — và khiến việc bảo trì trở nên khó khăn — là tên cột vật lý được đặt bằng tiếng Nhật: 図番 (số hiệu bản vẽ), 来歴 (lịch sử/phả hệ phiên bản), 材質 (vật liệu), và hàng chục cột khác. Bảng cốt lõi draws đã phình to đến hơn 60 cột: dung sai, tỷ trọng riêng, số đóng dấu, dung sai lắp ghép, thông số bu-lông mắt, kích thước gờ treo và gờ lắp đặt, dòng sản phẩm — các trường được tích lũy qua nhiều năm khi các yêu cầu mới được gắn thêm vào một bảng rộng duy nhất thay vì được mô hình hóa như các khái niệm riêng của chúng.

Những bảng rộng, được ghép tùy tiện không tự động sai. Rất nhiều hệ thống production có chúng và vẫn hoạt động tốt. Vấn đề thực sự xuất hiện ở cách hệ thống theo dõi phiên bản nào của một bản vẽ là phiên bản hiện tại.

Kiểu “Xóa hết rồi chèn lại”

Schema cũ giữ một bảng riêng, latestDraws, được cho là luôn chứa chính xác phiên bản mới nhất của mọi bản vẽ. Nó không được suy ra từ một câu truy vấn — nó là một cache được duy trì thủ công, được đồng bộ bằng cách xóa mọi dòng trong bảng rồi chèn lại tập hợp hiện tại, bọc trong một transaction. Trên đó còn có Q_Draws, một view SQL mà code phía sau đọc từ đó, che giấu đi sự thật rằng dữ liệu “mới nhất” bên dưới thực chất là một bản chụp mà ai đó phải nhớ để xây dựng lại đúng cách, mỗi lần, theo đúng thứ tự (revision DESC, registerDate DESC).

Cách làm này hoạt động tốt cho đến khi nó không còn hoạt động nữa. Nó dễ vỡ khi có ghi đồng thời, tốn kém ở bất kỳ quy mô thực tế nào (viết lại toàn bộ bảng chỉ để phản ánh một phiên bản mới của một bản vẽ), và — quan trọng nhất — nó đặt tính đúng đắn của “bản vẽ hiện tại là gì” vào tay code ứng dụng phải nhớ chạy đúng bước đồng bộ, thay vì vào chính mô hình dữ liệu. Một hệ thống lưu trữ chính thức cho bản vẽ kỹ thuật mà có thể âm thầm trôi lệch khỏi chính nó không phải là một lỗi nhỏ; đó là loại lỗi ăn mòn niềm tin vào toàn bộ nền tảng.

Còn có một dấu vết nhỏ nhưng đáng nói trong schema cũ: một cột chết tên là func_history.mongoId, sót lại từ thời kỳ trước đó khi một phần hệ thống chạy trên MongoDB trước khi di chuyển sang SQL Server. Nó không còn làm gì nữa. Nó chỉ ở đó — một dấu tích hóa thạch của một cuộc di chuyển trước đó cũng chưa dọn dẹp hoàn toàn sau khi hoàn tất.

Chúng tôi đang xây dựng gì thay thế

Backend mới của Zunavi chạy trên PostgreSQL với Prisma làm ORM. Riêng việc chuyển đổi này đã mang lại một schema khai báo (declarative), các migration thực sự, và kiểu dữ liệu được sinh tự động thay vì việc đồng bộ thủ công entity-sang-bảng của TypeORM — nhưng thay đổi schema thực sự quan trọng nằm ở cách chúng tôi mô hình hóa các phiên bản.

Thay vì một bảng draws cộng với một cache latestDraws được đồng bộ riêng, mô hình mới tách biệt các mối quan tâm một cách rõ ràng: một aggregate root Drawing (chính số hiệu bản vẽ) và một bảng con DrawingRevision (mỗi phiên bản của bản vẽ đó). “Mới nhất” không còn là một bảng riêng nữa — nó là một cờ boolean, DrawingRevision.isLatest, nằm ngay trên dòng phiên bản mà nó mô tả.

Khi một bản vẽ có phiên bản mới, toàn bộ thao tác diễn ra trong một transaction cơ sở dữ liệu duy nhất: cờ isLatest của phiên bản trước chuyển thành false, và phiên bản mới được chèn vào với isLatest = true. Không có bước xóa-hết-rồi-chèn-lại. Không có bảng cache riêng có thể âm thầm không khớp với nguồn dữ liệu gốc. Một ràng buộc duy nhất — @@unique([drawingId, revision]) — khiến việc chèn một số phiên bản trùng lặp cho cùng một bản vẽ trở nên bất khả thi về mặt cấu trúc, điều mà hệ thống cũ không có cơ chế bảo vệ tương đương.

Nếu sau này cần một view tối ưu cho việc đọc để “cho tôi phiên bản mới nhất của mọi bản vẽ”, đó là một view PostgreSQL đơn giản (DISTINCT ON (drawing_id) ... ORDER BY revision DESC) — được tính toán khi đọc, chứ không phải được duy trì thủ công mỗi lần ghi.

Phần dọn dẹp còn lại

Cùng một triết lý đó mở rộng ra ngoài phần phiên bản. Năm bảng func_* riêng biệt của hệ thống cũ dành cho bookmark, lịch sử xem, ghi chú, ghi chú hành động, và xác nhận thay đổi được gộp lại thành một bảng UserEvent duy nhất với trường phân loại type. Tệp đính kèm bản vẽ và tệp đính kèm linh kiện — trước đây là hai hệ thống phân cấp entity tương tự nhưng riêng biệt — trở thành một mô hình Asset duy nhất, được đánh khóa theo section, với cùng hành vi xóa mềm-khi-thay-thế mà hệ thống cũ dùng cho file PDF. Linh kiện và sự chia tách parts/latestParts cũ được hợp nhất thành một mô hình Item duy nhất với theo dõi mục đích sử dụng, thay vì một bảng “mới nhất” song song thứ hai cần logic đồng bộ riêng.

Hơn 60 cột của hệ thống cũ cũng không bị vứt bỏ hàng loạt — những cột có ý nghĩa rõ ràng, tần suất sử dụng cao (số hiệu bản vẽ, tên linh kiện, vật liệu, kích thước, người phụ trách, trạng thái, phiên bản) trở thành các cột có kiểu dữ liệu chính thức trên DrawingRevision; phần đuôi dài các trường tần suất thấp, biến đổi được chuyển vào cột JSONB DrawingRevision.extra, nơi chúng vẫn có thể truy vấn được và sau này, nếu hóa ra quan trọng, có thể được nâng cấp thành cột thực sự.

Đây không phải là vấn đề riêng của chúng tôi

Không có điều nào trong số này là vấn đề riêng của một công cụ nội bộ cụ thể. Đây là hình dạng chuẩn mà nợ kỹ thuật mang trong phần mềm sản xuất tồn tại lâu dài, và các bài viết ngành về hiện đại hóa hệ thống cũ mô tả cùng một khuôn mẫu từ góc nhìn bên ngoài. Một ước tính cho rằng nợ kỹ thuật chiếm 20–40% tổng giá trị công nghệ của một công ty — và việc di chuyển một hệ thống cũ đầy nợ mà không dọn dẹp nó chỉ đơn giản là mang nợ đó đi tiếp, với cùng chi phí liên tục (IT Convergence). Các bài viết chuyên về sản xuất mô tả trực tiếp cơ chế đứng sau con số đó: các hệ thống ERP và cơ sở dữ liệu lâu năm tích lũy “nhiều phụ thuộc chưa được ghi lại”, nên chỉ cần thay đổi cấu trúc của một bảng là các quy trình tưởng chừng không liên quan bị hỏng, và các script được viết như giải pháp tạm thời âm thầm trở thành một phần cố định của vận hành hàng ngày trong nhiều năm (Softacom). Các hướng dẫn di chuyển từ SQL Server sang PostgreSQL cũng liệt kê điểm ma sát lặp đi lặp lại tương tự: logic nghiệp vụ bị chôn trong stored procedure và trigger, gắn chặt với hành vi đặc thù của SQL Server — T-SQL so với PL/pgSQL, collation, xử lý ngoại lệ — mà các công cụ chuyển đổi tự động hiếm khi chuyển đổi đáng tin cậy đối với bất cứ logic nào không tầm thường (SourceFuse). Kiểu “xóa hết rồi chèn lại” của latestDraws và view Q_Draws chính xác là loại phụ thuộc chưa được ghi lại đó — một cách xử lý hợp lý khi xét riêng lẻ, trong một hệ thống mà không ai cố tình để nó trở nên mong manh.

Vì sao điều này quan trọng vượt ra ngoài Zunavi

Đây không phải là câu chuyện về việc SQL Server tệ hay PostgreSQL tốt. Đây là câu chuyện về điều gì xảy ra khi “phiên bản hiện tại của sự thật” được triển khai như một quy trình thay vì một ràng buộc. Hệ thống cũ không bị hỏng vì ai đó viết code cẩu thả — nó là kết quả tích lũy của nhiều năm những quyết định gia tăng hợp lý mà chưa bao giờ được xem xét lại. Đó chính xác là cách nợ kỹ thuật hoạt động trong phần mềm sản xuất tồn tại lâu dài: mỗi bản vá riêng lẻ trông ổn khi xét độc lập, và đống nợ đó chỉ trở nên rõ ràng khi bạn cố xây thứ gì đó mới lên trên nó.

Zunavi hiện vẫn đang trong quá trình phát triển tích cực và chưa được phát hành — bài viết này cố tình không phải là kiểu “hãy xem chúng tôi đã ra mắt gì”. Đây là cái nhìn về quyết định kỹ thuật cụ thể mà chúng tôi đã đưa ra và lý do, dành cho bất kỳ ai đang đối mặt với loại vấn đề tương tự trong hệ thống của chính họ.


Đọc thêm về cách chúng tôi tiếp cận Zunavi: vì sao feature flag “certain vs optional” quan trọng với dữ liệu kỹ thuật, vì sao chúng tôi xây dựng ưu tiên on-premise cho các nhà sản xuất bán dẫn, hoặc bài giới thiệu gốc về Zunavi. Đang xử lý một cuộc di chuyển tương tự, hoặc muốn nhận cập nhật khi Zunavi phát triển? Liên hệ [email protected].

Hãy cùng xây dựng

Bạn có dự án? Chúng tôi phát triển ứng dụng iOS, nền tảng web và giải pháp AI từ studio tại Nhật Bản.

Liên hệ

Bài viết liên quan