算法刷题笔记

Java - Web 开发笔记 Ⅱ

2024-05-20  本文已影响0人  Du1in9

👉 在线笔记:https://du1in9.github.io/javaweb.github.io/

第4章 - SpringBootWeb & 原理

4.1 SpringBootWeb

4.1.1 快速入门

Spring 发展到今天已经形成了一种开发生态圈,Spring 提供了若干个子项目,每个项目用于完成特定的功能

Spring Boot 可以帮助我们非常快速的构建应用程序、简化开发、提高效率

// 需求:浏览器发起请求 /hello 后,给浏览器返回字符串 "Hello World"

package com.itheima.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class hellocontroller {
    @RequestMapping("/hello")
    public String hello() {
        return "hello world";
    }
}

4.1.2 HTTP 协议

① HTTP - 概述

Hyper Text Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则

② HTTP - 请求协议

请求头 解释
Host 请求的主机名
User-Agent 浏览器版本
Accept 表示浏览器能接收的资源类型
Accept-Language 表示浏览器偏好的语言
Accept-Encoding 表示浏览器可以支持的压缩类型
Content-Type 请求主体的数据类型
Content-Length 请求主体的大小,单位 byte

③ HTTP - 响应协议

状态码 英文描述 解释
200 OK 客户端请求成功,即处理成功,这是我们最想看到的状态码
302 Found 资源已移动到由 Location 响应头给定的 URL,浏览器会自动重新访问到这个页面
304 Not Modified 请求资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向
400 Bad Request 客户端请求有语法错误,不能被服务器所理解
403 Forbidden 服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源
404 Not Found 请求资源不存在,一般是URL输入有误,或者网站资源被删除了
405 Method Not Allowed 请求方式有误,比如应该用GET请求方式的资源,用了POST
428 Precondition Required 服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头
429 Too Many Requests 指示用户在给定时间内发送了太多请求,配合 Retry-After响应头一起使用
431 Request Header Fields Too Large 请求头太大,服务器不愿意处理,因为它的头部字段太大,减少大小后重新提交
500 Internal Server Error 服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧
503 Service Unavailable 服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好
响应头 解释
Content-Type 表示该响应内容的类型,例如 text/html,application/json
Content-Length 表示该响应内容的长度,单位 byte
Content-Encoding 表示该响应压缩算法,例如 gzip
Cache-Control 指示客户端应如何缓存,例如 max-age=300 表示最多缓存300秒
Set-Cookie 告诉浏览器为当前页面所在的域设置 cookie

4.1.3 WEB 服务器 - Tomcat

① 简介

  1. Web 服务器:对 HTTP 协议操作进行封装,简化 web 程序开发;部署 web 项目,对外提供网上信息浏览服务

  2. Tomcat:一个轻量级的 web 服务器,支持 servlet、jsp 等少量 javaEE 规范;也被称为 web 容器、servlet 容器

② 基本使用

③ 入门程序解析

基于 Springboo t开发的 web 应用程序,内置了 tomcat 服务器,当启动类运行时会自动启动

4.1.4 SpringBootWeb 请求

① Postman

Postman 是一款功能强大的网页调试与发送网页 HTTP 请求的 Chrome 插件

作用:常用于进行接口测试

② 简单参数

  1. 原始方式获取请求参数

Controller 方法形参中声明 HttpServletRequest 对象,调用对象的 getParameter (参数名)

@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request){
    String name = request.getParameter("name");
    String ageStr = request.getParameter("age");
    int age = Integer.parseInt(ageStr);
    System.out.println(name+ ":" + age);
    return "OK";
}
  1. SpringBoot 中接收简单参数

请求参数名与方法形参变量名相同,会自动进行类型转换

@RequestMapping("/simpleParam")
public String simpleParam(String name, Integer age){
    System.out.println(name+ ":" + age);
    return "OK";
}
// get: url: http://localhost:8080/simpleParam?name=Tom&age=20
// post: url: http://localhost:8080/simpleParam; body: name=Tom, age=20
  1. @RequestParam 注解

方法形参名称与请求参数名称不匹配,通过该注解完成映射

@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name") String username, Integer age){
    System.out.println(username+ ":" + age);
    return "OK";
}

该注解的 required 属性默认是 true,代表请求参数必须传递

③ 实体参数

  1. 简单实体对象:请求参数名与形参对象属性名相同,定义 POJO 接收即可

    @RequestMapping("/simplePojo")
    public void simplePojo(User user){
        System.out.println(user);
    }        // get: ...?name=Tom&age=20
    
    public class User {
        private String name;
        private Integer age;
        ...(javabean)
    }
    
  2. 复杂实体对象:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套 POJO 属性参数

    @RequestMapping("/complexPojo")
    public void complexPojo(User user){
        System.out.println(user);
    }        // get: ...?name=Tom&age=20&Address.province=beijing&Address.city=beijing
    
    public class User {
        private String name;
        private Integer age;
        private Address address;
        ...(javabean)
    }
    
    public class Address {
        private String province;
        private String city;
        ...(javabean)
    }
    

④ 数组集合参数

@RequestMapping("/arrayParam")
public void arrayParam(String[] hobby){
    System.out.println(Arrays.toString(hobby));     // [rap, dance]
}           // get: ...?hobby=rap&hobby=dance

@RequestMapping("/listParam")       
public void listParam(@RequestParam List<String> hobby){
    System.out.println(hobby);                      // [rap, dance]
}           // get: ...?hobby=rap&hobby=dance

⑤ 日期参数

使用 @DateTimeFormat 注解完成日期参数格式转换

@RequestMapping("/dateParam")
public void dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime time){
    System.out.println(time);       // 2022-12-02T10:05:51
}           // get: ...?time=2022-12-02 10:05:51

⑥ JSON 参数

JSON 数据键名与形参对象属性名相同,定义 POJO 类型形参即可接收参数,需要使用 @RequestBody 标识

@RequestMapping("/jsonParam")
public void jsonParam(@RequestBody User user){
    System.out.println(user);
}           // User{name='tom', age=20, address=Address{province='beijing', city='beijing'}}
// post: body: raw:
{
    "name":"tom",
    "age":20,
    "address":{
        "province":"beijing",
        "city":"beijing"
    }
}

⑦ 路径参数

通过请求 URL 直接传递参数,使用 {…} 来标识该路径参数,需要使用 @PathVariable 获取路径参数

@RequestMapping("/path/{id}")
public void pathParam(@PathVariable Integer id){
    System.out.println(id);                 // 123
}       // get: http://localhost:8080/path/123

@RequestMapping("/path/{id}/{name}")
public void pathParam2(@PathVariable Integer id , @PathVariable String name){
    System.out.println(id + ":" + name);    // 123:Alice
}       // get: http://localhost:8080/path/123/Alice

4.1.5 SpringBootWeb 响应

① @ResponseBody

② 统一响应结果

@RequestMapping("/hello")
public Result hello(){
    ...
    return Result.success("Hello World ~");
}

@RequestMapping("/getAddr")
public Result getAddr(){
    ...
    return Result.success(addr);
}

@RequestMapping("/listAddr")
public Result listAddr(){
    ...
    return Result.success(list);
}
public class Result {
    private Integer code ;  // 1 成功, 0 失败
    private String msg;     // 提示信息
    private Object data;    // 数据 date
    
    public static Result success(){
        return new Result(1, "success", null);
    }
    public static Result success(Object data){
        return new Result(1, "success", data);
    }
    public static Result error(String msg){
        return new Result(0, msg, null);
    }
    ...(javabean)
}

③ 案例

需求:a. 获取员工数据,b. 返回统一响应结果,c. 在页面渲染展示

  1. 在 pom.xml 文件中引入 dom4j 的依赖,用于解析 XML 文件
<!-- pom.xml: 引入依赖, 用来解析xml文件 -->
<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.3</version>
</dependency>
  1. 引入资料中提供的解析 XML 的工具类 XMLParserUtils、对应的实体类 Emp、XML 文件 emp.xml
// XMLParserUtils.java: 工具类, 用来解析xml文件
public class XmlParserUtils {...}
<!-- emp.xml: 员工数据 -->
<emps>
    <emp>
        <name>金毛狮王</name>
        <age>55</age>
        <image>https://web-framework.oss-cn-hangzhou.aliyuncs.com/web/1.jpg</image>
        <!-- 1: 男, 2: 女 -->
        <gender>1</gender>
        <!-- 1: 讲师, 2: 班主任 , 3: 就业指导 -->
        <job>1</job>
    </emp>
    ...
</emps>
// Emp.java: 员工类
public class Emp {
    private String name;
    private Integer age;
    private String image;
    private String gender;
    private String job;
    ...(javabean)
}
  1. 引入资料中提供的静态页面文件,放在 resources 下的 static 目录下
<!-- emp.html: 展示页面, 用来接收统一结果, c.并渲染展示 -->
<body>
    <div id="app">
        <el-table :data="tableData" style="width: 100%"  stripe border >
            ...         
        </el-table>
    </div>
</body>

<script>
    new Vue({
        mounted(){
            axios.get('/listEmp').then(res=>{
                if(res.data.code){                  <!-- 根据 Result 的 code 判断 -->
                    this.tableData = res.data.data; <!-- 根据 Result 的 data 渲染 -->
                }
            });
        }
    });
</script>

注意:Springboot 项目的静态资源(html,css,js 等前端资源)默认存放目录为:classpath : /static 、/public、/resources

  1. 编写 Controller 程序,处理请求,响应数据
// EmpController.java: Controller程序, a.用来获取员工数据, b.并返回统一响应结果

@RestController
public class EmpController {
    @RequestMapping("/listEmp")
    public Result list(){
        //1. 加载并解析emp.xml
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
        //2. 对数据进行转换处理 - gender, job
        empList.stream().forEach(emp -> {
            String gender = emp.getGender();
            if("1".equals(gender)){
                emp.setGender("男");
            }else if("2".equals(gender)){
                emp.setGender("女");
            }
            String job = emp.getJob();
            if("1".equals(job)){
                emp.setJob("讲师");
            }else if("2".equals(job)){
                emp.setJob("班主任");
            }else if("3".equals(job)){
                emp.setJob("就业指导");
            }
        });
        //3. 响应数据
        return Result.success(empList);
    }
}

4.1.6 SpringBootWeb 分层解耦

① 三层架构

public class EmpController {
    private EmpService empService = new EmpServiceA();  // 调用service获取数据
    @RequestMapping("/listEmp")
    public Result list(){                           
        List<Emp> empList = empService.listEmp();   
        return Result.success(empList);                 // 3. 响应数据
    }
}
public interface EmpService {
    public List<Emp> listEmp();             
}
public class EmpServiceA implements EmpService {
    private EmpDao empDao = new EmpDaoA();      // 调用dao获取数据
    @Override
    public List<Emp> listEmp() {
        List<Emp> empList = empDao.listEmp();   
        empList.stream().forEach(emp -> {       
            ...                                 // 2. 对数据进行处理
        }); 
        return empList;                         // 返回service
    }
}
public interface EmpDao {
    public List<Emp> listEmp();                 
}
public class EmpDaoA implements EmpDao {        
    @Override
    public List<Emp> listEmp() {                // 1. 加载并解析emp.xml
        String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
        List<Emp> empList = XmlParserUtils.parse(file, Emp.class);
        return empList;                         // 返回 dao
    }
}

② 分层解耦

分层解耦

③ IOC & DI

  1. Service 层及 Dao 层的实现类,交给 IOC 容器管理
@Component          // 将当前对象交给 IOC 容器管理,成为 IOC 容器的 bean
public class EmpDaoA implements EmpDao {
}
@Component          // 将当前对象交给 IOC 容器管理,成为 IOC 容器的 bean
public class EmpServiceA implements EmpService {
}
  1. 为 Controller 及 Service 注入运行时,依赖的对象
@Component
public class EmpServiceA implements EmpService {
    @Autowired      // 运行时,需要从 IOC 容器中获取该类型对象,赋值给该变量
    private EmpDao empDao;
}
public class EmpController {
    @Autowired      // 运行时,需要从 IOC 容器中获取该类型对象,赋值给该变量
    private EmpService empService;
}

1. IOC 详解

注解 说明 位置
@Component 声明bean的基础注解 不属于以下三类时,用此注解
@Controller @Component的衍生注解 标注在控制器类上
@Service @Component的衍生注解 标注在业务类上
@Repository @Component的衍生注解 标注在数据访问类上(与 mybatis 整合,用的少)
@RestController
public class EmpController {...}
@Service
public class EmpServiceA implements EmpService {...}
@Repository("daoA")
public class EmpDaoA implements EmpDao {...}

@ComponentScan 注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中

// @ComponentScan({"com.itheima.dao","com.itheima"})
@SpringBootApplication      //默认扫描当前包及其子包
public class SpringbootWebReqRespApplication {...}

2. DI 详解

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

通过以下几种方案来解决:@Primary、@Qualifier、@Resource

public class EmpController {
    @Qualifier("empServiceA")
    @Autowired
    private EmpService empService;
}
public class EmpController {
    @Resource(name = "empServiceA")
    private EmpService empService;
}

@Primary
@Service
public class EmpServiceA implements EmpService {...}
@Service
public class EmpServiceB implements EmpService {...}

@Resource 与 @Autowired 区别:(面试题)

4.2 SpringBootWeb AOP

4.2.1 Spring 事务管理

① 事务管理

事务 是一组操作的集合,它是一个不可分割的工作单位,这些操作 要么同时成功,要么同时失败

案例:解散部门:删除部门,同时删除该部门下的员工

// main.java.com.itheima.service\impl\EmpServiceImpl.java

@Transactional
@Override
public void delete(Integer id) {
    deptMapper.deleteById(id);
    int i = 1/0;                    // 回滚事务, 部门删除失败
    empMapper.deleteByDeptId(id);   // 异常处理, 未删除部门员工
}
// main.java.com.itheima.mapper\EmpMapper.java

@Delete("delete  from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);
// main.java.com.itheima.service\impl\EmpServiceImpl.java

@Transactional(rollbackFor = Exception.class)
@Override
public void delete(Integer id) throws Exception {
    deptMapper.deleteById(id);
    if(true){throw new Exception("出错啦...");}
    empMapper.deleteByDeptId(id);
}

② 事务传播行为

事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制

属性值 含义
REQUIRED 需要事务,有则加入,无则创建新事务【默认值】
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
// REQUIRED :大部分情况下都是用该传播行为即可
@Transactional(propagation = Propagation.REQUIRED)
// REQUIRES_NEW: 当我们不希望事务之间相互影响时,可以使用该传播行为
// 比如: 下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功
@Transactional(propagation = Propagation.REQUIRES_NEW)

4.2.2 AOP 基础

① AOP 概述

② AOP 快速入门

③ AOP 核心概念

  1. AOP 核心概念

    • 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)

    • 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

    • 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用

    • 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)

    • 目标对象:Target,通知所应用的对象

  1. AOP 执行流程

4.2.3 AOP 进阶

① 通知类型

@Slf4j
@Component
@Aspect
public class MyAspect1 {
    // 该注解将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可
    @Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    private void pt(){}

    @Before("pt()")             // 前置通知
    public void before(){log.info("before ...");}

    @After("pt()")              // 后置通知
    public void after(){log.info("after ...");}

    @AfterReturning("pt()")     // 返回后通知, 了解
    public void afterReturning(){log.info("afterReturning ...");}

    @AfterThrowing("pt()")      // 异常后通知, 了解
    public void afterThrowing(){log.info("afterThrowing ...");}

    @Around("pt()")             // 环绕通知, 重点
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before ...");
        Object result = proceedingJoinPoint.proceed();
        log.info("around after ...");
        return result;
    }
}

② 通知顺序

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行

  1. 不同切面类中,默认按照切面类的类名字母排序:

    • 目标方法前的通知方法:字母排名靠前的先执行

    • 目标方法后的通知方法:字母排名靠前的后执行

  2. 用 @Order(数字)加在切面类上来控制顺序:

    • 目标方法前的通知方法:数字小的先执行

    • 目标方法后的通知方法:数字小的后执行

@Order(2)
public class MyAspect1 {
    @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void before(){log.info("before ...1");}

    @After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void after(){log.info("after ...1");}
}

@Order(3)
public class MyAspect2 {
    @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void before(){log.info("before ...2");}
    
    @After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void after(){log.info("after ...2");}
}

@Order(1)
public class MyAspect3 {
    @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void before(){log.info("before ...3");}

    @After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void after(){log.info(" ...3");}
}
com.itheima.aop.MyAspect3           : before ...3
com.itheima.aop.MyAspect1           : before ...1
com.itheima.aop.MyAspect2           : before ...2
com.itheima.aop.MyAspect2           : after ...2
com.itheima.aop.MyAspect1           : after ...1
com.itheima.aop.MyAspect3           : after ...3

③ 切入点表达式

  1. execution (……):根据方法的签名来匹配

    语法格式:execution (访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)

    @Pointcut("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
    @Pointcut("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
    @Pointcut("execution(void delete(java.lang.Integer))")   // 包名.类名不建议省略
    @Pointcut("execution(void com.itheima.service.DeptService.delete(java.lang.Integer))")
    

    * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

    .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

    @Pointcut("execution(void com.itheima.service.DeptService.*(java.lang.Integer))")
    @Pointcut("execution(* com.*.service.DeptService.*(*))")
    @Pointcut("execution(* com.itheima.service.*Service.delete*(*))")
    
    @Pointcut("execution(* com.itheima.service.DeptService.*(..))")
    @Pointcut("execution(* com..DeptService.*(..))")
    @Pointcut("execution(* com..*.*(..))")
    @Pointcut("execution(* *(..))")      // 慎用
    

    根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式

    @Pointcut("execution(* com.itheima.service.DeptService.list()) || " +
              "execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
    

    书写建议

    • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update开头
    • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
    • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包
  2. @annotation (……) :根据注解匹配

    @annotation 切入点表达式,用于匹配标识有特定注解的方法

    // com.itheima.aop\MyAspect.java
    
    public class MyAspect {
        @Pointcut("@annotation(com.itheima.aop.MyLog)")
        private void pt(){}
    }
    
    // com.itheima.aop\MyLog.java
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface MyLog {}
    
    // com.itheima.service.impl\DeptServiceImpl.java
    
    public class DeptServiceImpl implements DeptService {
        @MyLog
        public List<Dept> list() {...}
        @MyLog
        public void delete(Integer id) {...}
    }
    

④ 连接点

在 Spring 中用 JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等

// com.itheima.aop\MyAspect.java

@Before("pt()")
public void before(JoinPoint joinPoint){}

@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    String className = joinPoint.getTarget().getClass().getName();
    log.info("目标对象的类名:{}", className);
    String methodName = joinPoint.getSignature().getName();
    log.info("目标方法的方法名: {}",methodName);
    Object[] args = joinPoint.getArgs();
    log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));
    Object result = joinPoint.proceed();
    log.info("目标方法运行的返回值: {}",result);
    return result;
}

4.2.4 AOP 案例

需求:将 SpringBootWeb 案例中 增、删、改 相关接口的操作日志记录到数据库表中

日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

思路:

准备:

编码:

4.3 SpringBoot 原理

4.3.1 配置优先级

  1. SpringBoot 中支持三种格式的配置文件:application.properties、application.yml、application.yaml

    虽然 springboot 支持多种格式配置文件,但是在项目开发时,推荐统一使用一种格式的配置 (yml 是主流)

  2. SpringBoot 除了支持配置文件属性配置,还支持 Java 系统属性和命令行参数的方式进行属性配置

    • 打包前如何配置:
  1. 优先级(低→高):yaml、yml、properties、java 系统属性、命令行参数

4.3.2 Bean 管理

① 获取 Bean

默认情况下,Spring 项目启动时,会把 bean 都创建好放在 IOC 容器中,如果想要主动获取这些 bean,可以通过如下方式:

@Autowired
private ApplicationContext applicationContext;  // IOC 容器对象

@Test
public void testGetBean(){  // 获取 bean 对象
    // 根据 bean 的名称获取
    DeptController bean1 = (DeptController) applicationContext.getBean("deptController");
    // 根据 bean 的类型获取
    DeptController bean2 = applicationContext.getBean(DeptController.class);
    // 根据 bean 的名称及类型获取
    DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
}

② Bean 作用域

作用域 说明
singleton 容器内同名称 的 bean 只有一个实例(单例)(默认)
prototype 每次使用该 bean 时会创建新的实例(非单例)
@Scope("prototype")
public class DeptController {
    public DeptController(){
        System.out.println("DeptController constructor ....");
    }
}
@Test
public void testScope(){
    for (int i = 0; i < 3; i++) {
        DeptController deptController = applicationContext.getBean(DeptController.class);
        System.out.println(deptController);
    }
}
// DeptController constructor ....
// com.itheima.controller.DeptController@34d9df9f
// DeptController constructor ....
// com.itheima.controller.DeptController@35c8be21
// DeptController constructor ....
// com.itheima.controller.DeptController@60807fd9

③ 第三方 Bean

// com.itheima.config\CommonConfig.java

@Configuration  // 配置类
public class CommonConfig {
    @Bean   // 将当前方法的返回值对象交给 IOC 容器管理, 成为 IOC 容器 bean
    public SAXReader reader(){
        return new SAXReader();
    }
}
@Autowired
private SAXReader saxReader;

@Test   // 第三方 bean 的管理
public void testThirdBean() throws Exception {...}
@Configuration
public class CommonConfig {
    @Bean   
    public SAXReader reader(DeptService deptService){
        System.out.println(deptService);
        return new SAXReader();
    }
}

4.3.3 SpringBoot 原理

① 起步依赖

② 自动配置

当 spring 容器启动后,一些配置类、bean 对象就自动存入到了 IOC 容器中,从而简化了开发,省去了繁琐的配置操作

@Autowired
private DataSource dataSource;

@Test
public void testDataSource(){...}

A. 原理

方案一:@ComponentScan 组件扫描

@ComponentScan({"com.example","com.itheima","com.alibaba","com.google","org.springframework",...})
@SpringBootApplication
public class SpringbootWebConfig2Application {}     // => 使用繁琐, 性能低

方案二:@Import 导入。使用 @Import 导入的类会被 Spring 加载到 IOC 容器中,导入形式主要有以下几种:

@Import({TokenParser.class})                        // 1. 导入普通类
@Import({HeaderConfig.class})                       // 2. 导入配置类
@Import({MyImportSelector.class})                   // 3. 导入 ImportSelector 接口实现类
@EnableHeaderConfig                                 // 4. @Enablexxxx 注解,封装 @Import 注解 
@SpringBootApplication
public class SpringbootWebConfig2Application {}     // => 方便, 优雅
@Component                              // a. 普通类
public class TokenParser {}
@Configuration                          // b. 配置类
public class HeaderConfig {             
    @Bean
    public HeaderParser headerParser(){return new HeaderParser();}
    @Bean
    public HeaderGenerator headerGenerator(){return new HeaderGenerator();}
}

public class HeaderParser {}
public class HeaderGenerator {}
                                        // c. ImportSelector 接口实现类
public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.example.HeaderConfig"};
    }
}
@Retention(RetentionPolicy.RUNTIME)     
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)         
public @interface EnableHeaderConfig {} // d. 封装 @Import 注解

B. 源码跟踪

@SpringBootApplication 标识在 SpringBoot 工程引导类上,是 SpringBoot 中最最最重要的注解。该注解由三个部分组成:

C. @Conditional

③ 配置案例

需求:自定义 aliyun-oss-spring-boot-starter,完成阿里云 OSS 操作工具类 AliyunOSSUtils 的自动配置

目标:引入起步依赖引入之后,要想使用阿里云 OSS,注入 AliyunOSSUtils 直接使用即可

  1. 基础工程
<!-- aliyun-oss-spring-boot-starter\pom.xml -->

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- aliyun-oss-spring-boot-autoconfigure\pom.xml -->

<!-- 阿里云 OSS -->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.1</version>
</dependency>
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>
// java.com.aliyun.oss\AliOSSProperties.java

@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
    
    ...(javabean)
}
// java.com.aliyun.oss\AliOSSUtils.java

public class AliOSSUtils {
    private AliOSSProperties aliOSSProperties;
    
    public AliOSSProperties getAliOSSProperties() {
        return aliOSSProperties;
    }
    public void setAliOSSProperties(AliOSSProperties aliOSSProperties) {
        this.aliOSSProperties = aliOSSProperties;
    }
    public String upload(MultipartFile file) throws IOException {...}
}
// java.com.aliyun.oss\AliOSSAutoConfiguration.java

@Configuration
@EnableConfigurationProperties(AliOSSProperties.class)
public class AliOSSAutoConfiguration {
    @Bean
    public AliOSSUtils aliOSSUtils(AliOSSProperties aliOSSProperties){
        AliOSSUtils aliOSSUtils = new AliOSSUtils();
        aliOSSUtils.setAliOSSProperties(aliOSSProperties);
        return aliOSSUtils;
    }
}
// resources.META-INF.spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.aliyun.oss.AliOSSAutoConfiguration
  1. 测试工程
<!-- springboot-autoconfiguration-test\pom.xml -->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-oss-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
# resources\application.yml
aliyun:
  oss:
    endpoint: https://oss-cn-beijing.aliyuncs.com
    accessKeyId: LTAI5tSH18FLKGFhWgsuJsGS
    accessKeySecret: 5wPyUojv5seKI5XqsTbrexcHHIoCwD
    bucketName: web-framework2024-5-8-01
@RestController
public class UploadController {
    @Autowired
    private AliOSSUtils aliOSSUtils;

    @PostMapping("/upload")
    public String upload(MultipartFile image) throws Exception {
        String url = aliOSSUtils.upload(image);
        return url;
    }
}
// postman: post: body: form-data: image(file) => test.jpg
// https://web-framework2024-5-8-01.oss-cn-beijing.aliyuncs.com/dbb28c6d-9dfe-4e87-a6e0-4bc392ebd02f.jpg

第5章 - MySQL & Mybatis

5.1 MySQL

5.1.1 MySQL 概述

① 课程介绍

数据库:DataBase(DB),是存储和管理数据的仓库

数据库管理系统:DataBase Management System (DBMS),操纵和管理数据库的大型软件

SQL:Structured Query Language,操作关系型数据库的编程语言,定义了一套操作关系型数据库统一标准

MySQL 连接:mysql -uroot -p1234

MySQL 企业开发使用方式:mysql -h192.168.150.101 -P3306 -uroot -p1234

② 数据模型

关系型数据库(RDBMS): 建立在关系模型基础上,由多张相互连接的二维表组成的数据库

③ SQL 简介

SQL:一门操作关系型数据库的编程语言,定义操作所有关系型数据库的统一标准

SQL 通用语法

分类 全称 说明
DDL Data Definition Language 数据定义语言,用来定义数据库对象 (数据库,表,字段)
DML Data Manipulation Language 数据操作语言,用来对数据库表中的数据进行增删改
DQL Data Query Language 数据查询语言,用来查询数据库中表的记录
DCL Data Control Language 数据控制语言,用来创建数据库用户、控制数据库的访问权限

5.1.2 数据库设计 - DDL

① 项目开发流程

② 数据库操作

show databases;         -- 查询所有数据库
select database();      -- 查询当前数据库
create database db01;   -- 创建数据库
use  db01;              -- 使用数据库
drop  db01;             -- 删除数据库
# 注: 上述语法中的 database, 也可以替换成 schema. 如:create schema db01

图形化工具:

③ 表结构操作

1. 表结构 - 创建

create table tb_user(
    id int primary key auto_increment comment 'ID, 唯一标识',
    username varchar(20) not null unique comment '用户名',
    name varchar(10) not null comment '姓名',
    age int comment '年龄',
    gender char(1) default '男' comment '性别'
) comment '用户表';
约束 描述 关键字
非空约束 限制该字段值不能为null not null
唯一约束 保证字段的所有数据都是唯一、不重复的 unique
主键约束 主键是一行数据的唯一标识,要求非空且唯一 primary key
默认约束 保存数据时,如果未指定该字段值,则采用默认值 default
外键约束 让两张表的数据建立连接,保证数据的一致性和完整性 foreign key
类型 大小 (byte) 有符号范围 无符号范围 描述
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 (-263,263-1) (0,2^64-1) 极大整数值
float 4 单精度浮点数值
double 8 双精度浮点数值
decimal 小数值 (精度更高)
类型 大小 描述
char 0-255 bytes 定长字符串
varchar 0-65535 bytes 变长字符串
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 极大文本数据
类型 大小 范围 格式 描述
date 3 1000-01-01 至 9999-12-31 YYYY-MM-DD 日期值
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 混合日期和时间值
timestamp 4 1970-01-01 00:00:01 至 2038-01-19 03:14:07 YYYY-MM-DD HH:MM:SS 混合日期和时间值,时间戳

2. 案例

需求:参考资料中提供的页面原型,设计员工管理模块的表结构 (暂不考虑所属部门字段)

注:添加员工时,会给员工设置一个默认的密码 123456,添加完成后,员工就可以通过该密码登录该后台管理系统了

注:create_time 记录的是当前这条数据插入的时间,update_time 记录当前这条数据最后更新的时间

3. 表结构 - 查询、修改、删除

-- 查看: 当前数据库下的表
show tables;
-- 查看: 查看指定表结构
desc tb_emp;
-- 查看: 数据库的建表语句         # 右键 -> Edit Source
show create table tb_emp;       
-- 修改: 为表 tb_emp 添加字段 qq varchar(11)
alter table tb_emp add qq varchar(11) comment 'QQ';
-- 修改: 修改 tb_emp 字段类型 qq varchar(13)                # 右键 -> Modify Table
alter table tb_emp modify qq_num varchar(13) comment 'QQ';
-- 修改: 修改 tb_emp 字段名 qq 为 qq_num varchar(13)        # 右键 -> rename
alter table tb_emp change qq_num qq_num varchar(13) comment 'QQ';
-- 修改: 删除 tb_emp 的 qq_num 字段
alter table tb_emp drop column qq_num;
-- 修改: 将tb_emp 表名修改为 emp
rename table tb_emp to emp;
-- 删除: 删除 tb_emp 表          # 右键 -> Drop
drop table if exists tb_emp;

5.1.3 数据库操作 - DML

① 增加(insert)

-- 1. 为 tb_emp 表的 username, name, gender 字段插入值
insert into tb_emp(username,name,gender,create_time,update_time) values ('wuji','张无忌',1,now(),now());

-- 2. 为 tb_emp 表的 所有字段插入值
insert into tb_emp(id, username, password, name, gender, image, job, entrydate, create_time, update_time)
            values (null,'zhiruo','123','周芷若',2,'1.jpg',1,'2010-01-01',now(),now());
insert into tb_emp values (null,'zhiruo2','123','周芷若',2,'1.jpg',1,'2010-01-01',now(),now());

-- 3. 批量为 为 tb_emp 表的 username , name , gender 字段插入数据
insert into tb_emp(username,name,gender,create_time,update_time) values
              ('weifuwang','韦一笑',1,now(),now()),('xieshiwang','谢逊',1,now(),now());

# 插入数据时,指定的字段顺序需要与值的顺序是一一对应的
# 字符串和日期型数据应该包含在引号中
# 插入的数据大小,应该在字段的规定范围内

② 修改(update)

-- 1. 将 tb_emp 表的ID为1员工 姓名name字段更新为 '张三'
update tb_emp set name = '张三' , update_time = now() where id = 1;
-- 2. 将 tb_emp 表的所有员工的入职日期更新为 '2010-01-01'
update tb_emp set entrydate = '2010-01-01', update_time = now();

# 修改语句的条件可以有,也可以没有,如果没有条件,则会修改整张表的所有数据

③ 删除(delete)

-- 1. 删除 tb_emp 表中 ID为1的员工
delete from tb_emp where id = 1;
-- 2. 删除 tb_emp 表中的所有员工
delete from tb_emp;

# DELETE 语句的条件可以有,也可以没有,如果没有条件,则会删除整张表的所有数据
# DELETE 语句不能删除某一个字段的值(如果要操作,可以使用UPDATE,将该字段的值置为NULL)

5.1.4 数据库操作 - DQL

① 介绍

DQL 英文全称是 Data Query Language (数据查询语言),用来查询数据库表中的记录

② 基本查询

-- 1. 查询指定字段 name,entrydate 并返回
select name,entrydate from emp ;

-- 2. 查询返回所有字段
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from emp;
select * from emp;      # * 号代表查询所有字段, 在实际开发中尽量少用(不直观、影响效率)

-- 3. 查询所有员工的 name,entrydate, 并起别名(姓名、入职日期)
select name as '姓名' ,entrydate as '入职日期' from emp ;
select name '姓名' ,entrydate '入职日期' from emp ;

-- 4. 查询员工有哪几种职位(不要重复) -- distinct
select distinct job from emp;
select * from emp where id = 1;

③ 条件查询

-- 1. 查询 姓名 为 杨逍 的员工
select * from emp where name = '杨逍';
-- 2. 查询在 id小于等于5 的员工信息
select * from emp where id <= 5;
-- 3. 查询 没有分配职位 的员工信息  -- 判断 null , 用 is null
select * from emp where job is null;
-- 4. 查询 有职位 的员工信息  -- 判断 不是null , 用 is not null
select * from emp where job is not null ;
-- 5. 查询 密码不等于 '123456' 的员工信息
select * from emp where password <> '123456';
select * from emp where password != '123456';
-- 6. 查询入职日期 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间的员工信息
select * from emp where entrydate between '2000-01-01' and '2010-01-01' ;
-- 7. 查询 入职时间 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间 且 性别为女 的员工信息
select * from emp where (entrydate between '2000-01-01' and '2010-01-01') and  gender = 2;
-- 8. 查询 职位是 2 (讲师), 3 (学工主管), 4 (教研主管) 的员工信息
select * from emp where job = 2 or job = 3 or job = 4;
select * from emp where job in (2,3,4);
-- 9. 查询姓名为两个字的员工信息
select * from emp where name like '__';
-- 10. 查询姓 '张' 的员工信息
select * from emp where name like '张%';
-- 11. 查询姓名中包含 '三' 的员工信息
select * from emp where name like '%三%';

④ 分组查询

  1. 聚合函数
-- 1. 统计该企业员工数量
-- A. count(字段)
select count(id) from emp;
-- B. count(*) -- 推荐
select count(*) from emp;
-- C. count(值)
select count(1) from emp;
-- 2. 统计该企业员工 ID 的平均值
select avg(id) from emp;
-- 3. 统计该企业最早入职的员工的入职日期
select min(entrydate) from emp;
-- 4. 统计该企业最近入职的员工的入职日期
select max(entrydate) from emp;
-- 5. 统计该企业员工的 ID 之和
select sum(id) from emp;
  1. 分组查询
-- 1. 根据性别分组 , 统计男性和女性员工的数量
select gender,count(*) from emp group by gender;
-- 2. 先查询入职时间在 '2015-01-01' (包含) 以前的员工 , 并对结果根据职位分组 , 获取员工数量大于等于2的职位
select job,count(*)  from emp where entrydate <= '2015-01-01'  group by job having count(*) >= 2;

# 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义
# 执行顺序: where  >  聚合函数 > having

where 与 having 区别:(面试题)

⑤ 排序查询

-- 1. 根据入职时间, 对员工进行升序排序  -- 排序条件
select * from emp order by entrydate ;
-- 2. 根据入职时间, 对员工进行降序排序
select * from emp order by entrydate desc;
-- 3. 根据 入职时间 对公司的员工进行 升序排序 , 入职时间相同 , 再按照 ID 进行降序排序
select * from emp order by entrydate asc , id desc ;

⑥ 分页查询

-- 1. 查询第1页员工数据, 每页展示10条记录
select * from emp limit 0,10;
select * from emp limit 10;
-- 2. 查询第2页员工数据, 每页展示10条记录
select * from emp limit 10,10;

-- 公式 : 起始索引 = (页码 - 1) * 每页记录数

⑦ 案例

# 需求 : 员工管理列表查询 , 根据最后操作时间, 进行倒序排序
# 条件 : name , gender , entrydate

select *
from tb_emp
where name like '%张%'
  and gender = 1
  and entrydate between '2000-01-01' and '2010-01-01'
order by update_time desc
limit 0,10;
# 需求: 男性与女性员工的人数统计 (1 : 男性员工 , 2 : 女性员工)

-- 函数: if(条件表达式 , true , false)
select if(gender = 1, '男性员工', '女性员工') '性别',
       count(*) '人数'
from emp
group by gender;
# 需求: 员工职位信息

-- 函数: case when ... then ... when ... then ... else ... end
select (case
        when job = 1 then '班主任'
        when job = 2 then '讲师'
        when job = 3 then '教研主管'
        when job = 4 then '学工主管'
        else '无职位' end) '职位',
       count(*)
from emp
group by job;

-- 函数: case ... when ... then ... when ... then ... else ... end
select (case job
        when 1 then '班主任'
        when 2 then '讲师'
        when 3 then '教研主管'
        when 4 then '学工主管'
        else '无职位' end) '职位',
       count(*)
from emp
group by job;

5.1.5 多表设计 & 查询

① 多表设计

1. 一对多

一对多关系实现:在数据库表中多的一方,添加字段,来关联一的一方的主键

# 根据页面原型及需求文档,完成部门及员工模块的表结构设计
# 部门名称,必填,唯一,长度为2-10位

-- 部门
create table tb_dept (
    id int unsigned primary key auto_increment comment 'ID',
    name varchar(10) not null unique comment '部门名称',
    create_time datetime not null comment '创建时间',
    update_time datetime not null comment '修改时间'
) comment '部门表';

-- 员工
create table tb_emp (
    dept_id int unsigned comment '归属的部门ID'
    ...
) comment '员工表';

目前上述的两张表,在数据库层面,并未建立关联,所以是无法保证数据的一致性和完整性的

2. 一对一

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

3. 多对多

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

4. 案例

参考资料中提供的页面原型,设计分类管理、菜品管理、套餐管理模块的表结构

1.阅读页面原型及需求文档,分析各个模块涉及到的表结构,及表结构之间的关系

2.根据页面原型及需求文档,分析各个表结构中具体的字段及约束

② 多表查询

1. 概述

select * from tb_emp,tb_dept;
select * from tb_emp,tb_dept where tb_emp.dept_id = tb_dept.id;

2. 内连接

-- A. 查询员工的姓名 , 及所属的部门名称 (隐式内连接实现)
select tb_emp.name,tb_dept.name from tb_emp,tb_dept where tb_emp.dept_id = tb_dept.id;
select e.name, d.name from tb_emp e, tb_dept d where e.dept_id = d.id;          -- 起别名
-- B. 查询员工的姓名 , 及所属的部门名称 (显式内连接实现)
select tb_emp.name,tb_dept.name from tb_emp  join tb_dept on tb_emp.dept_id = tb_dept.id;

3. 外连接

-- A. 查询员工表 所有 员工的姓名, 和对应的部门名称 (左外连接)
select e.name, d.name from tb_emp e left join tb_dept d on e.dept_id = d.id;
select e.name, d.name from tb_dept d right join tb_emp e on e.dept_id = d.id;   -- 左右转换
-- B. 查询部门表 所有 部门的名称, 和对应的员工名称 (右外连接)
select e.name, d.name from tb_emp e right join tb_dept d on e.dept_id = d.id;

4. 子查询

-- A. 标量子查询: 子查询返回的结果为单个值. 常用的操作符: =, <>, >, >=, <, <=    
-- 查询 "教研部" 的所有员工信息
select * from tb_emp where dept_id = (select id from tb_dept where name = '教研部');
-- 查询在 "方东白" 入职之后的员工信息
select * from tb_emp where entrydate > (select entrydate from tb_emp where name = '方东白');

-- B. 列子查询: 子查询返回的结果为一列. 常用的操作符: in, not in
-- 查询 "教研部" 和 "咨询部" 的所有员工信息
select * from tb_emp where dept_id in (select id from tb_dept where name = '教研部' or name = '咨询部');

-- C. 行子查询: 子查询返回的结果为一行. 常用的操作符: =, <>, in, not in
-- 查询与 "韦一笑" 的入职日期 及 职位都相同的员工信息 ;
select * from tb_emp where (entrydate,job)=(select entrydate,job from tb_emp where name = '韦一笑');

-- D. 表子查询: 子查询返回的结果为多行多列. 
-- 查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门名称
select e.*, d.name from (select * from tb_emp where entrydate > '2006-01-01') e, tb_dept d where e.dept_id = d.id;

5. 案例

-- 1. 查询价格低于10元的菜品的名称、价格及其菜品的分类名称
-- 表: dish , category
select d.name, d.price, c.name 
from dish d, category c
where d.category_id = c.id and d.price < 10;

-- 2. 查询所有价格在10元到50元之间且状态为'起售'的菜品, 展示出菜品的名称、价格及其菜品的分类名称
-- 表: dish , category
select d.name, d.price, c.name 
from dish d left join category c on d.category_id = c.id
where d.price between 10 and 50 and d.status = 1;

-- 3. 查询每个分类下最贵的菜品, 展示出分类的名称、最贵的菜品的价格
-- 表: dish , category
select c.name, max(d.price)
from dish d, category c
where d.category_id = c.id group by c.name;

-- 4. 查询各个分类下菜品状态为'起售', 并且该分类下菜品总数量大于等于3的分类名称 .
-- 表: dish, category
select c.name, count(*)
from dish d, category c
where d.category_id = c.id and d.status = 1
group by c.name having count(*) >= 3;

-- 5. 查询出 "商务套餐A" 中包含了哪些菜品 (展示出套餐名称、价格, 包含的菜品名称、价格、份数).
-- 表: setmeal, setmeal_dish, dish
select s.name, s.price, d.name, d.price, sd.copies
from setmeal s, setmeal_dish sd, dish d
where s.id = sd.setmeal_id and sd.dish_id = d.id and s.name = '商务套餐A';

-- 6. 查询出低于菜品平均价格的菜品信息 (展示出菜品名称、菜品价格).
-- 表: dish
select * from dish where price < (select avg(price) from dish);

5.1.6 事务 & 索引

① 事务

  1. 事务介绍

    事务是一组操作的集合,它是一个不可分割的工作单位

    事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败

  2. 事务操作

    # 场景:学工部 整个部门解散了,该部门及部门下的员工都需要删除了
    # 问题:如果删除部门成功了,而删除该部门的员工时失败了,就造成了数据的不一致
    
    -- 开启事务
    start transaction;
    -- 删除部门
    delete from tb_dept where id = 2;
    -- 删除部门下的员工
    delete from tb_emp where dept_id == 2;
    -- 提交事务
    commit;
    -- 回滚事务
    rollback;
    
  3. 事务四大特性(面试题)

    • 原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败

    • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态

    • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行

    • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的

② 索引

  1. 介绍

    索引(index)是帮助数据库 高效获取数据 的 数据结构

select * from tb_sku where sn = '100000003145008';   -- 14s
select count(*) from tb_sku;                     -- 6000000
create index idx_sku_sn on tb_sku(sn);               
select * from tb_sku where sn = '100000003145008';   -- 6ms

优点

缺点(忽略不计)

  1. 语法

    -- 创建 : 为 tb_emp 表的 name 字段建立一个索引 
    create index idx_emp_name on tb_emp(name);
    -- 查询 : 查询 tb_emp 表的索引信息 
    show index from tb_emp;
    -- 删除: 删除 tb_emp 表中 name 字段的索引 
    drop index idx_emp_name on tb_emp;
    
    • 主键字段,在建表时,会自动创建主键索引
    • 添加唯一约束时,数据库实际上会添加唯一索引

5.2 Mybatis

5.2.1 快速入门

① 入门程序

MyBatis 是一款优秀的持久层框架,用于简化 JDBC 的开发

需求:使用 Mybatis 查询所有用户数据

  1. 准备工作(创建 springboot 工程、数据库表 user、实体类 User)
// src\main\java\com.itheima\pojo\User.java

public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;
    ...(javabean)
}
  1. 引入 Mybatis 的相关依赖,配置 Mybatis(数据库连接信息)

    <!--mybatis 的起步依赖-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <!-- mysql 驱动包-->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    # 驱动类名称
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    # 数据库连接的 url
    spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
    # 连接数据库的用户名
    spring.datasource.username=root
    # 连接数据库的密码
    spring.datasource.password=123456
    
  2. 编写SQL语句(注解 / XML)

    // src\main\java\com.itheima\mapper\UserMapper.java
    
    @Mapper
    public interface UserMapper {
        @Select("select * from user")
        public List<User> list();
    }
    
    // src\test\java\com.itheima\Test.java
    
    @SpringBootTest 
    class SpringbootMybatisQuickstartApplicationTests {
        @Autowired
        private UserMapper userMapper;
        @Test
        public void testListUser(){
            List<User> userList = userMapper.list();
            userList.stream().forEach(user -> {
                System.out.println(user);
            });
        }
    }
    

② 配置 SQL 提示

  1. 默认在 mybatis 中编写 SQL 语句是不识别的。可以做如下配置:
  1. 数据库表识别不了

    • 产生原因:Idea 和数据库没有建立连接,不识别表信息

    • 解决方式:在 Idea 中配置 MySQL 数据库连接

③ JDBC 简介

JDBC ( Java DataBase Connectivity ),就是使用 Java 语言操作关系型数据库的一套 API

5.2.2 连接池 & lombok

① 连接池

数据库连接池是一个容器,负责分配、管理数据库连接 (Connection)

② lombok

Lombok 是一个实用的 Java 类库,能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString 等方法(即 javabean),并可以自动化生成日志变量,简化 java 开发、提高效率

注解 作用
@Getter / @Setter 为所有的属性提供 get / set 方法
@ToString 会给类自动生成易阅读的 toString 方法
@EqualsAndHashCode 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法
@Data 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode)
@NoArgsConstructor 为实体类生成无参的构造器方法
@AllArgsConstructor 为实体类生成除了static修饰的字段之外带有各参数的构造器方法
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;
}

5.2.3 基础操作

① 需求

根据资料中提供的页面原型及需求,完成员工管理的需求开发

② 准备

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=123456
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
    private Integer id;         // ID
    private String username;    // 用户名
    private String password;    // 密码
    private String name;        // 姓名
    private Short gender;       // 性别, 1 男, 2 女
    private String image;       // 图像 url
    private Short job;          // 职位, 1 班主任, 2 讲师, 3 学工主管, 4 教研主管, 5 咨询师
    private LocalDate entrydate;        // 入职日期
    private Integer deptId;             // 部门 ID
    private LocalDateTime createTime;   // 创建时间
    private LocalDateTime updateTime;   // 修改时间
}
@Mapper
public interface EmpMapper {
}

③ 删除

@Mapper
public interface EmpMapper {
    @Delete("delete from emp where id = #{id}")
    public void delete(Integer id);
}
@SpringBootTest
class Test {
    @Autowired
    private EmpMapper empMapper;
    @Test
    public void testDelete(){
        empMapper.delete(16);
    }
}

可以在 application.properties 中,打开 mybatis 的日志,并指定输出到控制台

mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

预编译 SQL 优点

性能更高:

更安全(防止 SQL 注入):

select count(*) from emp where user name = '?' and password = '?';                  -- 正常登录
select count(*) from emp where user name = 'abcd' and password = '0' or '1' = '1';  -- 注入成功
select count(*) from emp where user name = ? and password = ?;                      -- 预编译 SQL
select count(*) from emp where user name = abcd and password = 0' or '1' = '1;      -- 注入失败

④ 新增

@Mapper
public interface EmpMapper {
    // 自动将生成的主键值,赋值给 emp 对象的 id 属性
    @Options(useGeneratedKeys = true, keyProperty = "id")
    @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username},#{name},#{gender},#{image},#{job},#{entrydate},#{deptId},#{createTime},#{updateTime})")
    public void insert(Emp emp);
}
@SpringBootTest
class Test {
    @Autowired
    private EmpMapper empMapper;
    @Test
    public void testInsert(){
        Emp emp = new Emp();
        emp.setUsername("Tom3");
        ...
        empMapper.insert(emp);
        System.out.println(emp.getId());
    }
}

⑤ 更新

@Mapper
public interface EmpMapper {
    @Update("update emp set username = #{username}, name = #{name}, gender = #{gender}, image = #{image}, job = #{job}, entrydate = #{entrydate}, dept_id = #{deptId},update_time = #{updateTime} where id = #{id}")
    public void update(Emp emp);
}
@SpringBootTest
class Test {
    @Autowired
    private EmpMapper empMapper;
    @Test
    public void testInsert(){
        Emp emp = new Emp();
        emp.setUsername("Tom3");
        ...
        empMapper.insert(emp);
        System.out.println(emp.getId());
    }
}

⑥ 查询

@Mapper
public interface EmpMapper {
    @Select("select * from emp where id = #{id}")
    public Emp getById(Integer id);
}
@SpringBootTest
class Test {
    @Autowired
    private EmpMapper empMapper;
    @Test
    public void testGetById(){
        Emp emp = empMapper.getById(20);
        System.out.println(emp);
    }
}

如果实体类属性名 和 数据库表查询返回的字段名不一致,不能自动封装

开启驼峰命名:如果字段名与属性名符合驼峰命名规则,mybatis 会自动通过驼峰命名规则映射

mybatis.configuration.map-underscore-to-camel-case=true

5.2.4 XML 配置文件

使用 Mybatis 的注解,主要是来完成一些简单的增删改查功能。如果需要实现复杂的 SQL 功能,建议使用 XML 来配置映射语句

// java\com\itheima\mapper\EmpMapper.java
@Mapper
public interface EmpMapper {
    public List<Emp> list(String name, Short gender, LocalDate begin , LocalDate end);
}
<!-- 1. XML 映射文件名称与 Mapper 接口一致,并且放置在相同包下 -->
<!-- resources\com\itheima\mapper\EmpMapper.xml -->

<!-- 2. XML 映射文件的 namespace 属性为 Mapper 接口全限定名一致 -->
<mapper namespace="com.itheima.mapper.EmpMapper">
    <!-- 3. XML 映射文件中 sql 语句的 id 与 Mapper 接口中的方法名一致, 且返回类型一致 -->
    <select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp where name like concat('%',#{name},'%') and gender = #{gender} and entrydate between #{begin} and #{end} order by update_time desc
    </select>
</mapper>

5.2.5 动态 SQL

① 介绍

随着用户的输入或外部条件的变化而变化的 SQL 语句,我们称为动态 SQL

② 动态 SQL - if

@Mapper
public interface EmpMapper {
    public List<Emp> list(String name, Short gender, LocalDate begin , LocalDate end);
}
<mapper namespace="com.itheima.mapper.EmpMapper">
    <select id="list" resultType="com.itheima.pojo.Emp">
        select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time, from emp
        <where>
            <if test="name != null">
                name like concat('%', #{name}, '%')
            </if>
            <if test="gender != null">
                and gender = #{gender}
            </if>
            <if test="begin != null and end != null">
                and entrydate between #{begin} and #{end}
            </if>
        </where>
        order by update_time desc
    </select>
</mapper>
@Mapper
public interface EmpMapper {
    public void update(Emp emp);
}
<mapper namespace="com.itheima.mapper.EmpMapper">
    <update id="update">
        update emp
        <set>
            <if test="username != null">username = #{username},</if>
            <if test="name != null">name = #{name},</if>
            <if test="gender != null">gender = #{gender},</if>
            <if test="image != null">image = #{image},</if>
            <if test="job != null">job = #{job},</if>
            <if test="entrydate != null">entrydate = #{entrydate},</if>
            <if test="deptId != null">dept_id = #{deptId},</if>
            <if test="updateTime != null">update_time = #{updateTime}</if>
        </set>
        where id = #{id}
    </update>
</mapper>

③ 动态 SQL - foreach

@Mapper
public interface EmpMapper {
    public void deleteByIds(List<Integer> ids);
}
<mapper namespace="com.itheima.mapper.EmpMapper">
    <delete id="deleteByIds">
        delete  from emp where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>
</mapper>

④ 动态 SQL - sql & include

<mapper namespace="com.itheima.mapper.EmpMapper">
    <sql id="commonSelect">
        select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp
    </sql>

    <select id="list" resultType="com.itheima.pojo.Emp">
        <include refid="commonSelect"/> <where> ... </where> order by update_time desc
    </select>
</mapper>

5.3 课程总结

参考链接:https://www.bilibili.com/video/BV1m84y1w7Tb?p=1&vd_source=ed621eaa6bcf9bf6acb7d0527c30489a

上一篇下一篇

猜你喜欢

热点阅读