回顾javaweb全流程上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#固定配置文件直接复制
spring:
application:
name: SpringBoot-web-02
datasource:
url: jdbc:mysql://localhost:3306/db01
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 文件名:logback.xml info级别不那么乱,debug级别会打印很多信息 -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%logger显示日志记录器的名称, %msg表示日志消息,%n表示换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
</encoder>
</appender>

<!-- 系统文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件输出的文件名, %i表示序号 -->
<FileNamePattern>D:/tlias-%d{yyyy-MM-dd}-%i.log</FileNamePattern>
<!-- 最多保留的历史日志文件数量 -->
<MaxHistory>30</MaxHistory>
<!-- 最大文件大小,超过这个大小会触发滚动到新文件,默认为 10MB -->
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>

<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d 表示日期,%thread 表示线程名,%-5level表示级别从左显示5个字符宽度,%msg表示日志消息,%n表示换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
</encoder>
</appender>

<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/**
* JWT 令牌操作工具类 (生产级版本)
* 适配 jjwt 0.12.6+ 及 Spring Boot 环境
*
* @author YourName
*/
@Slf4j
@Component // 注册为 Spring Bean,以便注入配置
public class JwtUtils {

// --- 配置属性 (从 application.yml 读取,若无则使用默认值) ---

/**
* 密钥字符串
* 默认值: 一个安全的 32 字节随机密钥的 Base64 编码
* 建议在 application.yml 中配置: jwt.secret-key
*/
@Value("${jwt.secret-key:kG8sL9mP2nQ5rT6vW8xY0zA3bC4dE7fH}")
private String secretKeyString;

/**
* 令牌过期时间 (毫秒)
* 默认值: 12 小时 (43200000 ms)
* 建议在 application.yml 中配置: jwt.expiration
*/
@Value("${jwt.expiration:43200000}")
private Long expirationMillis;

// --- 内部运行时变量 ---

private SecretKey secretKey;

/**
* 初始化方法
* 在 Spring 容器加载 Bean 后执行,将字符串密钥转换为 SecretKey 对象
*/
@PostConstruct
public void init() {
if (!StringUtils.hasText(secretKeyString)) {
throw new IllegalArgumentException("JWT Secret Key 不能为空,请在配置文件中设置 jwt.secret-key");
}
// 将字符串转换为 SecretKey,如果长度不足 256 bits,这里会抛出 WeakKeyException
try {
this.secretKey = Keys.hmacShaKeyFor(secretKeyString.getBytes(StandardCharsets.UTF_8));
log.info("JWT Utils 初始化成功,密钥长度: {} bits", secretKey.getEncoded().length * 8);
} catch (Exception e) {
log.error("JWT 密钥初始化失败,请检查密钥长度是否至少为 256 bits (32 字节)", e);
throw new RuntimeException("JWT 密钥配置错误", e);
}
}

// ================= 核心功能方法 =================

/**
* 生成 JWT 令牌 (通用版)
*
* @param claimsMap 自定义声明信息 (如 id, username, role 等)
* @return JWT 字符串
*/
public String generateToken(Map<String, Object> claimsMap) {
if (claimsMap == null) {
claimsMap = new HashMap<>();
}

Date now = new Date();
Date expiryDate = new Date(now.getTime() + expirationMillis);

try {
return Jwts.builder()
.claims(claimsMap) // 放入自定义数据
.issuedAt(now) // 签发时间
.expiration(expiryDate) // 过期时间
.signWith(secretKey) // 签名
.compact();
} catch (Exception e) {
log.error("生成 JWT 令牌时发生错误", e);
throw new RuntimeException("生成令牌失败", e);
}
}

/**
* 生成 JWT 令牌 (便捷版 - 常用场景)
* 仅传入用户 ID 和用户名,自动构建 Claims
*
* @param id 用户 ID
* @param username 用户名
* @return JWT 字符串
*/
public String generateToken(Integer id, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("id", id);
claims.put("username", username);
return generateToken(claims);
}

/**
* 解析 JWT 令牌
*
* @param token JWT 字符串
* @return Claims 对象 (包含所有声明信息)
* @throws ExpiredJwtException 如果令牌已过期
* @throws SignatureException 如果签名验证失败 (密钥错误或数据被篡改)
* @throws MalformedJwtException 如果令牌格式不正确
* @throws IllegalArgumentException 如果令牌为 null 或空
*/
public Claims parseToken(String token) {
if (!StringUtils.hasText(token)) {
throw new IllegalArgumentException("JWT Token 不能为空");
}

try {
return Jwts.parser()
.verifyWith(secretKey) // 设置验证密钥
.build() // 构建解析器
.parseSignedClaims(token) // 解析并验证 (核心步骤)
.getPayload(); // 获取载荷
} catch (ExpiredJwtException e) {
log.warn("JWT 令牌已过期: {}", e.getMessage());
throw e; // 向上抛出,让调用者知道是过期了
} catch (SignatureException e) {
log.error("JWT 签名验证失败 (可能密钥不匹配或数据被篡改): {}", e.getMessage());
throw e;
} catch (MalformedJwtException e) {
log.error("JWT 格式错误: {}", e.getMessage());
throw e;
} catch (Exception e) {
log.error("解析 JWT 令牌时发生未知错误", e);
throw new RuntimeException("解析令牌失败", e);
}
}

// ================= 辅助便捷方法 =================

/**
* 判断令牌是否有效 (不抛出异常,只返回布尔值)
* 适用于拦截器中快速判断
*
* @param token JWT 字符串
* @return true: 有效且未过期; false: 无效或已过期
*/
public boolean isTokenValid(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
// 任何异常都视为无效
return false;
}
}

/**
* 从令牌中获取用户 ID
*
* @param token JWT 字符串
* @return 用户 ID,如果获取失败返回 null
*/
public Integer getUserIdFromToken(String token) {
try {
Claims claims = parseToken(token);
// 注意:根据生成时的类型,可能需要转换。如果是 Integer 存进去就是 Integer
Object idObj = claims.get("id");
if (idObj instanceof Integer) {
return (Integer) idObj;
} else if (idObj instanceof Number) {
return ((Number) idObj).intValue();
} else if (idObj instanceof String) {
return Integer.parseInt((String) idObj);
}
} catch (Exception e) {
// 静默失败,返回 null
}
return null;
}

/**
* 从令牌中获取用户名
*
* @param token JWT 字符串
* @return 用户名,如果获取失败返回 null
*/
public String getUsernameFromToken(String token) {
try {
Claims claims = parseToken(token);
return claims.get("username", String.class);
} catch (Exception e) {
return null;
}
}

/**
* 获取令牌的过期时间
* @param token JWT 字符串
* @return 过期时间,如果获取失败返回 null
*/
public Date getExpirationDateFromToken(String token) {
try {
Claims claims = parseToken(token);
return claims.getExpiration();
} catch (Exception e) {
return null;
}
}
}

单元测试

在pom.xml中添加JUnit依赖

1
2
3
4
5
6
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.1</version>
<scope>test</scope>
</dependency>
  • Junit单元测试规范:类名以Test结尾,方法名以test开头,每个方法都是一个测试用例。
  • 方法上添加@Test注解,标识这是一个测试方法。 规定:
    • 测试方法必须是public void类型。
    • 测试方法不能有参数。
    • 测试方法不能抛出异常。

断言

断言是单元测试中常用的一种机制,用于验证代码的行为是否符合预期。在JUnit中,断言通常用于测试方法中,用于检查测试结果是否与预期结果一致。

  • 常用的断言方法Assertions:
    • assertEquals(expected, actual, String message->这里选填有对应的重载方法):检查预期值与实际值是否相等。
    • assertTrue(condition):检查条件是否为真。
    • assertFalse(condition):检查条件是否为假。
    • assertNotNull(object):检查对象是否不为空。
    • assertNull(object):检查对象是否为空。

常见注解

  • @Test:标识这是一个测试方法。
  • @ParameterizedTest:标识这是一个参数化测试方法。(可以在一个测试方法中使用多个不同的参数进行测试)
  • @ValueSource:为参数化测试方法提供参数值。(可以指定多个参数值,每个值之间用逗号隔开)与上面@ParameterizedTest进行搭配使用
  • @BeforeEach:在每个测试方法执行前执行。
  • @AfterEach:在每个测试方法执行后执行。
  • @BeforeAll:在所有测试方法执行前执行。在每个测试方法执行前执行,用于初始化测试环境所以应该加上static关键字。
  • @AfterAll:在所有测试方法执行后执行。
  • @DisplayName:为测试类或测试方法指定自定义的显示名称。

JUnit 常见问题总结

1. JUnit单元测试的方法,是否可以声明方法形参?

  • 可以的,通过参数化测试实现
  • 使用 @ParameterizedTest + @ValueSource 组合

2. 如何实现在单元测试方法运行之前,做一些初始化操作?

  • 使用 @BeforeEach 注解:在每个测试方法执行前执行
  • 使用 @BeforeAll 注解:在所有测试方法执行前执行(需要声明为static)

3. 如何实现在单元测试方法运行之后,释放对应的资源?

  • 使用 @AfterEach 注解:在每个测试方法执行后执行
  • 使用 @AfterAll 注解:在所有测试方法执行后执行(需要声明为static)

单元测试-Maven依赖范围

  • 测试范围(test scope):
    • 仅在测试阶段有效,不会被包含在最终的可执行文件(如JAR或WAR)中。
    • 常用的测试范围依赖包括JUnit、Mockito、AssertJ等。
  • 编译范围(compile scope):
    • 编译时需要,运行时也需要。
    • 常用的编译范围依赖包括Servlet API、JSP API等。
  • 运行时范围(runtime scope):
    • 运行时需要,编译时不需要。
    • 常用的运行时范围依赖包括数据库驱动、日志框架等。
  • 提供范围(provided scope):
    • 编译时需要,运行时由容器提供。
    • 常用的提供范围依赖包括Servlet容器、JSP容器等。
      1
      2
      3
      4
      5
      6
      7
      <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.9.1</version>
      <scope>test</scope>
      <!-- 测试范围依赖,仅在测试阶段有效 -->
      </dependency>

SpringBoot快速入门

项目创建

  • 项目创建:
    • 打开Spring Initializr(https://start.spring.io/)。
    • 填写项目元数据(Group、Artifact、Name、Description、Package Name等)。
    • 选择项目类型(Maven Project或Gradle Project)。
    • 选择Java版本(如Java 17)。
    • 选择Spring Boot版本(如2.7.13)。
    • 添加所需的依赖(如Web、Data JPA、MySQL Driver等)。
    • 点击“Generate”按钮,下载项目压缩包。
  • 项目目录结构:
    • src/main/java:包含应用程序的Java源文件。
    • src/main/resources:包含应用程序的资源文件(如配置文件、静态资源等)。
    • src/test/java:包含测试用的Java源文件。
    • src/test/resources:包含测试用的资源文件。
    • pom.xml(或build.gradle):项目的构建配置文件,包含项目依赖、插件等。
  • 项目依赖管理:
    • pom.xml(或build.gradle):项目的构建配置文件,包含项目依赖、插件等。
    • 依赖管理:
      • 直接依赖:在项目的构建配置文件中直接声明的依赖,会被包含在最终的可执行文件中。
      • 传递依赖:项目依赖的其他库,会被自动下载并包含在项目中。
    • 依赖范围:
      • 编译范围(compile scope):编译时需要,运行时也需要。
      • 测试范围(test scope):仅在测试阶段有效,不会被包含在最终的可执行文件中。
      • 运行时范围(runtime scope):运行时需要,编译时不需要。
      • 提供范围(provided scope):编译时需要,运行时由容器提供。

HTTP协议

HTTP请求协议

  • 请求行:包含请求方法、请求URL和HTTP版本。
  • 请求头:包含请求的元数据,如请求的客户端信息、请求的内容类型等。常见的请求头字段如下:
请求头字段 说明
Host 请求的主机名
User-Agent 浏览器版本,例如Chrome浏览器的标识类似Mozilla/5.0 … Chrome/79,IE浏览器的标识类似Mozilla/5.0 (Windows NT …) like Gecko
Accept 表示浏览器能接收的资源类型,如text/*, image/或者/*表示所有
Accept-Language 表示浏览器偏好的语言,服务器可以据此返回不同语言的网页
Accept-Encoding 表示浏览器可以支持的压缩类型,例如gzip, deflate等
Content-Type 请求主体的数据类型
Content-Length 请求主体的大小(单位:字节)
  • 空行:用于分隔请求头和请求体。
  • 请求体:包含请求的数据,如表单数据、JSON数据等。

请求数据获取

Web服务器(Tomcat)对HTTP协议的请求数据进行解析,并进行了封装(HttpServletRequest),在调用Controller方法的时候传递给了该方法。这样,就使得程序员不必直接对协议进行操作,让Web开发更加便捷。

HTTP请求示例:

1
2
3
4
5
6
GET /brand/findAll?name=OPPO&status=1 HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...

Tomcat会将这些请求数据解析并封装到HttpServletRequest对象中,开发者可以通过该对象方便地获取请求信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
public class requestController {
@RequestMapping("/request")
public String request(HttpServletRequest request) {//获取前端请求数据封装好了对应的方法
//获取请求行里面的请求地址
String url = request.getRequestURL().toString();//这里获取的是完整的请求地址
//获取请求uri地址
String requestURI = request.getRequestURI();
System.out.println(url);
System.out.println(requestURI);
//获取请求HTTP协议版本号
String protocol = request.getProtocol();
System.out.println(protocol);
//获取请求行中的请求参数
String name = request.getParameter("name");
String age = request.getParameter("age");
System.out.println(name);
System.out.println(age);
//获取请求头
String accept = request.getHeader("Accept");
System.out.println(accept);
return "ok";
}
}

响应数据格式

HTTP响应示例:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 10 May 2022 07:51:07 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[{"id": 1, "brandName": "阿里巴巴", "companyName": "腾讯计算机系统有限公司", "description": "玩玩玩"}]

HTTP响应协议由以下几部分组成:

  • 响应行:响应数据第一行(协议、状态码、描述)

    • 示例:HTTP/1.1 200 OK
    • HTTP版本:如HTTP/1.1
    • 状态码:三位数字,表示请求的处理结果(如200表示成功,404表示资源未找到)
    • 状态消息:对状态码的简短描述
  • 响应头:第二行开始,格式为key: value

    • 示例:Content-Type: application/json
    • 包含响应的元数据,如内容类型、内容长度、服务器信息等
    • 常见的响应头字段:
      • Content-Type:响应内容的类型(如text/html、application/json)
      • Content-Length:响应内容的长度
      • Server:服务器的名称和版本
      • Date:响应的日期和时间
      • Cache-Control:缓存控制策略
  • 空行:用于分隔响应头和响应体

  • 响应体:最后一部分,存放响应数据

    • 示例:[{"id": 1, "brandName": "阿里巴巴", "companyName": "腾讯计算机系统有限公司", "description": "玩玩玩"}]
    • 包含响应的实际数据,如HTML页面、JSON数据、图片等

响应状态码

HTTP状态码分为五大类:

类别 范围 描述
1xx 100-199 信息性状态码,表示请求已接收,继续处理
2xx 200-299 成功状态码,表示请求已成功处理
3xx 300-399 重定向状态码,表示需要进一步操作才能完成请求
4xx 400-499 客户端错误状态码,表示请求有错误
5xx 500-599 服务器错误状态码,表示服务器处理请求时出错

常见的状态码:

  • 200 OK:请求成功
  • 400 Bad Request:请求参数错误
  • 401 Unauthorized:未授权
  • 403 Forbidden:禁止访问
  • 404 Not Found:资源未找到
  • 500 Internal Server Error:服务器内部错误
  • 503 Service Unavailable:服务器暂时不可用

响应数据设置

在Controller方法中,可以通过HttpServletResponse对象来设置响应数据。

操作案例中的核心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestController // 标记为Spring MVC的控制器,负责处理HTTP请求
public class UserController {

// 映射GET /list请求,返回用户列表
@RequestMapping("/list")
public List<User> list() {
// 从类路径下加载user.txt文件,获取输入流
InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
// 使用IoUtil工具类按行读取文件内容,默认UTF-8编码,结果存入ArrayList
ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());

// 将每行文本解析成User对象:按逗号拆分字段并封装
List<User> userList = lines.stream().map(line -> {
String[] parts = line.split(","); // 拆分每行数据
Integer id = Integer.parseInt(parts[0]); // 解析用户ID
String username = parts[1]; // 用户名
String password = parts[2]; // 密码
String name = parts[3]; // 真实姓名
Integer age = Integer.parseInt(parts[4]); // 年龄
// 解析更新时间,格式为yyyy-MM-dd HH:mm:ss
LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new User(id, username, password, name, age, updateTime); // 构造User对象
}).toList(); // 收集为不可变List

// 将封装好的用户列表返回给前端(Spring自动转为JSON)
return userList;
}
}
  • 问题解决
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <properties>
    <!-- Source: https://mvnrepository.com/artifact/org.projectlombok/lombok -->
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.42</version>
    <!-- 这里要加版本号不加版本号要报错 -->
    </dependency>
    <!-- Source: https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
    <annotationProcessorPaths>
    <path>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.42</version>
    <!-- 这里要加版本号不加版本号要报错 -->
    <exclude>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.42</version>
    <!-- 这里要加版本号不加版本号要报错 -->
    </exclude>
  • response 作用将其转换为 JSON 格式并返回给前端

Web分层解耦

  • 控制器(Controller):负责处理HTTP请求,调用服务层(Service)处理业务逻辑,最后将结果返回给前端。

  • 服务层(Service):负责业务逻辑的处理,调用数据访问层(Repository)进行数据操作。

  • 数据访问层(Dao):负责与数据库或其他数据源交互,执行CRUD操作。

  • 调用的时候使用多态的调用形式: UserService userService = new UserServiceImpl();

  • 控制反转(IoC):

    • 控制反转是一种设计模式,通过将对象的创建和依赖关系的管理交给容器来实现。
    • 容器负责创建对象并管理它们的生命周期,开发人员只需要关注业务逻辑的实现。
    • 控制反转的实现方式有两种:基于XML配置文件的方式和基于注解的方式。
  • 依赖注入(DI):

    • 依赖注入是控制反转的一种实现方式,通过容器将依赖的对象注入到目标对象中。
    • 依赖注入可以分为构造函数注入、Setter方法注入和接口注入等方式。
    • 构造函数注入:通过构造函数参数来注入依赖的对象。
    • Setter方法注入:通过Setter方法来注入依赖的对象。
    • 接口注入:通过接口方法来注入依赖的对象。
  • Bean对象:

    • Bean对象是指在IoC容器中管理的对象,通常是业务逻辑组件、数据访问组件等。
    • Bean对象的创建和管理由IoC容器负责,开发人员只需要关注业务逻辑的实现。
    • Bean对象的生命周期包括:实例化、依赖注入、初始化、使用、销毁。

Spring IOC & DI 实际应用

Spring IOC和依赖注入在实际开发中的核心应用包括:

  1. 将Dao及Service层的实现类,交给IOC容器管理
  2. 为Controller及Service注入运行时所依赖的对象

代码示例

UserController(控制器层)

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class UserController {
@Autowired
private UserService userService;

@RequestMapping("/list")
public List<User> list(){
//1.调用service,查询用户信息
List<User> userlist = userService.list();
//2...
}
}

UserServiceImpl(业务逻辑层)

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;

@Override
public List<User> list() {
//1.调用dao获取数据
List<String> lines = userDao.list();
//2...
}
}

UserDaoImpl(数据访问层)

1
2
3
4
5
6
7
8
9
@Component
public class UserDaoImpl implements UserDao {

@Override
public List<String> list() {
InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
return IOUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
}
}

实现原理

在这个示例中:

  • 使用@Component注解将UserDaoImplUserServiceImpl注册到IOC容器中
  • 使用@RestController注解(特殊的@Component)将UserController注册到IOC容器中
  • 使用@Autowired注解自动注入依赖对象:
    • UserController注入UserService
    • UserServiceImpl注入UserDao

这样,Spring容器会自动管理这些Bean的生命周期和依赖关系,简化了代码开发和维护。

IOC详解

要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:

注解 说明 位置
@Component 声明bean的基础注解 不属于以下三类时,用此注解
@Controller @Component的衍生注解 标注在控制层类上
@Service @Component的衍生注解 标注在业务层类上
@Repository @Component的衍生注解 标注在数据访问层类上(由于与mybatis整合,用的少)

三层架构与IOC容器

在Web应用中,通常采用三层架构:

  1. Controller(控制层):接收请求,响应数据
  2. Service(业务层):处理业务逻辑
  3. Dao(数据访问层):进行数据访问

Spring IOC容器可以管理这三层中的所有组件,通过不同的注解来区分组件类型,使代码结构更加清晰。

组件扫描机制

前面声明bean的四大注解(@Component、@Controller、@Service、@Repository)要想生效,还需要被组件扫描注解@ComponentScan扫描。

该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中,默认扫描的范围是启动类所在包及其子包。

项目结构示例

1
2
3
4
5
6
7
8
9
10
11
tlias-management/
├── src/main/
│ ├── java/
│ │ └── com.itheima/
│ │ ├── controller/ # 控制层
│ │ ├── dao/ # 数据访问层
│ │ ├── pojo/ # 实体类
│ │ ├── service/ # 业务层
│ │ └── TliasManagementApplication.java # 启动类
│ └── resources/
└── pom.xml

启动类示例

1
2
3
4
5
6
@SpringBootApplication
public class TliasManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasManagementApplication.class, args);
}
}

在这个示例中,@SpringBootApplication注解包含了@ComponentScan,会自动扫描com.itheima包及其子包下的所有带有组件注解的类,将它们注册到IOC容器中。

DI详解

基于@Autowired进行依赖注入的常见方式有如下三种:

1. 属性注入

1
2
3
4
5
6
7
@RestController
public class UserController {
@Autowired
private UserService userService;

//......
}

2. 构造函数注入

1
2
3
4
5
6
7
8
9
@RestController
public class UserController {
private final UserService userService;

@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
}

3. Setter注入

1
2
3
4
5
6
7
8
9
@RestController
public class UserController {
private UserService userService;

@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}

三种注入方式的对比

注入方式 优点 缺点
属性注入 简洁,使用方便 1. 无法注入final修饰的属性
2. 容易违反单一职责原则
3. 可能导致循环依赖
构造函数注入 1. 可以注入final修饰的属性
2. 避免循环依赖
3. 保证依赖不为空
代码相对复杂
Setter注入 1. 可以选择性注入
2. 灵活性更高
1. 无法注入final修饰的属性
2. 可能导致循环依赖

最佳实践

  • Spring 4.x推荐使用构造函数注入
  • Spring 5.x推荐使用构造函数注入或属性注入
  • 对于必需的依赖,优先使用构造函数注入
  • 对于可选的依赖,可以使用Setter注入

@Autowired的注入规则

@Autowired注解默认是按照类型进行注入的。如果存在多个相同类型的bean,将会报出如下错误:

1
2
3
4
5
Field userService in com.itheima.controller.UserController required a single bean, but 2 were found:
- userServiceImpl: defined in file [D:\idea_workspace\web\ai\web-project1\springboot-web-quickstart\target\classes\com\itheima\service\impl\UserServiceImpl.class]
- userServiceImpl2: defined in file [D:\idea_workspace\web\ai\web-project1\springboot-web-quickstart\target\classes\com\itheima\service\impl\UserServiceImpl2.class]
Action:
Consider making one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

解决方法

有三种方法可以解决多个同类型bean的注入问题:

方案一:@Primary
1
2
3
4
5
6
7
8
@Primary
@Service
public class UserServiceImpl implements UserService {
@Override
public List<User> list() {
// 省略实现...
}
}
方案二:@Qualifier
1
2
3
4
5
6
@RestController
public class UserController {
@Autowired
@Qualifier("userServiceImpl")
private UserService userService;
}
方案三:@Resource
1
2
3
4
5
@RestController
public class UserController {
@Resource(name = "userServiceImpl")
private UserService userService;
}

三种方案的对比

  • @Primary:在bean定义时指定优先级,适合全局默认选择
  • @Qualifier:在注入时指定具体的bean名称,需要与@Autowired配合使用
  • @Resource:JDK提供的注解,默认按名称注入,也可以指定name属性

注意@Resource不是Spring特有的注解,而是JSR-250规范中的注解,功能与@Autowired类似但有一些差异。

mysql数据库

DDL-数据库

操作语法

查询所有数据库

1
show databases;

查询当前数据库

1
select database();

使用/切换数据库

1
use 数据库名;

创建数据库

1
create database [if not exists] 数据库名 [default charset utf8mb4];

删除数据库

1
drop database [if exists] 数据库名;

说明

  • [if not exists]:创建数据库时的可选条件,如果数据库不存在则创建
  • [default charset utf8mb4]:设置数据库的默认字符集为utf8mb4,支持表情符号
  • [if exists]:删除数据库时的可选条件,如果数据库存在则删除

DDL-表结构-创建

创建表的语法

1
2
3
4
5
create table tablename(
字段1 字段类型 [约束] [comment 字段1注释],
.......
字段2 字段类型 [约束] [comment 字段2注释]
)[comment 表注释];

说明

  • tablename:要创建的表名
  • 字段1 字段类型:定义表的字段名称和数据类型
  • [约束]:可选的字段约束(如PRIMARY KEY, NOT NULL, UNIQUE等)
  • [comment 字段1注释]:可选的字段注释,用于说明字段的含义
  • [comment 表注释]:可选的表注释,用于说明表的用途

约束

约束的定义

约束是作用于表中字段上的规则,用于限制存储在表中的数据。

约束的目的

保证数据库中数据的正确性、有效性和完整性。

约束类型

约束 描述 关键字
非空约束 限制字段取值不能为空 not null
唯一约束 保证字段的所有数据都是唯一、不重复的 unique
主键约束 主键是一行数据的唯一标识,要求非空且唯一 primary key
默认约束 保存数据时,如果未指定该字段值,则采用默认值 default
外键约束 让两张表的数据建立连接,保证数据的一致性和完整性 foreign key

约束示例

以一个用户表为例,展示各种约束的应用:

id username name age gender
1 qingyifuwang 韦一笑 45
2 baimeiyingwang 殷天正 55
3 jinmaoshiwang 谢逊 50
4 zixiaonvwang 黛绮丝 38

约束说明

  • id:主键约束(唯一标识)
  • username:非空约束 + 唯一约束
  • name:非空约束
  • gender:默认约束(默认值:男)

通过这些约束,可以保证表中数据的完整性和一致性,防止无效或错误的数据被插入到表中。

  • 主键自增:auto_increment

DDL-表关系-一对多

一对多关系示例

场景:部门与员工的关系(一个部门下有多个员工)。

部门表(dept)

1
2
3
4
5
6
create table dept(
id int unsigned primary key auto_increment comment 'ID,主键',
name varchar(10) not null unique comment '部门名称',
create_time datetime comment '创建时间',
update_time datetime comment '修改时间'
) comment '部门表';

员工表(emp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
create table emp(
id int unsigned primary key auto_increment comment 'ID,主键',
username varchar(20) not null unique comment '用户名',
password varchar(32) not null comment '密码',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别,1:男,2:女',
phone char(11) not null unique comment '手机号',
job tinyint unsigned comment '职位,1:班主任;2:讲师...',
salary int unsigned comment '薪资',
image varchar(255) comment '头像',
entry_date date comment '入职日期',
create_time datetime comment '创建时间',
update_time datetime comment '修改时间'
) comment '员工表';

DDL-表关系-一对一

一对一关系示例

案例:用户与身份证信息的关系

关系:一对一关系,多用于单表拆分,将一张表的基础字段放在一张表中,其他字段放在另一张表中,以提升操作效率

实现:在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)

用户基本信息表(tb_user)

1
2
3
4
5
6
7
create table tb_user(
id int unsigned primary key auto_increment comment 'ID,主键',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别,1:男,2:女',
phone varchar(11) not null unique comment '手机号',
degree varchar(10) comment '学历'
) comment '用户基本信息表';

用户身份证信息表(tb_user_card)

1
2
3
4
5
6
7
8
9
10
11
create table tb_user_card(
id int unsigned primary key auto_increment comment 'ID,主键',
name varchar(10) not null comment '姓名',
birthday date comment '出生日期',
idcard varchar(18) not null unique comment '身份证号',
issued varchar(20) comment '发证机关',
expire_begin date comment '有效期开始',
expire_end date comment '有效期结束',
user_id int unsigned unique comment '关联用户ID',
constraint fk_user_card_user foreign key (user_id) references tb_user(id)
) comment '用户身份证信息表';

DDL-表关系-多对多

多对多关系示例

案例:学生与课程的关系

关系:一个学生可以选修多门课程,一门课程也可以供多个学生选择

实现:建立第三张中间表,中间表至少包含两个外键,分别关联双方主键

学生表(tb_student)

1
2
3
4
5
create table tb_student(
id int unsigned primary key auto_increment comment 'ID,主键',
name varchar(10) not null comment '姓名',
no varchar(10) not null unique comment '学号'
) comment '学生表';

课程表(tb_course)

1
2
3
4
create table tb_course(
id int unsigned primary key auto_increment comment 'ID,主键',
name varchar(20) not null unique comment '课程名称'
) comment '课程表';

学生课程关系表(tb_student_course)

1
2
3
4
5
6
7
8
create table tb_student_course(
id int unsigned primary key auto_increment comment 'ID,主键',
student_id int unsigned not null comment '学生ID',
course_id int unsigned not null comment '课程ID',
constraint fk_student_course_student foreign key (student_id) references tb_student(id),
constraint fk_student_course_course foreign key (course_id) references tb_course(id),
unique key uk_student_course (student_id, course_id)
) comment '学生课程关系表';

DDL-外键约束

外键约束的添加方式

可以在创建表时或表结构创建完成后,为字段添加外键约束。具体语法如下:

1. 创建表时添加外键约束

1
2
3
4
5
create table 表名(
字段名 数据类型,
...
[constraint] [外键名称] foreign key (外键字段名) references 主表 (字段名)
);

2. 创建表后添加外键约束

1
alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表(字段名);

物理外键与逻辑外键

1. 物理外键

  • 概念:使用 foreign key 定义外键关联另外一张表。

  • 缺点

    1. 影响增、删、改的效率(需要检查外键关系)。
    2. 仅用于单节点数据库,不适用于分布式、集群场景。
    3. 容易引发数据库的死锁问题,消耗性能。

2. 逻辑外键

  • 概念:在业务层逻辑中,解决外键关联。

  • 优势

    1. 不依赖数据库,灵活性高。
    2. 适用于分布式、集群环境。
    3. 不会影响数据库性能。
    4. 避免数据库死锁问题。

在实际开发中,特别是在分布式系统中,推荐使用逻辑外键而不是物理外键。

DDL-表结构-数据类型

字符串类型

分类 类型 大小 描述 示例 优势
字符串类型 char 0-255 bytes 定长字符串 idcard char(18)
phone char(11)
性能较高
字符串类型 varchar 0-65535 bytes 变长字符串 username varchar(50) 节省磁盘空间
字符串类型 tinyblob 0-255 bytes 不超过255个字节的二进制数据
字符串类型 tinytext 0-255 bytes 短文本字符串
字符串类型 blob 0-65 535 bytes 二进制形式的长文本数据
字符串类型 text 0-65 535 bytes 长文本数据
字符串类型 mediumblob 0-16 777 215 bytes 二进制形式的中等长度文本数据
字符串类型 mediumtext 0-16 777 215 bytes 中等长度文本数据
字符串类型 longblob 0-4 294 967 295 bytes 二进制形式的极大文本数据
字符串类型 longtext 0-4 294 967 295 bytes 极大文本数据

char与varchar的区别

  • char(10):固定占用10个字符空间,存储A占10个空间,存储ABC占10个空间
  • varchar(10):最多占用10个字符空间,存储A占1个空间,存储ABC占3个空间

日期类型

分类 类型 大小(byte) 范围 格式 描述 示例
日期类型 date 3 1000-01-01 至 9999-12-31 YYYY-MM-DD 日期值 birthday date
日期类型 time 3 -838:59:59 至 838:59:59 HH:MM:SS 时间值或持续时间
日期类型 year 1 1901 至 2155 YYYY 年份值
日期类型 datetime 8 1000-01-01 00:00:00 至 9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS 混合日期和时间值 operateTime datetime
日期类型 timestamp 4 1970-01-01 00:00:01 至 2038-01-19 03:14:07 YYYY-MM-DD HH:MM:SS 混合日期和时间值,时间戳

datetime与timestamp的区别

  • datetime:存储范围更广,占用8字节,不会自动更新
  • timestamp:存储范围较小,占用4字节,会自动更新为当前时间(当插入或更新数据时)

数值类型

MySQL还支持多种数值类型,包括整数类型和浮点类型:

整数类型

  • tinyint:1字节,-128至127或0至255
  • smallint:2字节,-32768至32767或0至65535
  • mediumint:3字节,-8388608至8388607或0至16777215
  • int:4字节,-2147483648至2147483647或0至4294967295
  • bigint:8字节,-9223372036854775808至9223372036854775807或0至18446744073709551615

浮点类型

  • float:4字节,单精度浮点数值
  • double:8字节,双精度浮点数值
  • decimal:可变长度,精确数值(用于财务数据)

注意:选择数据类型时,应根据实际存储需求选择合适的类型,以节省存储空间并提高查询性能。

DDL-表结构-查询、修改、删除

表结构的查询、修改、删除相关语法如下:

查询表结构

1
2
3
4
5
show tables; -- 查询当前数据库的所有表

desc 表名; -- 查询表结构

show create table 表名; -- 查询建表语句

修改表结构

1
2
3
4
5
6
7
8
9
10
11
-- 添加字段
alter table 表名 add 字段名 类型(长度) [comment 注释] [约束];

-- 修改字段类型
alter table 表名 modify 字段名 新数据类型(长度);

-- 修改字段名与字段类型
alter table 表名 change 旧字段名 新字段名 类型(长度) [comment 注释] [约束];

-- 修改表名
alter table 表名 rename to 新表名;

删除表结构

1
2
3
4
5
-- 删除字段
alter table 表名 drop column 字段名;

-- 删除表
drop table [if exists] 表名;

DML-数据操作

DML-insert(新增数据)

1
2
3
4
5
6
7
8
9
10
11
-- 指定字段添加数据
insert into 表名(字段名1, 字段名2) values (值1, 值2);

-- 全部字段添加数据
insert into 表名 values (值1, 值2, ...);

-- 批量添加数据(指定字段)
insert into 表名(字段名1, 字段名2) values (值1, 值2), (值1, 值2);

-- 批量添加数据(全部字段)
insert into 表名 values (值1, 值2, ...), (值1, 值2, ...);

DML-update(修改数据)

1
2
3
4
-- 修改数据
update 表名 set 字段名1 =1, 字段名2 =2, ... [where 条件];

-- 注意:如果不添加where条件,会修改表中所有数据

DML-delete(删除数据)

1
2
3
4
-- 删除数据
delete from 表名 [where 条件];

-- 注意:如果不添加where条件,会删除表中所有数据

注意

  • DDL语句(数据定义语言)用于定义数据库对象,如数据库、表、约束等
  • DML语句(数据操作语言)用于操作数据库中的数据,如插入、修改、删除数据
  • DQL语句(数据查询语言)用于查询数据库中的数据
  • 执行DML语句时,应谨慎使用where条件,避免误操作影响大量数据

DQL-数据查询

DQL完整语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
select
字段列表
from
表名列表
where
条件列表
group by
分组字段列表
having
分组后条件列表
order by
排序字段列表
limit
分页参数

DQL查询类型

DQL查询包括以下五种类型:

  1. 基本查询 (select...from...)
  2. 条件查询 (where)
  3. 分组查询 (group by)
  4. 排序查询 (order by)
  5. 分页查询 (limit)

DQL-基本查询

查询多个字段

1
select 字段1,字段2,字段3 from 表名;

查询所有字段(通配符)

1
select * from 表名;

为查询字段设置别名

1
2
3
4
select 字段1 [as 别名1], 字段2 [as 别名2] from 表名;

-- as关键字可以省略
select 字段1 别名1, 字段2 别名2 from 表名;

去除重复记录

1
select distinct 字段列表 from 表名;

注意

  • select * 虽然方便,但在实际开发中应尽量避免使用,建议明确指定需要查询的字段
  • distinct 会去除查询结果中所有字段值都相同的记录
  • 设置别名可以使查询结果更易读,特别是在进行多表查询或使用函数时

DQL-条件查询

条件查询基本语法

1
select 字段列表 from 表名 where 条件列表 ;

比较运算符

比较运算符 功能
> 大于
>= 大于等于
< 小于
<= 小于等于
= 等于
<> 或 != 不等于
between … and … 在某个范围之内(含最小、最大值)
in(…) 在in之后的列表中的值,多选一
Like 占位符 模糊匹配(_匹配单个字符,%匹配任意个字符)
is null 是null

逻辑运算符

逻辑运算符 功能
and 或 && 并且 (多个条件同时成立)
or 或 || 或者 (多个条件任意一个成立)
not 或 ! 非 ,不是

条件查询示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 查询年龄大于等于18的用户
select * from user where age >= 18;

-- 查询年龄在18-30之间的用户
select * from user where age between 18 and 30;

-- 查询年龄为18、20、22的用户
select * from user where age in(18,20,22);

-- 查询姓名中包含"张"的用户
select * from user where name like '%张%';

-- 查询姓名以"李"开头的用户
select * from user where name like '李%';

-- 查询姓名为2个字符的用户
select * from user where name like '__';

-- 查询年龄不为null的用户
select * from user where age is not null;

-- 查询年龄大于18且性别为男的用户
select * from user where age > 18 and gender = '男';

注意

  • 在SQL中,字符串和日期类型的数据需要使用单引号括起来
  • like 运算符中,_ 表示匹配单个字符,% 表示匹配任意个字符
  • null 值不能使用 =!= 进行比较,必须使用 is nullis not null

5. 聚合函数

5.1 聚合函数概述

聚合函数是将一列数据作为一个整体,进行纵向计算的函数。

5.2 常用聚合函数

函数 功能
count 统计数量
max 最大值
min 最小值
avg 平均值
sum 求和

5.3 注意事项

  • 所有的聚合函数不参与null的统计

5.4 聚合函数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 统计员工数量
-- count(字段): 统计指定字段非空值的数量
select count(id) from emp;

-- count(*): 统计所有记录的数量(推荐使用)
select count(*) from emp;

-- count(常量): 统计所有记录的数量(推荐使用)
select count(1) from emp;

-- 统计员工的最高工资
select max(salary) from emp;

-- 统计员工的最低工资
select min(salary) from emp;

-- 统计员工的平均工资
select avg(salary) from emp;

-- 统计员工的工资总和
select sum(salary) from emp;

6. 分组查询

6.1 分组查询语法

1
select 字段列表 from 表名 [where 条件列表] group by 分组字段名 [having 分组后过滤条件];

6.2 where与having的区别

区别 where having
执行时机 分组之前进行过滤 分组之后对结果进行过滤
判断条件 不能对聚合函数进行判断 可以对聚合函数进行判断

6.3 分组查询示例

1
2
3
4
5
6
7
8
9
10
11
-- 根据性别分组,统计男性和女性员工的数量
select gender, count(*) from emp group by gender;

-- 根据性别分组,统计男性和女性员工的平均工资
select gender, avg(salary) from emp group by gender;

-- 先查询年龄大于20的员工,再根据性别分组,统计男性和女性员工的数量
select gender, count(*) from emp where age > 20 group by gender;

-- 根据性别分组,统计男性和女性员工的平均工资,筛选平均工资大于5000的结果
select gender, avg(salary) from emp group by gender having avg(salary) > 5000;

6.4 分组查询注意事项

  • 分组查询的查询列表中,只能包含分组字段聚合函数,不能包含其他字段(除非这些字段在所有分组内的取值都相同)
  • 如果需要对分组后的结果进行过滤,必须使用 having 子句,而不能使用 where 子句
  • where 子句用于在分组前对数据进行过滤,having 子句用于在分组后对数据进行过滤
  • 可以对多个字段进行分组,分组字段之间用逗号分隔
1
2
-- 根据部门和性别分组,统计各部门各性别的员工数量
select dept, gender, count(*) from emp group by dept, gender;

7. 排序查询

7.1 排序查询语法

1
select 字段列表 from 表名 [where 条件列表] [group by 分组字段名 having 分组后过滤条件] order by 排序字段 排序方式;

7.2 排序方式

  • 升序asc(默认值,可以省略)
  • 降序desc

7.3 排序查询示例

1
2
3
4
5
6
7
8
9
10
11
-- 按照工资从低到高排序(升序)
select * from emp order by salary;

-- 按照工资从高到低排序(降序)
select * from emp order by salary desc;

-- 先按照工资从高到低排序,再按照年龄从大到小排序
select * from emp order by salary desc, age desc;

-- 查询年龄大于20的员工,按照工资从高到低排序
select * from emp where age > 20 order by salary desc;

8. 分页查询

8.1 分页查询语法

1
select 字段列表 from 表名 [where 条件列表] [group by 分组字段名 having 分组后过滤条件] [order by 排序字段 排序方式] limit 起始索引, 查询记录数;

8.2 分页查询说明

  • 起始索引:从0开始,即第一页的起始索引为0
  • 查询记录数:每页显示的记录条数
  • 分页查询是数据库方言:不同的数据库实现方式不同,MySQL中使用LIMIT关键字

8.3 分页查询示例

1
2
3
4
5
6
7
8
9
10
11
-- 查询第1页,每页显示5条记录
select * from emp limit 0, 5;

-- 查询第2页,每页显示5条记录(起始索引 = (页码 - 1) * 每页记录数)
select * from emp limit 5, 5;

-- 查询第3页,每页显示5条记录
select * from emp limit 10, 5;

-- 先按照工资从高到低排序,再查询第2页,每页显示3条记录
select * from emp order by salary desc limit 3, 3;

8.4 注意事项

  • 如果起始索引为0,可以省略起始索引,直接写limit 记录数
    1
    2
    -- 查询第1页,每页显示5条记录(简写形式)
    select * from emp limit 5;
  • 分页查询在实际开发中非常常用,通常用于数据列表的分页显示
  • 为了保证分页结果的稳定性,建议在分页查询时添加order by排序条件

9. 多表查询

9.1 多表查询概述

多表查询:指从多张表中查询数据。

笛卡尔积:指在数学中,两个集合(A集合和B集合)的所有组合情况。(在多表查询时,需要消除无效的笛卡尔积)

9.2 连接查询

连接查询是多表查询的一种重要方式,通过表之间的关联关系查询数据。

9.2.1 内连接

内连接查询的是两张表交集部分的数据。

语法

  1. 隐式内连接

    1
    select 字段列表 from1, 表2 where 连接条件 ...;
  2. 显式内连接

    1
    select 字段列表 from1 [inner] join2 on 连接条件 ...;

示例

1
2
3
4
5
6
7
-- A. 查询所有员工的ID,姓名,及所属的部门名称(隐式、显式内连接实现)
select emp.id as 员工ID, emp.name as 姓名, dept.name as 所属部门 from emp, dept where emp.dept_id = dept.id;
select emp.id as 员工ID, emp.name as 姓名, dept.name as 所属部门 from emp inner join dept on emp.dept_id = dept.id;

-- B. 查询性别为男,且工资高于8000的员工的ID,姓名,及所属的部门名称(隐式、显式内连接实现)
select emp.id as 员工ID, emp.name as 姓名, dept.name as 所属部门 from emp, dept where emp.gender = 1 and emp.salary > 8000 and emp.dept_id = dept.id;
select emp.id as 员工ID, emp.name as 姓名, dept.name as 所属部门 from emp inner join dept on emp.dept_id = dept.id where emp.gender = 1 and emp.salary > 8000;

9.2.2 外连接

外连接分为左外连接和右外连接,用于查询一张表的所有数据以及另一张表与之匹配的数据。

外连接语法

  1. 左外连接

    1
    select 字段列表 from1 left [outer] join2 on 连接条件 ...;
  2. 右外连接

    1
    select 字段列表 from1 right [outer] join2 on 连接条件 ...;

外连接说明

  • 左外连接:查询左表所有数据(包括两张表交集部分数据)

    • 如果左表中的某条记录在右表中没有匹配的数据,则右表字段显示为NULL
  • 右外连接:查询右表所有数据(包括两张表交集部分数据)

    • 如果右表中的某条记录在左表中没有匹配的数据,则左表字段显示为NULL

示例

1
2
3
4
5
-- 查询所有部门的信息,以及每个部门下的员工信息(如果没有员工,则显示为NULL)
select dept.id as 部门ID, dept.name as 部门名称, emp.id as 员工ID, emp.name as 员工姓名 from dept left join emp on dept.id = emp.dept_id;

-- 查询所有员工的信息,以及每个员工所属的部门信息(如果没有部门,则显示为NULL)
select emp.id as 员工ID, emp.name as 员工姓名, dept.id as 部门ID, dept.name as 部门名称 from emp right join dept on emp.dept_id = dept.id;

9.3 子查询

9.3.1 子查询概述

子查询是SQL语句中嵌套的select语句,称为嵌套查询,又称子查询。

形式

1
select * from t1 where column1 = (select column1 from t2 ...);

说明

  • 子查询外部的语句可以是insert / update / delete / select的任何一个,最常见的是select
  • 子查询可以嵌套多层使用

9.3.2 子查询的分类

根据子查询返回结果的类型,子查询可以分为:

  1. 标量子查询:子查询返回的结果为单个值
  2. 列子查询:子查询返回的结果为一列
  3. 行子查询:子查询返回的结果为一行
  4. 表子查询:子查询返回的结果为多行多列

9.3.3 子查询示例

1. 标量子查询

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- A. 查询最早入职的员工信息
-- a. 获取到最早入职时间
select min(entry_date) from emp;
-- b. 查询最早入职的员工信息
select * from emp where entry_date='2020-01-01';
-- c. 合并查询语句
select * from emp where entry_date=(select min(entry_date) from emp);

-- B. 查询在"李华"入职之后入职的员工信息
-- a. 查询李华什么时候入职的
select entry_date from emp where name='李华';
-- b. 查询之后的信息
select * from emp where entry_date>'2022-08-01';
-- c. 合并查询语句
select * from emp where entry_date>(select entry_date from emp where name='李华');
2. 列子查询

示例

1
2
3
4
5
6
7
-- A. 查询"教研部"和"咨询部"的所有员工信息
-- a. 查询教研部和咨询部的信息
select id from dept where name='教研部' or name='咨询部';
-- b. 查询员工信息
select * from emp where dept_id in (2,3);
-- c. 合并查询语句
select * from emp where dept_id in (select id from dept where name='教研部' or name='咨询部');
3. 行子查询

示例

1
2
3
4
5
6
7
-- A. 查询与"李华"的薪资及职位位都相同的员工信息:
-- a. 李华的薪资和职位
select emp.salary,job from emp where emp.name='李华';
-- b. 查询相同的员工信息
select * from emp where salary=6700 and job =2;
-- c. 合并查询语句
select * from emp where (salary, job) = (select salary, job from emp where name='李华');
4. 表子查询

示例1

1
2
-- 查询每个部门的平均工资,并找出平均工资高于5000的部门
select dept_id, avg_salary from (select dept_id, avg(salary) as avg_salary from emp group by dept_id) as dept_avg where avg_salary > 5000;

示例2

1
2
3
4
5
6
-- 获取每个部门中薪资最高的员工信息
-- a. 获取每个部门的最高薪资
select dept_id, max(salary) from emp group by dept_id;
-- b. 查询每个部门中薪资最高的员工信息
select * from emp e, (select dept_id, max(salary) max_sal from emp group by dept_id) a
where e.dept_id = a.dept_id and e.salary = a.max_sal;

说明

  • 这个示例使用表子查询获取每个部门的最高薪资信息
  • 然后将原表与子查询结果进行连接,找出每个部门中薪资等于最高薪资的员工
  • 这种方式可以同时获取每个部门的最高薪资和对应的员工详细信息

9.3.4 子查询的使用场景

  • 作为查询条件:用于过滤需要的数据
  • 作为数据源:用于提供新的查询数据集
  • 作为计算字段:用于计算或转换数据
  • 作为更新目标:用于更新或删除数据

9.3.5 子查询的注意事项

  • 子查询必须用括号括起来
  • 标量子查询可以使用比较运算符(=、>、<、>=、<=、<>)
  • 列子查询通常使用IN、ANY、ALL等操作符
  • 表子查询可以作为FROM子句的数据源,需要使用别名

JDBC

1. JDBC入门程序

1.1 需求

基于JDBC程序,执行update语句:

1
update user set age = 25 where id = 1

1.2 实现步骤

1.2.1 准备工作

  1. 创建一个Maven项目
  2. 引入MySQL驱动依赖
  3. 准备数据库表user

1.2.2 代码实现

编写JDBC程序,操作数据库

1.3 Maven依赖配置

pom.xml文件中添加MySQL驱动依赖:

1
2
3
4
5
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>

1.4 JDBC程序实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;

public class JdbcDemo {
public static void main(String[] args) throws Exception {
// 1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 2. 获取连接
String url = "jdbc:mysql://localhost:3306/web01";
String username = "root";
String password = "1234";
Connection connection = DriverManager.getConnection(url, username, password);

// 3. 获取SQL语句执行对象
Statement statement = connection.createStatement();

// 4. 执行SQL
String sql = "update user set age = 25 where id = 1";
int i = statement.executeUpdate(sql);

// 5. 释放资源
statement.close();
connection.close();

System.out.println("影响行数: " + i);
}
}

1.5 代码详解

  1. 注册驱动

    1
    Class.forName("com.mysql.cj.jdbc.Driver");

    加载并注册MySQL驱动类。在JDBC 4.0之后,这一步可以省略,因为会自动加载驱动。

  2. 获取连接

    1
    2
    3
    4
    String url = "jdbc:mysql://localhost:3306/web01";
    String username = "root";
    String password = "1234";
    Connection connection = DriverManager.getConnection(url, username, password);
    • url:数据库连接地址,格式为jdbc:mysql://主机:端口/数据库名
    • username:数据库用户名
    • password:数据库密码
    • Connection:数据库连接对象,用于建立与数据库的连接
  3. 获取SQL执行对象

    1
    Statement statement = connection.createStatement();

    Statement是SQL语句执行对象,用于发送SQL语句到数据库执行。

  4. 执行SQL

    1
    2
    String sql = "update user set age = 25 where id = 1";
    int i = statement.executeUpdate(sql);
    • executeUpdate()方法用于执行DML语句(INSERT、UPDATE、DELETE)
    • 返回值是受影响的行数
  5. 释放资源

    1
    2
    statement.close();
    connection.close();

    释放数据库资源,先关闭Statement,再关闭Connection。

2. JDBC核心API

2.1 DriverManager

DriverManager是JDBC驱动程序的管理类,主要负责加载、注册JDBC驱动,并根据连接字符串创建数据库连接。

2.1.1 主要功能

  • 加载驱动Class.forName("com.mysql.cj.jdbc.Driver")
  • 获取连接DriverManager.getConnection(url, username, password)

2.1.2 示例

1
2
3
4
5
6
7
8
// 注册驱动(JDBC 4.0后可省略)
Class.forName("com.mysql.cj.jdbc.Driver");

// 获取连接
String url = "jdbc:mysql://localhost:3306/web01";
String username = "root";
String password = "1234";
Connection connection = DriverManager.getConnection(url, username, password);

2.2 Connection

Connection是数据库连接对象,负责建立与数据库的连接,并提供创建SQL执行对象的方法。

2.2.1 主要功能

  • 创建Statement对象connection.createStatement()
  • 创建PreparedStatement对象connection.prepareStatement(sql)
  • 创建CallableStatement对象connection.prepareCall(sql)
  • 管理事务:提交、回滚、设置事务隔离级别

2.2.2 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建Statement对象
Statement statement = connection.createStatement();

// 创建PreparedStatement对象
String sql = "insert into user(name, age) values(?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);

// 事务管理
connection.setAutoCommit(false); // 关闭自动提交
try {
// 执行SQL语句
// ...
connection.commit(); // 提交事务
} catch (SQLException e) {
connection.rollback(); // 回滚事务
e.printStackTrace();
}

2.3 Statement

Statement是SQL语句执行对象,用于发送SQL语句到数据库执行。

2.3.1 主要功能

  • 执行DML语句executeUpdate(sql) - 返回受影响的行数
  • 执行DQL语句executeQuery(sql) - 返回ResultSet结果集
  • 执行任意SQL语句execute(sql) - 返回boolean值,表示是否有结果集

2.3.2 示例

1
2
3
4
5
6
7
// 执行DML语句
String sql1 = "update user set age = 25 where id = 1";
int rows = statement.executeUpdate(sql1);

// 执行DQL语句
String sql2 = "select * from user";
ResultSet resultSet = statement.executeQuery(sql2);

2.4 ResultSet

ResultSet是查询结果集对象,用于存储和处理查询结果。

2.4.1 主要功能

  • 遍历结果集next() - 移动到下一行记录
  • 获取字段值getXxx(字段名)getXxx(字段索引)
  • 释放资源close()

2.4.2 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String sql = "select id, name, age from user";
ResultSet resultSet = statement.executeQuery(sql);

// 遍历结果集
while (resultSet.next()) {
int id = resultSet.getInt("id"); // 或 resultSet.getInt(1)
String name = resultSet.getString("name"); // 或 resultSet.getString(2)
int age = resultSet.getInt("age"); // 或 resultSet.getInt(3)

System.out.println("id: " + id + ", name: " + name + ", age: " + age);
}

// 释放资源
resultSet.close();
statement.close();
connection.close();

2.5 PreparedStatement

PreparedStatement是Statement的子类,用于执行预编译的SQL语句。它比Statement更加安全和高效,可以防止SQL注入攻击。

2.5.1 主要功能

  • 预编译SQL语句:提高执行效率
  • 防止SQL注入:通过参数化查询
  • 设置参数setXxx(参数索引, 参数值)
  • 执行SQL语句executeUpdate()executeQuery()

2.5.2 PreparedStatement与Statement的区别

特性 Statement PreparedStatement
SQL语句 静态SQL,每次执行都需要重新解析 预编译SQL,只需要解析一次
性能 较低 较高
安全性 容易受到SQL注入攻击 可以防止SQL注入攻击
参数处理 直接拼接字符串 使用参数占位符(?)

2.5.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 创建PreparedStatement对象
String sql = "insert into user(name, age) values(?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);

// 设置参数
preparedStatement.setString(1, "张三"); // 第一个参数,索引从1开始
preparedStatement.setInt(2, 25); // 第二个参数

// 执行SQL语句
int rows = preparedStatement.executeUpdate();
System.out.println("插入了" + rows + "条记录");

// 查询示例
String sql2 = "select * from user where age > ?";
PreparedStatement preparedStatement2 = connection.prepareStatement(sql2);
preparedStatement2.setInt(1, 20);
ResultSet resultSet = preparedStatement2.executeQuery();

// 遍历结果集
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
System.out.println("id: " + id + ", name: " + name + ", age: " + age);
}

// 释放资源
resultSet.close();
preparedStatement2.close();
preparedStatement.close();
connection.close();

2.5.4 SQL注入防护

使用Statement时,SQL语句是通过字符串拼接形成的,容易受到SQL注入攻击:

1
2
3
4
5
6
// 不安全的做法
String username = "admin' or '1'='1";
String password = "任意密码";
String sql = "select * from user where username = '" + username + "' and password = '" + password + "'";
// 生成的SQL:select * from user where username = 'admin' or '1'='1' and password = '任意密码'
// 由于'1'='1'永远为真,这条SQL会查询出所有用户记录

使用PreparedStatement可以防止SQL注入:

1
2
3
4
5
6
7
8
// 安全的做法
String username = "admin' or '1'='1";
String password = "任意密码";
String sql = "select * from user where username = ? and password = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, username);
preparedStatement.setString(2, password);
// PreparedStatement会将参数中的特殊字符进行转义,防止SQL注入

3. JDBC事务管理

3.1 事务的概念

事务是一组SQL语句的集合,这些语句要么全部执行成功,要么全部执行失败,是数据库操作的最小工作单元。

3.2 事务的四大特性(ACID)

特性 描述
原子性(Atomicity) 事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败
一致性(Consistency) 事务执行前后,数据库的完整性约束没有被破坏
隔离性(Isolation) 多个事务并发执行时,一个事务的执行不影响其他事务的执行
持久性(Durability) 事务一旦提交,它对数据库的改变就是永久性的

3.3 JDBC事务管理API

JDBC通过Connection对象提供事务管理功能:

  • 设置自动提交connection.setAutoCommit(false) - 关闭自动提交,开启事务
  • 提交事务connection.commit() - 提交事务,使所有已执行的SQL语句生效
  • 回滚事务connection.rollback() - 回滚事务,取消所有已执行的SQL语句
  • 设置保存点connection.setSavepoint() - 设置事务保存点,允许部分回滚
  • 回滚到保存点connection.rollback(savepoint) - 回滚到指定的保存点

3.4 事务管理示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Connection connection = null;
PreparedStatement preparedStatement = null;

String url = "jdbc:mysql://localhost:3306/web01";
String username = "root";
String password = "1234";

try {
// 获取连接
connection = DriverManager.getConnection(url, username, password);

// 关闭自动提交,开启事务
connection.setAutoCommit(false);

// 执行第一条SQL语句:转账(转出)
String sql1 = "update account set balance = balance - ? where id = ?";
preparedStatement = connection.prepareStatement(sql1);
preparedStatement.setDouble(1, 1000.0);
preparedStatement.setInt(2, 1);
int rows1 = preparedStatement.executeUpdate();

// 执行第二条SQL语句:转账(转入)
String sql2 = "update account set balance = balance + ? where id = ?";
preparedStatement = connection.prepareStatement(sql2);
preparedStatement.setDouble(1, 1000.0);
preparedStatement.setInt(2, 2);
int rows2 = preparedStatement.executeUpdate();

// 模拟异常
// int i = 1 / 0;

// 提交事务
connection.commit();
System.out.println("转账成功");

} catch (SQLException e) {
// 发生异常,回滚事务
if (connection != null) {
try {
connection.rollback();
System.out.println("转账失败,事务已回滚");
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 释放资源
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

3.5 事务隔离级别

事务隔离级别决定了事务之间的隔离程度,JDBC提供了以下隔离级别:

隔离级别 描述 可能产生的问题
TRANSACTION_READ_UNCOMMITTED(读未提交) 允许读取未提交的数据 脏读、不可重复读、幻读
TRANSACTION_READ_COMMITTED(读已提交) 只能读取已提交的数据 不可重复读、幻读
TRANSACTION_REPEATABLE_READ(可重复读) 保证同一事务中多次读取同一数据的结果一致 幻读
TRANSACTION_SERIALIZABLE(串行化) 最高隔离级别,事务串行执行 性能较差

3.6 设置事务隔离级别

1
2
3
4
5
6
// 获取当前隔离级别
int isolationLevel = connection.getTransactionIsolation();
System.out.println("当前隔离级别: " + isolationLevel);

// 设置隔离级别
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

MyBatis

1. 使用MyBatis查询所有用户数据

1.1 准备工作

  1. 创建SpringBoot工程、引入MyBatis相关依赖

    • 创建SpringBoot工程时,选择MyBatis Framework和MySQL Driver依赖
  2. 准备数据库表user和实体类User

    • 数据库表结构:

      id username password name age
      1 dqiaodai 1234567890 大乔 22
      2 xiaqiao 123456 小乔 17
      3 luban 123456 鲁班 18
      4 zhaoyun 12345678 赵云 27
    • 实体类User:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class User {
      private Integer id; // ID
      private String username; // 用户名
      private String password; // 密码
      private String name; // 姓名
      private Integer age; // 年龄

      // getter和setter方法
      // ...
      }
  3. 配置MyBatis
    application.properties文件中配置数据库连接信息:

    1
    2
    3
    4
    spring.datasource.url=jdbc:mysql://localhost:3306/web
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=1234

1.2 编写MyBatis程序

编写MyBatis的持久层接口,定义SQL

1
2
3
4
5
@Mapper
public interface UserMapper {
@Select("select * from user")
public List<User> findAll();
}

1.3 辅助配置-配置MyBatis的日志输出

默认情况下,在MyBatis中,SQL语句执行时,我们并看不到SQL语句的执行日志。加入如下配置,即可查看日志:

1
2
# mybatis的配置
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

数据库连接池

1. 数据库连接池概述

数据库连接池是一种创建和管理数据库连接的技术,它允许应用程序从一个预创建的连接池中获取连接,使用完毕后将连接归还到池中,而不是每次都创建和销毁连接。

1.1 数据库连接池的优势

  • 提高性能:避免频繁创建和销毁数据库连接的开销
  • 资源管理:控制数据库连接的数量,防止资源耗尽
  • 连接复用:通过连接复用,减少系统开销
  • 统一配置:集中管理数据库连接参数

2. 切换到Druid数据库连接池

Druid是阿里巴巴开源的数据库连接池,具有强大的监控功能和扩展性。

2.1 引入Druid依赖

pom.xml文件中添加Druid依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.19</version>
</dependency>

2.2 配置Druid数据源

application.properties文件中配置数据源类型为Druid:

1
2
3
4
5
6
7
8
# 数据源类型配置为Druid
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/web
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=1234

2.3 常用Druid配置参数

可以根据需要添加更多Druid配置参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 初始化时建立物理连接的个数
spring.datasource.druid.initial-size=5

# 连接池的最小空闲数量
spring.datasource.druid.min-idle=5

# 连接池最大连接数量
spring.datasource.druid.max-active=20

# 获取连接时最大等待时间(毫秒)
spring.datasource.druid.max-wait=60000

# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接(毫秒)
spring.datasource.druid.time-between-eviction-runs-millis=60000

# 配置一个连接在池中最小生存的时间(毫秒)
spring.datasource.druid.min-evictable-idle-time-millis=300000

# 测试连接是否可用的SQL语句
spring.datasource.druid.validation-query=SELECT 1

# 申请连接时执行validationQuery检测连接是否有效
spring.datasource.druid.test-on-borrow=false

# 归还连接时执行validationQuery检测连接是否有效
spring.datasource.druid.test-on-return=false

# 连接空闲时执行validationQuery检测连接是否有效
spring.datasource.druid.test-while-idle=true

Mybatis-增删改查-新增用户

1. 新增用户需求

需求:添加一个用户到数据库中。

SQL语句示例

1
insert into user(username,password,name,age) values('zhouyu','123456','周瑜',20);

2. Mapper接口实现

2.1 错误示例

不要使用硬编码的SQL语句,这样无法接收动态参数:

1
2
3
// 错误的做法
@Insert("insert into user(username,password,name,age) values('zhouyu', '123456', '周瑜', 20)")
public void insert();

2.2 正确示例

应该使用#{}占位符接收User对象的属性值:

1
2
3
// 正确的做法
@Insert("insert into user(username,password,name,age) values(#{username},#{password},#{name},#{age})")
public void insert(User user);

3. 实现说明

  1. 参数类型:使用User对象作为方法参数,这样可以一次性传递多个字段的值
  2. 占位符:使用#{属性名}的形式引用User对象的属性值,MyBatis会自动从对象中获取对应属性的值
  3. 返回值:可以使用void,也可以使用Integer表示受影响的行数

4. 测试插入功能

可以在Controller中添加插入接口,或者使用单元测试来测试插入功能:

4.1 添加插入接口

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;

// 新增用户
@PostMapping("/insert")
public String insert(@RequestBody User user) {
userMapper.insert(user);
return "添加成功";
}
}

4.2 使用Postman测试插入接口

  1. 启动SpringBoot应用程序
  2. 打开Postman,选择POST请求方式
  3. 输入请求URL:http://localhost:8080/user/insert
  4. 在Body中选择JSON格式,输入用户信息:
    1
    2
    3
    4
    5
    6
    {
    "username": "zhouyu",
    "password": "123456",
    "name": "周瑜",
    "age": 20
    }
  5. 点击发送按钮
  6. 查看响应结果:”添加成功”

Mybatis-增删改查-修改用户

1. 修改用户需求

需求:根据ID更新用户信息。

SQL语句示例

1
update user set username = 'zhouyu', password = '123456', name = '周瑜', age = 20 where id = 1;

2. Mapper接口实现

1
2
@Update("update user set username=#{username}, password=#{password}, name=#{name}, age=#{age} where id=#{id}")
public void update(User user);

3. 实现说明

  1. 参数类型:使用User对象作为方法参数,包含所有需要更新的字段和ID
  2. 占位符:使用#{属性名}引用User对象的属性值
  3. where条件:必须包含ID条件,否则会更新所有记录

4. 测试修改功能

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;

// 修改用户
@PutMapping("/update")
public String update(@RequestBody User user) {
userMapper.update(user);
return "更新成功";
}
}

Mybatis-增删改查-查询用户

1. 查询用户需求

需求:根据用户名和密码查询用户信息。

SQL语句示例

1
select * from user where username = 'zhouyu' and password = '666888';

2. Mapper接口实现

2.1 直接参数传递

1
2
@Select("select * from user where username=#{username} and password=#{password}")
public User findByUsernameAndPassword(String username, String password);

2.2 使用@Param注解

如果方法参数名与SQL中的占位符不一致,可以使用@Param注解为参数起别名:

1
2
@Select("select * from user where username=#{username} and password=#{password}")
public User findByUsernameAndPassword(@Param("username") String username, @Param("password") String password);

3. @Param注解说明

@Param注解的作用是为接口的方法形参起名字,便于在SQL语句中引用参数值。

4. 测试查询功能

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;

// 根据用户名和密码查询用户
@GetMapping("/find")
public User findByUsernameAndPassword(String username, String password) {
return userMapper.findByUsernameAndPassword(username, password);
}
}

Mybatis-增删改查-删除操作

1. 使用MyBatis完成删除操作

使用MyBatis完成删除操作的步骤与查询操作类似,但需要使用@Delete注解来定义删除SQL语句。

2. 编写删除操作的Mapper接口方法

在之前创建的UserMapper接口中添加删除操作的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Mapper
public interface UserMapper {
// 查询所有用户
@Select("select * from user")
public List<User> findAll();

// 新增用户
@Insert("insert into user(username,password,name,age) values(#{username},#{password},#{name},#{age})")
public void insert(User user);

// 修改用户
@Update("update user set username=#{username}, password=#{password}, name=#{name}, age=#{age} where id=#{id}")
public void update(User user);

// 根据用户名和密码查询用户
@Select("select * from user where username=#{username} and password=#{password}")
public User findByUsernameAndPassword(@Param("username") String username, @Param("password") String password);

// 根据ID删除用户
@Delete("delete from user where id = #{id}")
public Integer deleteById(Integer id);

// 批量删除用户
@Delete("<script>delete from user where id in (<foreach collection='ids' item='id' separator=','>#{id}</foreach>)</script>")
public Integer batchDelete(@Param("ids") List<Integer> ids);
}

4. 执行删除操作的注意事项

  1. 参数传递:删除操作通常需要传递一个或多个参数来确定要删除的记录。在MyBatis中,可以使用#{}占位符来接收参数。

  2. 返回值:删除操作的返回值通常是一个整数,表示受影响的行数。如果返回值大于0,则表示删除成功;否则表示删除失败。

  3. 事务处理:删除操作是一个DML操作,默认情况下会自动提交事务。如果需要手动控制事务,可以在方法上添加@Transactional注解。

  4. SQL注入防护:MyBatis的#{}占位符会自动进行参数类型转换和SQL注入防护,因此不需要手动处理。

5. 执行删除操作的示例

5.1 使用Postman测试删除接口

  1. 启动SpringBoot应用程序
  2. 打开Postman,选择DELETE请求方式
  3. 输入请求URL:http://localhost:8080/user/deleteById?id=1
  4. 点击发送按钮
  5. 查看响应结果:”删除成功”

5.2 查看数据库记录变化

执行删除操作后,可以查看数据库中的记录,确认ID为1的用户是否已被删除。

6. 批量删除操作

如果需要同时删除多条记录,可以使用MyBatis的批量删除功能。例如:

1
2
3
4
5
6
@Mapper
public interface UserMapper {
// 批量删除用户
@Delete("<script>delete from user where id in (<foreach collection='ids' item='id' separator=','>#{id}</foreach>)</script>")
public Integer batchDelete(@Param("ids") List<Integer> ids);
}

注意:使用批量删除时,需要使用<script>标签包裹SQL语句,并使用<foreach>标签遍历参数列表。

6. MyBatis高级特性补充

6.1 获取插入数据后的主键值

在插入数据之后,可以通过以下方式获取到自动生成的主键值:

1
2
3
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into user(name, age) values(#{name}, #{age})")
public Integer insert(User user);

参数说明

  • useGeneratedKeys = true:开启自动生成主键
  • keyProperty = "id":指定主键对应的实体类属性名

执行插入操作后,MyBatis会自动将生成的主键值设置到传入的User对象的id属性中。

6.2 <foreach>动态SQL标签

作用

遍历集合/数组,用于构建动态SQL语句。

属性含义

属性 描述 是否必选
collection 集合名称
item 集合遍历出来的元素/项
separator 每一次遍历使用的分隔符
open 遍历开始前拼接的片段
close 遍历结束后拼接的片段

使用示例

批量删除

1
2
@Delete("<script>delete from user where id in (<foreach collection='ids' item='id' separator=','>#{id}</foreach>)</script>")
public Integer batchDelete(@Param("ids") List<Integer> ids);

批量插入

1
2
@Insert("<script>insert into user(name, age) values <foreach collection='users' item='user' separator=','>(#{user.name}, #{user.age})</foreach></script>")
public Integer batchInsert(@Param("users") List<User> users);

XML映射文件中的批量删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--根据id删除员工基本信息-->
<delete id="deleteById">
DELETE FROM emp
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>

<!--根据id删除员工工作经历-->
<delete id="deleteByExprId">
delete from emp_expr where emp_id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>

注意事项

  • 在使用<foreach>标签构建IN条件时,open="("close=")"属性非常重要,它们会在遍历开始和结束时分别拼接左括号和右括号,确保生成的SQL语句语法正确。
  • 如果缺少这些属性,生成的SQL语句会变成WHERE id IN 1,2,3而不是WHERE id IN (1,2,3),导致SQL语法错误。

7. MyBatis中的#和$符号

在MyBatis中,#{}和${}`是两种不同的参数传递方式,它们在实现原理、安全性和使用场景上存在明显区别。

7.1 #{}和${}的区别

符号 说明 场景 优缺点
#{} 占位符。执行时,会将#{…}替换为?,生成预编译SQL 参数值传递 安全、性能高(推荐)
${} 拼接符。直接将参数拼接在SQL语句中,存在SQL注入问题 表名、字段名动态设置时使用 不安全、性能低

7.2 使用示例

7.2.1 #{}的使用示例

1
2
3
// 根据ID删除部门
@Delete("delete from dept where id = #{id}")
public Integer deleteDeptById(Integer id);

执行时,MyBatis会将#{id}替换为?,生成预编译SQL:

1
delete from dept where id = ?

然后将参数值传递给预编译的SQL语句,这样可以防止SQL注入攻击。

7.2.2 ${}的使用示例

1
2
3
// 根据动态表名和排序字段查询数据
@Select("select id, name, score from ${tableName} order by ${sortField}")
public List<Student> findStudents(@Param("tableName") String tableName, @Param("sortField") String sortField);

执行时,MyBatis会直接将参数值拼接到SQL语句中:

1
select id, name, score from student order by score

这种方式适用于表名或字段名需要动态设置的场景,但存在SQL注入风险,因此不推荐用于普通参数传递。

7.3 如何选择

  • **优先使用#{}**:对于普通参数传递,优先使用#{},因为它更安全、性能更高
  • **谨慎使用${}**:只有在需要动态设置表名、字段名等场景下才使用${},并且需要确保参数值的安全性

7.4 防止SQL注入

使用${}时,需要特别注意防止SQL注入攻击:

  1. 参数验证:对传入的参数进行严格验证,确保只包含允许的字符
  2. 白名单机制:只允许特定的表名或字段名
  3. 使用预编译:如果可能,尽量使用#{}代替${}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 安全的动态表名查询
public List<Student> findStudents(String tableName, String sortField) {
// 验证表名是否在白名单中
List<String> validTables = Arrays.asList("student", "teacher", "course");
if (!validTables.contains(tableName)) {
throw new IllegalArgumentException("Invalid table name");
}

// 验证排序字段是否在白名单中
List<String> validSortFields = Arrays.asList("id", "name", "score", "age");
if (!validSortFields.contains(sortField)) {
throw new IllegalArgumentException("Invalid sort field");
}

// 执行查询
return studentMapper.findStudents(tableName, sortField);
}

8. XML映射配置

在MyBatis中,既可以通过注解配置SQL语句,也可以通过XML配置文件配置SQL语句。

8.1 默认规则

  1. 文件命名与位置:XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)。

  2. 命名空间配置:XML映射文件的namespace属性与Mapper接口全限定名一致。

  3. SQL语句配置:XML映射文件中SQL语句的id与Mapper接口中的方法名一致,并保持返回类型一致。

8.2 项目结构示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mybatis-demo
└── src
└── main
├── java
│ └── com.itheima
│ ├── mapper
│ │ └── UserMapper.java # Mapper接口
│ ├── pojo
│ │ └── User.java
│ └── MybatisDemoApplication.java
└── resources
├── com
│ └── itheima
│ └── mapper
│ └── UserMapper.xml # XML映射文件
└── application.properties

8.3 接口与XML示例

**Mapper接口 (UserMapper.java)**:

1
2
3
public interface UserMapper {
public List<User> findAll();
}

**XML映射文件 (UserMapper.xml)**:

1
2
3
4
5
<mapper namespace="com.itheima.mapper.UserMapper">
<select id="findAll" resultType="com.itheima.pojo.User">
select id, username, password, name, age from user
</select>
</mapper>

8.4 XML映射的优势

  • 复杂SQL支持:对于复杂的SQL语句(如多表关联查询、动态SQL等),XML配置方式更加清晰和易于维护
  • SQL语句集中管理:所有SQL语句都集中在XML文件中,便于统一管理和优化
  • 代码与SQL分离:业务代码与SQL语句完全分离,提高代码的可读性和可维护性
  • 更好的格式化支持:XML文件支持SQL语句的格式化,便于阅读和调试

8.5 注解与XML的选择

  • 注解方式:适用于简单的SQL语句,配置简单,开发效率高
  • XML方式:适用于复杂的SQL语句,维护性好,可读性强

在实际开发中,可以根据具体情况选择合适的配置方式,也可以混合使用两种方式。

8.6 XML映射文件位置配置

在Spring Boot项目中,可以在application.properties文件中指定XML映射配置文件的位置:

1
2
# 指定XML映射配置文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml

项目结构示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring-boot-mybatis-quickstart
└── src
└── main
├── java
│ └── com.itheima
│ ├── mapper
│ │ └── UserMapper.java
│ ├── pojo
│ └── SpringbootMybatisQuickstartApplication.java
└── resources
├── mapper
│ └── *.xml # XML映射文件放在此处
└── application.properties

8.7 MyBatisX插件

MyBatisX是一款基于IDEA的快速开发MyBatis的插件,为效率而生。

主要功能:

  • mapper.xml和mapper.java可以互相跳转
  • mybatis.xml文件支持代码提示
  • mapper接口和xml映射文件支持类似JPA的自动提示和引用

安装方式:

  1. 打开IDEA的Settings(设置)
  2. 选择Plugins(插件)
  3. 在Marketplace中搜索”mybatisx”
  4. 点击Install(安装)按钮进行安装
  5. 安装完成后重启IDEA即可使用

9. resultMap

9.1 问题描述

MyBatis在查询时,当实体类的属性名与表的字段名不一致时,会出现无法自动封装数据的问题。

例如:

  • 实体类属性:id、username、password、name、age
  • 表字段名:id、username、password、real_name、age

此时,由于namereal_name不一致,导致查询结果中name属性为null。

9.2 解决方案

方案一:使用别名

1
2
3
<select id="findAll" resultType="com.itheima.pojo.User">
select id, username, password, real_name as name, age from user
</select>

方案二:使用resultMap

resultMap是MyBatis提供的用于解决实体类属性名与表字段名不一致问题的一种方式,它可以定义映射规则,将查询结果中的列映射到实体类的属性中。

使用步骤:

  1. 在XML映射文件中定义resultMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!-- 定义resultMap,id为映射规则的唯一标识,type为实体类的全限定名 -->
    <resultMap id="userResultMap" type="com.itheima.pojo.User">
    <!-- id元素用于映射主键,column为表的主键字段,property为实体类的主键属性 -->
    <id column="id" property="id"/>

    <!-- result元素用于映射普通字段,column为表的字段名,property为实体类的属性名 -->
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <result column="real_name" property="name"/>
    <result column="age" property="age"/>
    </resultMap>
  2. 在查询语句中使用resultMap

    1
    2
    3
    4
    <!-- 使用resultMap属性引用定义好的映射规则,而不是使用resultType -->
    <select id="findAll" resultMap="userResultMap">
    select id, username, password, real_name, age from user
    </select>

9.3 resultMap的优势

  • 可复用性:定义一次映射规则,可以在多个查询语句中复用
  • 灵活性:支持复杂的映射关系(如一对一、一对多、多对多关联查询)
  • 可读性:将映射规则集中管理,提高代码的可读性和可维护性
  • 性能:比使用别名的方式性能更好,因为别名需要在每次查询时都进行解析

9.4 自动映射

MyBatis默认支持自动映射,即当实体类的属性名与表的字段名一致时,会自动将查询结果封装到实体类中。

自动映射的条件:

  • 实体类的属性名与表的字段名一致(不区分大小写)
  • 开启自动映射功能(默认开启)

开启自动映射的配置:

1
2
# 开启自动映射
mybatis.configuration.map-underscore-to-camel-case=true

该配置会将表中的下划线命名(如real_name)自动映射到实体类的驼峰命名(如realName)。

9.5 复杂映射

resultMap还支持复杂的映射关系,如:

  • 一对一关联:使用<association>元素
  • 一对多关联:使用<collection>元素
  • 多对多关联:通过中间表实现,结合<collection>元素

这些高级特性使得MyBatis能够处理复杂的业务场景,满足各种数据查询需求。

9.6 基于注解的结果映射

除了XML映射文件外,MyBatis也支持使用注解进行结果映射,适用于简单的映射场景。

9.6.1 手动结果映射(@Results和@Result)

使用@Results@Result注解可以实现手动结果映射,用于解决实体类属性名与表字段名不一致的问题。

示例:

1
2
3
4
5
6
@Results({
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime")
})
@Select("select id, name, create_time, update_time from dept order by update_time desc")
public List<Dept> findAll();

9.6.2 使用别名

在SQL语句中,对不一致的列名起别名,使别名与实体类属性名一致。

示例:

1
2
@Select("select id, name, create_time createTime, update_time updateTime from dept order by update_time desc")
public List<Dept> findAll();

9.6.3 开启驼峰命名转换

如果字段名与属性名符合驼峰命名规则,MyBatis可以自动通过驼峰命名规则进行映射。

配置方式(application.yml):

1
2
3
mybatis:
configuration:
map-underscore-to-camel-case: true

工作原理:

  • 表字段名:create_time → 实体类属性名:createTime
  • 表字段名:update_time → 实体类属性名:updateTime
  • 表字段名:user_name → 实体类属性名:userName

9.6.4 三种方式对比

方式 优点 缺点 适用场景
@Results/@Result 配置灵活,无需修改SQL 代码较长,重复配置 字段映射关系较少的情况
别名 SQL清晰,易于理解 SQL语句冗余,修改麻烦 简单映射场景
驼峰命名转换 配置一次,全局生效 仅适用于驼峰命名规则 符合驼峰命名规范的项目

9.7 最佳实践

  • 简单映射:优先使用驼峰命名转换
  • 少量特殊映射:使用别名或@Results注解
  • 复杂映射:使用XML格式的resultMap
  • 统一风格:在项目中保持一致的映射方式

10. YAML配置文件

YAML(YAML Ain’t Markup Language)是一种人类可读的数据序列化语言,常用于配置文件。在Spring Boot项目中,YAML配置文件是一种常用的配置方式。

10.1 YAML格式规则

  • 数值格式:数值前边必须有空格,作为分隔符
  • 层级关系:使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(IDEA中会自动将Tab转换为空格)
  • 缩进要求:缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • 注释格式:使用#表示注释,从这个字符一直到行尾,都会被解析器忽略

10.2 YAML数据结构

10.2.1 定义对象/Map集合

1
2
3
4
user:
name: 张三
age: 18
password: 123456

10.2.2 定义数组/List/Set集合

1
2
3
4
hobby:
- java
- game
- sport

10.3 特殊注意事项

在YAML格式的配置文件中,如果配置项的值是以 0 开头的,值需要使用 '' 引起来,因为以0开头在YAML中表示8进制的数据。

10.4 完整示例

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 配置服务相关信息
server:
port: 8080
address: 127.0.0.1

# 配置应用信息
spring:
application:
name: SpringBoot-web-02
datasource:
url: jdbc:mysql://localhost:3306/db01
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456

# 配置MyBatis
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml

11. 前后端分离开发

11.1 概述

当前最为主流的开发模式:前后端分离

11.2 工作原理

前后端分离开发模式下,前端和后端是两个独立的系统,它们通过接口进行通信:

  1. 前端系统

    • 负责用户界面的展示和用户交互
    • 技术栈:HTML、CSS、JavaScript(Vue、React、Angular等框架)
    • 通常通过NGINX等Web服务器进行部署
  2. 后端系统

    • 负责业务逻辑处理、数据存储和管理
    • 技术栈:Java(Spring Boot等框架)、数据库等
    • 提供RESTful API或GraphQL接口供前端调用
  3. 通信方式

    • 前端通过HTTP请求向后端发送数据请求
    • 后端处理请求后,返回JSON或XML格式的数据响应
    • 前后端通过接口文档约定通信格式和参数

11.3 核心优势

  • 职责清晰:前端专注于用户体验,后端专注于业务逻辑
  • 并行开发:前后端团队可以同时进行开发,提高开发效率
  • 技术栈独立:前后端可以选择适合各自需求的技术栈
  • 可维护性高:接口文档清晰记录每一个功能,便于后续维护和扩展
  • 易于扩展:前后端可以独立扩展和部署,适应不同的业务需求

11.4 关键角色

  • 前端开发者:负责构建用户界面和用户交互逻辑
  • 后端开发者:负责实现业务逻辑和数据处理
  • 接口设计师:负责设计和维护接口文档
  • 测试工程师:负责测试前端功能、后端接口和整个系统的集成

11.5 RESTful API设计

REST (REpresentational State Transfer),表述性状态转换,它是一种软件架构风格。

11.5.1 RESTful设计规范

REST风格URL 请求方式 含义 备注
http://localhost:8080/users/1 GET 查询id为1的用户
http://localhost:8080/users/1 DELETE 删除id为1的用户
http://localhost:8080/users POST 新增用户
http://localhost:8080/users PUT 修改用户

11.5.2 设计原则

  • URL定位资源:使用URL来唯一标识资源
  • HTTP动词描述操作:使用GET/POST/PUT/DELETE等HTTP方法描述对资源的操作
  • 简洁、规范、优雅:保持API的简洁性和一致性

11.5.3 重要注意事项

  1. REST是风格,是约定方式,约定不是规定,可以打破
  2. 描述功能模块通常使用复数形式(加s),表示此类资源,而非单个资源。如:users、books…