Phần mềm quản lý sách trên desktop
Ứng dụng desktop Java Swing quản lý kho sách: phân quyền theo role, phiếu nhập xuất cập nhật tồn kho, in PDF, dữ liệu MySQL qua JDBC.
- Java
- Java Swing
- JDBC
- MySQL
Bài tập lớn thời sinh viên của tôi: một ứng dụng desktop Java Swing quản lý kho sách của một nhà sách. Dữ liệu nằm trong MySQL, truy cập bằng JDBC thuần, không ORM.
Bốn role, ba entry point#
Phần mềm không có màn hình "tất cả trong một". Mỗi dòng trong bảng Account mang một
role, và Login.checkLogin() quyết định mở cửa sổ nào: Admin cho quản trị, NhapKho
cho nhân viên nhập, XuatKho cho nhân viên xuất. Cột status cho phép khóa tài khoản
mà không phải xóa đi. Đọc lại repo hôm nay tôi mới thấy dữ liệu mẫu còn role "Quản lý kho"
nhưng checkLogin() không có nhánh nào cho nó — một chỗ bỏ dở mà lúc đó tôi không thấy.
Ba lớp view – dao – model#
Thư mục src tách thành view (các JFrame do NetBeans sinh), model, dao và
controller. Thứ tôi thấy đáng giá nhất là dao/DAOInterface.java: một interface generic
với đúng năm phương thức insert, update, delete, selectAll, selectById. Mọi DAO
đều cài đặt nó, nên đến lúc viết PhieuXuatDAO tôi đã biết trước nó trông thế nào.
public int updateSoLuong(String id, int soluong) {
int ketQua = 0;
try {
Connection con = JDBCUtils.getConnection();
String sql = "UPDATE Sach SET stock_quantity=? WHERE id=?";
PreparedStatement pst = con.prepareStatement(sql);
pst.setInt(1, soluong);
pst.setString(2, id);
ketQua = pst.executeUpdate();
JDBCUtils.closeConnection(con);
} catch (SQLException ex) {
Logger.getLogger(SachDAO.class.getName()).log(Level.SEVERE, null, ex);
}
return ketQua;
}Mọi query trong dao/ đều đi qua PreparedStatement với tham số ?, không chỗ nào
nối string SQL từ ô nhập liệu. Đổi lại, JDBCUtils.getConnection() mở một kết nối mới cho
mỗi lần gọi DAO, với mật khẩu MySQL nằm cứng ngay trong file.
Tồn kho đi theo phiếu#
Số lượng tồn là cột stock_quantity trên bảng Sach, còn từng dòng hàng nằm ở
ChiTietPhieuNhap và ChiTietPhieuXuat, foreign key trỏ về phiếu và về sách:
ALTER TABLE `ChiTietPhieuXuat`
ADD CONSTRAINT `FK_ChiTietPhieuXuat_PhieuXuat`
FOREIGN KEY (`maPhieu`) REFERENCES `PhieuXuat` (`maPhieu`),
ADD CONSTRAINT `FK_ChiTietPhieuXuat_Sach`
FOREIGN KEY (`id`) REFERENCES `Sach` (`id`);Khi người dùng bấm xuất hàng, form ghi phiếu rồi duyệt từng dòng chi tiết và trừ tồn kho:
PhieuXuatDAO.getInstance().insert(pn);
SachDAO sdao = SachDAO.getInstance();
for (var i : CTPhieu) {
ChiTietPhieuXuatDAO.getInstance().insert(i);
sdao.updateSoLuong(i.getId(),
sdao.selectById(i.getId()).getStockQuantity() - i.getSoLuong());
}Đây là đoạn tôi muốn viết lại nhất. Ba lệnh ghi là ba transaction rời nhau: mất kết nối
giữa vòng lặp thì phiếu đã nằm trong database còn tồn kho thì sai. Cả repo không có lấy
một lần gọi setAutoCommit(false).
In phiếu và nạp phiếu từ Excel#
controller/WritePDF.java dùng iText 5 để in phiếu. Bẫy ở đây là font: font mặc định của
iText làm chữ tiếng Việt rụng hết dấu, nên tôi phải nhúng Roboto với encoding
BaseFont.IDENTITY_H. Chiều ngược lại, form nhập/xuất đọc .xlsx bằng Apache POI để nạp
một lúc nhiều dòng thay vì gõ tay từng cuốn. Đăng nhập dùng BCrypt (gensalt(12)), quên
mật khẩu thì gửi OTP qua SMTP của Gmail.
Kết quả#
- Một phần mềm chạy được đầu-cuối: đăng nhập theo role, CRUD sách, lập phiếu nhập/xuất có trừ cộng tồn kho, tìm kiếm, in PDF tiếng Việt
- Thói quen dùng
PreparedStatementvà táchview/dao/model, hai thứ tôi mang theo sang các dự án sau - Những hạn chế tôi chỉ thấy khi đọc lại: nghiệp vụ nhiều bảng mà không có transaction,
controller/SearchProduct.javagọiselectAll()rồi lọc bằngcontains()trong Java thay vì đểWHERE ... LIKEcho MySQL, và mật khẩu ứng dụng Gmail nằm nguyên trongcontroller/SendEmailSMTP.javađã đẩy lên GitHub — bài học đắt nhất của cả dự án