Xây dựng ứng dụng CRUD với Bootstrap, Ajax và Spring MVC + MyBatis

Khi làm việc với các dự án Java web, việc tích hợp nhiều công nghệ thường gặp khó khăn ở khâu cấu hình. Bài viết này hướng dẫn cách xây dựng hệ thống quản lý nhân viên sử dụng Spring MVC kết hợp MyBatis (thay vì Spring JDBC), Bootstrap cho giao diện và Ajax để xử lý bất đồng bộ.

Thiết kế cơ sở dữ liệu

Tạo database hr_management với hai bảng chính: phòng ban và nhân viên.

-- Tạo database
CREATE DATABASE IF NOT EXISTS hr_management 
CHARACTER SET utf8mb4 
COLLATE utf8mb4_unicode_ci;

USE hr_management;

-- Bảng phòng ban
CREATE TABLE department (
    dept_no INT AUTO_INCREMENT PRIMARY KEY,
    dept_title VARCHAR(50) NOT NULL COMMENT 'Tên phòng ban'
) ENGINE=InnoDB;

-- Bảng nhân viên
CREATE TABLE staff (
    staff_no INT AUTO_INCREMENT PRIMARY KEY,
    fullname VARCHAR(100) NOT NULL COMMENT 'Họ tên',
    sex CHAR(1) COMMENT 'Giới tính: M/F',
    mail VARCHAR(100) COMMENT 'Email liên hệ',
    dept_no INT COMMENT 'Mã phòng ban',
    FOREIGN KEY (dept_no) REFERENCES department(dept_no)
) ENGINE=InnoDB;

Cấu trúc dự án Maven

File pom.xml với các dependency cần thiết:

<dependencies>
    <!-- Spring MVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.20</version>
    </dependency>
    
    <!-- MyBatis + Spring -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.7</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
    </dependency>
    
    <!-- MySQL Connector -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.29</version>
    </dependency>
    
    <!-- Connection Pool -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.11</version>
    </dependency>
    
    <!-- JSON processing -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.3</version>
    </dependency>
</dependencies>

Cấu hình Spring

db.properties - Thông tin kết nối:

db.url=jdbc:mysql://127.0.0.1:3306/hr_management?useSSL=false&serverTimezone=Asia/Ho_Chi_Minh&allowPublicKeyRetrieval=true
db.driver=com.mysql.cj.jdbc.Driver
db.account=root
db.secret=your_password

spring-context.xml - Cấu hình bean:

<context:property-placeholder location="classpath:db.properties"/>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="url" value="${db.url}"/>
    <property name="driverClassName" value="${db.driver}"/>
    <property name="username" value="${db.account}"/>
    <property name="password" value="${db.secret}"/>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    <property name="typeAliasesPackage" value="com.demo.entity"/>
</bean>

<mybatis-spring:scan base-package="com.demo.dao"/>

Entity và Mapper

Class Staff.java:

public class Staff {
    private Integer staffNo;
    private String fullname;
    private String sex;
    private String mail;
    private Integer deptNo;
    private Department department; // Đối tượng phòng ban
    
    // Getters, setters, constructors
}

Interface StaffDao.java:

@Repository
public interface StaffDao {
    List<Staff> selectWithDept();
    int insertSelective(Staff record);
    int deleteByPrimaryKey(Integer staffNo);
    int updateByPrimaryKeySelective(Staff record);
    Staff selectByPrimaryKey(Integer staffNo);
}

File mapper StaffMapper.xml:

<mapper namespace="com.demo.dao.StaffDao">
    <resultMap id="withDept" type="Staff">
        <id column="staff_no" property="staffNo"/>
        <result column="fullname" property="fullname"/>
        <result column="sex" property="sex"/>
        <result column="mail" property="mail"/>
        <association property="department">
            <id column="dept_no" property="deptNo"/>
            <result column="dept_title" property="deptTitle"/>
        </association>
    </resultMap>
    
    <select id="selectWithDept" resultMap="withDept">
        SELECT s.*, d.dept_title 
        FROM staff s 
        LEFT JOIN department d ON s.dept_no = d.dept_no
    </select>
</mapper>

Controller xử lý Ajax

@RestController
@RequestMapping("/api/staff")
public class StaffController {
    
    @Autowired
    private StaffDao staffDao;
    
    @GetMapping("/list")
    public ResponseEntity<List<Staff>> fetchAll() {
        return ResponseEntity.ok(staffDao.selectWithDept());
    }
    
    @PostMapping("/add")
    public ResponseEntity<String> create(@RequestBody Staff staff) {
        int rows = staffDao.insertSelective(staff);
        return rows > 0 ? ResponseEntity.ok("SUCCESS") : 
                         ResponseEntity.status(500).body("FAILED");
    }
    
    @PutMapping("/update/{id}")
    public ResponseEntity<String> modify(@PathVariable Integer id, 
                                          @RequestBody Staff staff) {
        staff.setStaffNo(id);
        return staffDao.updateByPrimaryKeySelective(staff) > 0 ?
               ResponseEntity.ok("UPDATED") : 
               ResponseEntity.badRequest().body("ERROR");
    }
    
    @DeleteMapping("/remove/{id}")
    public ResponseEntity<String> erase(@PathVariable Integer id) {
        return staffDao.deleteByPrimaryKey(id) > 0 ?
               ResponseEntity.ok("DELETED") : 
               ResponseEntity.notFound().build();
    }
}

Giao diện Bootstrap + Ajax

Trang JSP với Bootstrap 5:

<!DOCTYPE html>
<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <h3>Quản lý nhân sự</h3>
        <button class="btn btn-primary mb-3" onclick="openModal()">Thêm mới</button>
        
        <table class="table table-bordered">
            <thead>
                <tr>
                    <th>Mã NV</th>
                    <th>Họ tên</th>
                    <th>Giới tính</th>
                    <th>Phòng ban</th>
                    <th>Thao tác</th>
                </tr>
            </thead>
            <tbody id="dataBody"></tbody>
        </table>
    </div>

<script>
// Load dữ liệu khi trang ready
document.addEventListener('DOMContentLoaded', loadData);

function loadData() {
    fetch('/api/staff/list')
        .then(r => r.json())
        .then(data => {
            const tbody = document.getElementById('dataBody');
            tbody.innerHTML = data.map(item => `
                <tr>
                    <td>${item.staffNo}</td>
                    <td>${item.fullname}</td>
                    <td>${item.sex === 'M' ? 'Nam' : 'Nữ'}</td>
                    <td>${item.department?.deptTitle || '-'}</td>
                    <td>
                        <button class="btn btn-sm btn-warning" onclick="editRow(${item.staffNo})">Sửa</button>
                        <button class="btn btn-sm btn-danger" onclick="removeRow(${item.staffNo})">Xóa</button>
                    </td>
                </tr>
            `).join('');
        });
}

function removeRow(id) {
    if (!confirm('Xác nhận xóa?')) return;
    
    fetch(`/api/staff/remove/${id}`, { method: 'DELETE' })
        .then(r => r.ok ? loadData() : alert('Xóa thất bại'));
}

function saveData(payload) {
    const method = payload.staffNo ? 'PUT' : 'POST';
    const url = payload.staffNo ? `/api/staff/update/${payload.staffNo}` : '/api/staff/add';
    
    fetch(url, {
        method: method,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
    }).then(r => r.ok && loadData());
}
</script>
</body>
</html>

Lưu ý khi triển khai

  • Phiên bản MySQL 8.x yêu cầu driver com.mysql.cj.jdbc.Driver và tham số serverTimezone
  • Thêm allowPublicKeyRetrieval=true nếu gặp lỗi authentication với MySQL 8
  • Đảm bảo dispatcher servlet mapping trong web.xml hoặc class WebAppInitializer
  • Thư mục mapper cần được build vào classpath (cấu hình trong pom.xml)

Thẻ: Spring MVC mybatis Bootstrap ajax Maven

Đăng vào ngày 3 tháng 8 lúc 20:57