本次实验的主要任务是完成期末案例的**数据访问层**（Data Access Object, DAO）开发和其单元测试，并且再次熟悉单元测试的使用方法。



## 准备工作

{{< admonition info 环境准备>}}
在开始今天的任务之前，请确保你已经完成了前两天的任务，包括项目框架搭建和实体类的开发等。
{{< /admonition >}}

### 注册账号并建立代码仓库

[Gitea](https://git.seahi.me)

注册一个账号，成功后，点击占上角的+创建仓库：

![](https://static.seahi.me/2024/12/202412191440855.png)

按下列信息填写：

- 仓库名：stu00，把00换成自己学号的后两位
- 仓库描述：填写姓名
- 可见性：私有

![](https://static.seahi.me/2024/12/202412200844243.png)


### 安装Git工具

[下载](https://static.seahi.me/2024/12/Git-2.47.1-64-bit.exe)

### 解压项目

将上节课保存好的项目解压到桌面上，最终效果如下：

![](https://static.seahi.me/2024/12/202412191438488.png)

在代码目录点击右键，选择 Open Git Bash here（意思是“在这里打开Git Bash”）

![](https://static.seahi.me/2024/12/202412191439007.png)

### 提交项目

**在刚刚打开的 Git Bash 中**执行下列命令：

```bash
git init
git checkout -b main
git add .
git commit -m "完成Day2"
git remote add origin https://git.seahi.me/用户名/stu00.git # 00换成学号
git push -u origin main
```

执行完成后，刷新 Gitea 网页，可以看到自己的代码

## 任务一：DAO层接口开发

在本任务中，我们将开发数据访问层（DAO）的接口。**DAO层是连接业务逻辑和数据库的桥梁，通过定义统一的接口规范，我们可以确保数据访问的一致性和可维护性。**

### 需要开发的接口

{{< admonition info 提示>}}
通过代码中的包名来判断该文件应该处于哪个目录。
{{< /admonition >}}

1. **BaseDAO接口**

基础DAO接口，该接口规定通用的CRUD操作应该包含哪些操作。其他具体的DAO接口应该继承自该接口。

{{< admonition quote >}}
CRUD的含义是：
- **C** 创建 Create
- **R** 读取 Read
- **U** 更新 Update
- **D** 删除 Delete
  CRUD即平时说的增、删、改、查。
  {{< /admonition >}}

```java
package dao;

import java.util.List;

/**
 * 基础DAO接口，定义通用的CRUD操作
 * @param <T> 实体类型
 */
public interface BaseDAO<T> {
    /**
     * 插入一条记录
     * @param entity 实体对象
     * @return 影响的行数
     */
    int insert(T entity);

    /**
     * 根据ID删除记录
     * @param id 主键ID
     * @return 影响的行数
     */
    int deleteById(int id);

    /**
     * 更新记录
     * @param entity 实体对象
     * @return 影响的行数
     */
    int update(T entity);

    /**
     * 根据ID查询记录
     * @param id 主键ID
     * @return 实体对象
     */
    T findById(int id);

    /**
     * 查询所有记录
     * @return 实体对象列表
     */
    List<T> findAll();
}

```

2. **StudentDAO接口**

StudentDAO 接口应该继承 BaseDAO 接口，同时扩展学生相关的数据库操作。

```java
package dao;

import model.Student;
import java.util.List;

/**
 * 学生DAO接口
 */
public interface StudentDAO extends BaseDAO<Student> {
    /**
     * 根据学号查询学生
     * @param studentId 学号
     * @return 学生对象
     */
    Student findByStudentId(String studentId);
    
    /**
     * 根据姓名模糊查询学生
     * @param name 学生姓名
     * @return 学生列表
     */
    List<Student> findByNameLike(String name);
}
```

3. **TeacherDAO接口**

教师的 DAO 接口应该继承 BaseDAO 接口，同时扩展教师相关的数据库操作。

```java
package dao;

import model.Teacher;
import java.util.List;

/**
 * 教师DAO接口
 */
public interface TeacherDAO extends BaseDAO<Teacher> {
    /**
     * 根据工号查询教师
     * @param teacherId 教师工号
     * @return 教师对象
     */
    Teacher findByTeacherId(String teacherId);
}
```

4. **LeaveRequestDAO接口**

```java
package dao;

import model.LeaveRequest;
import model.ApprovalStatus;
import java.util.Date;
import java.util.List;

/**
 * 请假申请DAO接口
 */
public interface LeaveRequestDAO extends BaseDAO<LeaveRequest> {
    /**
     * 根据学生ID查询请假记录
     * @param studentId 学生ID
     * @return 请假记录列表
     */
    List<LeaveRequest> findByStudentId(int studentId);

    /**
     * 根据审批状态查询请假记录
     * @param status 审批状态
     * @return 请假记录列表
     */
    List<LeaveRequest> findByStatus(ApprovalStatus status);


    /**
     * 更新请假申请状态
     * @param id 请假申请ID
     * @param status 新状态
     * @param approverComment 审批意见
     * @return 影响的行数
     */
    int updateStatus(int id, ApprovalStatus status, String approverComment);
}
```



## 任务二：StudentDAOImpl类开发


在完成了DAO层接口的定义后，我们需要**实现这些接口**。本任务将以`StudentDAOImpl`为例，详细讲解如何实现一个完整的DAO实现类。`StudentDAOImpl`类需要实现`StudentDAO`接口，提供对学生数据的具体数据库操作实现。


### 1. 创建类基本结构

```java
package dao.impl;

/**
 * StudentDAO接口的实现类，提供对学生数据的访问操作
 * 实现了StudentDAO接口定义的所有方法，包括基础的CRUD操作和特定的查询方法
 * 使用JDBC与数据库进行交互，所有数据库操作都包含适当的异常处理
 */
public class StudentDAOImpl implements StudentDAO {
    // TODO 实现StudentDAO接口的所有方法
}
```

### 2. 实现insert方法

```java
/**
 * 插入一条学生记录
 * 将Student对象的信息保存到数据库中，并获取生成的主键ID
 * 
 * @param student 要插入的学生对象，包含学生的详细信息
 * @return 影响的行数，插入成功返回1，失败返回0
 */
@Override
public int insert(Student student) {
    String sql = "INSERT INTO students (student_id, name, class_name, contact, college, major, password) VALUES (?, ?, ?, ?, ?, ?, ?)";
	try (Connection conn = DatabaseUtil.getConnection();
		 PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
		
		// 设置预处理语句的参数
		stmt.setString(1, student.getStudentId());
		stmt.setString(2, student.getName());
		stmt.setString(3, student.getClassName());
		stmt.setString(4, student.getContact());
		stmt.setString(5, student.getCollege());
		stmt.setString(6, student.getMajor());
		stmt.setString(7, student.getPassword());
		
		int affectedRows = stmt.executeUpdate();
		if (affectedRows == 0) {
			return 0;
		}

		// 获取自动生成的主键
		try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
			if (generatedKeys.next()) {
				student.setId(generatedKeys.getInt(1));
			}
		}
		return affectedRows;
	} catch (SQLException e) {
		e.printStackTrace();
		return 0;
	}
}
```

{{< admonition info 关于注解>}}
@Override是一个注解，作用是**表示当前方法重写了父类的方法**。
它的主要作用是：
1. 防止写错方法名：如果方法名写错了，编译器会报错提醒你
2. 提高代码可读性：让其他程序员一眼就能看出这是一个重写的方法
   {{< /admonition >}}


### 3. 实现查询方法

```java
/**
 * 根据ID查询学生信息
 * 
 * @param id 要查询的学生ID
 * @return 如果找到则返回学生对象，否则返回null
 */
@Override
public Student findById(int id) {
    String sql = "SELECT * FROM students WHERE id = ?";
    try (Connection conn = DatabaseUtil.getConnection();
         PreparedStatement stmt = conn.prepareStatement(sql)) {
        
        stmt.setInt(1, id);
        ResultSet rs = stmt.executeQuery();
        
        if (rs.next()) {
            return mapResultSetToStudent(rs);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return null;
}
```

仿照以上示例，完成下列两个方法：

```java
/**
 * 查询所有学生记录
 * 
 * @return 包含所有学生对象的列表，如果没有记录则返回空列表
 */
@Override
public List<Student> findAll() {
	List<Student> students = new ArrayList<>();
	String sql = "SELECT * FROM students";
	try (Connection conn = DatabaseUtil.getConnection();
		 Statement stmt = conn.createStatement();
		 ResultSet rs = stmt.executeQuery(sql)) {
		
		while (rs.next()) {
			students.add(mapResultSetToStudent(rs));
		}
	} catch (SQLException e) {
		e.printStackTrace();
	}
	return students;
}

/**
 * 根据姓名模糊查询学生信息
 * 
 * @param name 要查询的学生姓名（支持模糊查询）
 * @return 符合条件的学生对象列表，如果没有匹配则返回空列表
 */
@Override
public List<Student> findByNameLike(String name) {
	List<Student> students = new ArrayList<>();
	String sql = "SELECT * FROM students WHERE name LIKE ?";
	try (Connection conn = DatabaseUtil.getConnection();
		 PreparedStatement stmt = conn.prepareStatement(sql)) {
		
		stmt.setString(1, "%" + name + "%");
		ResultSet rs = stmt.executeQuery();
		
		while (rs.next()) {
			students.add(mapResultSetToStudent(rs));
		}
	} catch (SQLException e) {
		e.printStackTrace();
	}
	return students;
}

/**
 * 根据学号查询学生信息
 * 
 * @param studentId 要查询的学号
 * @return 如果找到则返回学生对象，否则返回null
 */
@Override
public Student findByStudentId(String studentId) {
	// TODO 根据数据库中表的定义，完成该方法
}
```

### 4. 实现ResultSet映射方法

```java
/**
 * 将ResultSet映射为Student对象
 * 从结果集中提取数据并创建Student实例
 * 
 * @param rs 包含学生数据的ResultSet对象
 * @return 映射后的Student对象
 * @throws SQLException 如果访问ResultSet时发生错误
 */
private Student mapResultSetToStudent(ResultSet rs) throws SQLException {
	Student student = new Student();
	student.setId(rs.getInt("id"));
	student.setStudentId(rs.getString("student_id"));
	student.setName(rs.getString("name"));
	student.setClassName(rs.getString("class_name"));
	student.setContact(rs.getString("contact"));
	student.setCollege(rs.getString("college"));
	student.setMajor(rs.getString("major"));
	student.setPassword(rs.getString("password"));
	return student;
}
```

## 任务三：单元测试

将下列文件放置在 `src/test/dao/impl` 目录中，检查测试是否通过

[StudentDAOImplTest.java](https://static.seahi.me/2024/12/StudentDAOImplTest.java)


## 附

### 如何获取课上代码

1. 确保电脑已安装Git
2. 选择一个文件夹存放代码
3. 在该文件夹内部右键，选择"open Git Bash Here"
4. 输入命令：

```bash
# 第一次获取
git clone https://git.seahi.me/用户名/stu学号.git
# 后期再次获取时，直接进入到项目目录，执行下列命令即可
git pull 
```

> 注意：把"学号"替换成你的实际学号后两位

### 如何提交修改的代码

当你在自己电脑上修改了代码后，需要提交到仓库：

1. 打开Git Bash
2. 输入以下命令：

```
git add .
git commit -m "修改说明"
git push
```

{{< admonition tip "提示">}}
- 第一次使用时需要设置用户名和密码
- 建议经常提交代码，避免代码丢失
  {{< /admonition >}}