第7章 后端Web实战(Tlias系统)

一、准备工作

1、开发规范-开发模式

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

开发流程

2、开发规范-Restful风格

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

(1)传统的接口风格

不同开发者使用不同的请求路径

/user/addUser /user/add /user/insertUser /user/insert /user/saveUser /user/save

(2)Restful风格

url固定,请求方式按规定给出

(3)注意

1.REST是风格,是约定方式,约定不是规定,可以打破。

2.描述功能模块通常使用复数形式(加s),表示此类资源,而非单个资源。如:users、books..

(4)小结

1.REST风格的特点?

URL定义资源

HTTP动词描述操作

2.REST风格中的四种请求方式及对应的操作?

GET:查询

POST:新增

PUT:修改

DELETE:删除

3、开发规范(Apifox)

(1)问题

前后端都在并行开发,

后端 开发完对应的接口之后,如何对接口进行请求测试呢?

前端 开发过程中,如何获取到数据,测试页面的渲染展示呢?

(2)Apifox

介绍:Apifox是一款集成了Api文档、Api调试、Api Mock、Api测试的一体化协作平台。

作用:接口文档管理、接口请求测试、Mock服务。

官网:Apifox - API 文档、调试、Mock、测试一体化协作平台。拥有接口文档管理、接口调试、Mock、自动化测试等功能,接口开发、测试、联调效率,提升 10 倍。最好用的接口文档管理工具,接口自动化测试工具。

(3)为什么要使用Apifox

由于浏览器地址栏发起的请求,都是GET方式的请求,如果我们需要发起POST、PUT、DELETE方式的请求,就需要借助于这类工具

4、工程搭建

(1)搭建总过程

1.创建SpringBoot工程,并引入web开发起步依赖、mybatis、mysql驱动、lombok。

2.创建数据库表dept,并在application.yml中配置数据库的基本信息。

3.准备基础代码结构,并引入实体类Dept及统一的响应结果封装类Result。

(2)环境搭建

创建空项目,命名为web-ai-project02

检查JDK版本

检查Maven路径

编码字符集

(3)创建SpringBoot项目

创建并引入四项起步依赖

删掉没用的文件

(4)创建数据库表

sql 复制代码
CREATE TABLE dept (
    id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT 'ID,主键',
    name varchar(10) NOT NULL UNIQUE COMMENT '部门名称',
    create_time datetime DEFAULT NULL COMMENT '创建时间',
    update_time datetime DEFAULT NULL COMMENT '修改时间'
) COMMENT '部门表';

INSERT INTO dept VALUES 
(1,'学工部','2024-09-25 09:47:40','2024-09-25 09:47:40'),
(2,'教研部','2024-09-25 09:47:40','2024-09-09 15:17:04'),
(3,'咨询部','2024-09-25 09:47:40','2024-09-30 21:26:24'),
(4,'就业部','2024-09-25 09:47:40','2024-09-25 09:47:40'),
(5,'人事部','2024-09-25 09:47:40','2024-09-25 09:47:40'),
(6,'行政部','2024-11-30 20:56:37','2024-09-30 20:56:37');

(4)配置yml配置信息

TypeScript 复制代码
# 配置数据库连接信息
spring:
  application:
    name: tlias-web-management

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

# Mybatis的相关配置
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

(5)创建各包类

包类结构如下:

  • pojo包下

    • 实体类Dept

      java 复制代码
      package com.itheima.pojo;
      
      import lombok.AllArgsConstructor;
      import lombok.Data;
      import lombok.NoArgsConstructor;
      
      import java.time.LocalDateTime;
      
      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class Dept {
          private Integer id;
          private String name;
          private LocalDateTime createTime;
          private LocalDateTime updateTime;
      }
    • 响应结果封装类Result

      java 复制代码
      package com.itheima.pojo;
      
      import lombok.Data;
      
      import java.io.Serializable;
      
      /**
       * 后端统一返回结果
       */
      @Data
      public class Result {
      
          private Integer code; //编码:1成功,0为失败
          private String msg; //错误信息
          private Object data; //数据
      
          public static Result success() {
              Result result = new Result();
              result.code = 1;
              result.msg = "success";
              return result;
          }
      
          public static Result success(Object object) {
              Result result = new Result();
              result.data = object;
              result.code = 1;
              result.msg = "success";
              return result;
          }
      
          public static Result error(String msg) {
              Result result = new Result();
              result.msg = msg;
              result.code = 0;
              return result;
          }
      
      }
  • 业务层service

    • 业务层接口DeptService

      java 复制代码
      package com.itheima.service;
      
      public interface DeptService {
      }
    • 接口实现类包impl

      • 接口实现类DeptServiceImpl

        java 复制代码
        package com.itheima.service.impl;
        
        import com.itheima.service.DeptService;
        import org.springframework.stereotype.Service;
        
        @Service
        public class DeptServiceImpl implements DeptService {
        }
  • 数据访问层mapper

    • 数据访问接口DeptMapper

      java 复制代码
      package com.itheima.mapper;
      
      import org.apache.ibatis.annotations.Mapper;
      
      @Mapper
      public interface DeptMapper {
      }
  • 控制层controller

    • DeptController文件

      java 复制代码
      package com.itheima.controller;
      
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      public class DeptController {
      }

各层都需要引入不同的注解,总结如下:

java 复制代码
//控制层交给IOC容器管理
//该注解包括两个注解
//@Controller将控制层交给IOC容器管理
//@ResponseBody将方法的返回值直接作为相应数据响应给前端,返回值是对象或集合会先转json
@RestController

//数据访问层交给IOC容器管理
@Mapper

//实体类中的注解代替了get、set方法;空参、有参构造
@Data
@NoArgsConstructor
@AllArgsConstructor

//业务层交给IOC容器管理
@Service

二、部门管理

1、列表查询-接口开发

(1)三层架构的每一层的职责

(2)功能实现

(3)具体功能实现代码

控制层:

java 复制代码
@RestController
public class DeptController {

    @Autowired
    private DeptService deptService;

//    @RequestMapping(value = "/depts", method = RequestMethod.GET)
    //带请求方式的请求路径可以简化成以下方式
    @GetMapping("/depts")
    public Result list() {
        System.out.println("查询全部部门数据");
        List<Dept> deptList = deptService.findAll();
        return Result.success(deptList);
    }
}

业务层:

java 复制代码
//接口
public interface DeptService {
    /**
     * 查询所有的部门数据
     */
    List<Dept> findAll();
}
java 复制代码
//实现类
@Service
public class DeptServiceImpl implements DeptService {

    @Autowired
    private DeptMapper deptMapper;

    @Override
    public List<Dept> findAll() {
        return deptMapper.findAll();
    }

}

数据访问层:

java 复制代码
@Mapper
public interface DeptMapper {
    /**
     * 查询所有的部门数据
     * @return
     */
    @Select("select id,name,create_time,update_time from dept order by update_time desc")
    List<Dept> findAll();

}

(4)使用Apifox的快捷请求

使用GET请求获取JSON

2、列表查询-结果封装

(1)数据封装

实体类属性名数据库表查询返回的字段名一致,mybatis会自动封装。

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

(2)存在的问题

实体类属性名和数据库表查询返回的字段名不一致

(3)解决方案

  • 手动结果映射

    java 复制代码
        //方式一:使用@Results和@Result注解
        @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")
        List<Dept> findAll();
  • 起别名

    java 复制代码
    @Select("select id,name,create_time createTime,update_time updateTime from dept order by update_time desc")
    List<Dept> findAll();
  • 开启驼峰命名(推荐)

    只需要在配置文件中输入camel回车即可,不需要修改数据访问层

    但必须要遵守驼峰命名的规则(要求:xxx_abc-> xxxAbc)

    TypeScript 复制代码
    # Mybatis的相关配置
    mybatis:
      configuration:
        # 开启驼峰命名转换
        map-underscore-to-camel-case: true

3、列表查询-前后端联调测试

将资料中提供的前端工程文件夹中的压缩包,拷贝到一个没有中文不带空格的目录下,解压。

启动nginx ,访问测试:http://localhost:90

在浏览器开发者工具F12中,我们可以看到

(1)使用Nginx的反向代理

前端工程请求服务器的地址为http://localhost:90/是如何访问到后端的tomcat服务器的?

反向代理是一种网络架构,通过代理服务器为后端的服务器做代理 ,客户端的请求直接请求代理服务器 ,然后转发给后端的服务器(安全、灵活、负载均衡)

(2)具体代理方式

在Nginx服务器的conf文件中有这段代码

1.location:用于定义匹配路径匹配的规则

2.^~/api/:表示精确匹配 ,即只匹配以/api/开头的路径。

3.rewrite:该指令用于重写匹配到的路径。

4.proxy_pass:该指令用于代理转发 ,它将匹配到的请求转发给位于后端的指令服务器。

Nginx中反向代理的配置

4、删除部门-接口开发(参数接收)

如何接收前端传递过来的简单参数

(1)方式一:使用HttpServletRequest获取参数

java 复制代码
@DeleteMapping("/depts")
public Result delete(HttpServletRequest request) {
    String idStr = request.getParameter("id");
    Integer id = Integer.parseInt(idStr);
    System.out.println("删除部门:" + id);
    return Result.success();
}

(2)方式二:使用@RequestParam获取参数

注意事项:一旦声明了@RequestParam,该参数在请求时必须传递,如果不传递将会报错,(默认required为true)

java 复制代码
@DeleteMapping("/depts")
public Result delete(@RequestParam(value = "id", required = false) Integer deptId){
    System.out.println("删除部门:" + deptId);
    return Result.success();
}

(3)方式三:直接定义方法形参(省略@RequestParam)(推荐)

如果请求参数名形参变量名相同

java 复制代码
@DeleteMapping("/depts")
public Result delete(Integer id){
    System.out.println("删除部门:" + id);
    return Result.success();
}

5、删除部门-接口开发(功能实现)

业务层接口

java 复制代码
void deleteById(Integer id);

业务层实现类

java 复制代码
@Override
public void deleteById(Integer id) {
    deptMapper.deleteById(id);
}

数据访问层

java 复制代码
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);

6、新增部门-接口开发

控制层

java 复制代码
/**
 * 新增部门
 */
@PostMapping("/depts")
public Result add(@RequestBody Dept dept){//通过实体对象接收JSON传递过来的参数
    System.out.println("新增部门:" + dept);
    deptService.add(dept);
    return Result.success();
}

业务层接口

java 复制代码
/**
 * 添加部门数据
 */
void add(Dept dept);

业务层实现类

实现接口并补全基础属性

java 复制代码
@Override
public void add(Dept dept) {
    //补全基础属性createTime,updateTime
    dept.setCreateTime(LocalDateTime.now());
    dept.setUpdateTime(LocalDateTime.now());
    deptMapper.insert(dept);
}

数据访问层

java 复制代码
@Insert("insert into dept(name,create_time,update_time) values(#{name},#{createTime},#{updateTime})")
void insert(Dept dept);

1.如何接收JSON格式的请求参数?

通常通过实体对象接收,保证json格式的键名与对象属性名保持一致,并添加@RequestBody注解

2.json格式的请求参数适用场景?

主要在POST、PUT请求 中,在请求体传递请求参数

7、修改部门-查询回显

控制层

若请求参数名与形参变量名相同,则可以省略

java 复制代码
@PathVariable("id")-->@PathVariable
java 复制代码
/**
 * 根据id查询部门
 */
@GetMapping("/depts/{id}")
public Result getInfo(@PathVariable("id") Integer deptId){
    System.out.println("根据id查询部门:" + deptId);
    Dept dept = deptService.getById(id);
    return Result.success(dept);
}

业务层

java 复制代码
/**
 * 根据id查询部门数据
 */
Dept getById(Integer deptId);
java 复制代码
@Override
public Dept getById(Integer deptId) {
    return deptMapper.getById(deptId);
}

数据访问层

java 复制代码
/**
 * 根据id查询部门数据
 */
@Select("select id,name,create_time createTime,update_time updateTime from dept where id = #{id}")
Dept getById(Integer deptId);

如何接收路径参数?

@PathVariable注解来声明获取的是路径参数

在url中是否可以携带多个路径参数呢,如:/depts/1/0??

可以,接收方式如下

java 复制代码
@GetMapping("/depts/{id}/{sta}")
public Result getInfo(@PathVariable Integer id, @Pathvariable Integer sta){
	//...
}

8、修改部门-修改数据

控制层

java 复制代码
/**
     * 修改部门
     */
    @PutMapping
    public Result update(@RequestBody Dept dept){//该注解用于将请求体中的json数据自动转换为java对象
//        System.out.println("修改部门:" + dept);
        log.info("修改部门:{}", dept);
        deptService.update(dept);
        return Result.success();
    }

业务层

java 复制代码
/**
 * 修改部门数据
 */
void update(Dept dept);
java 复制代码
@Override
public void update(Dept dept) {
    //补全基础属性updateTime
    dept.setUpdateTime(LocalDateTime.now());
    //调用Mapper层的update方法
    deptMapper.update(dept);
}

数据访问层

java 复制代码
/**
 * 修改部门数据
 */
@Update("update dept set name = #{name},update_time = #{updateTime} where id = #{id}")
void update(Dept dept);

9、修改部门-公共路径抽取

抽取前

抽取后

@RequestMapping注解可以加在哪儿?

  • 类上(可选的)

  • 方法上

  • 一个完整的请求路径=类上+方法上的

三、日志技术

好比生活中的日记,可以记录你生活中的点点滴滴。

程序中的日志,是用来记录应用程序的运行信息、状态信息、错误信息等。

日志的功能:数据追踪、性能优化、问题排查、系统监控

常用的日志技术:

JUL:这是JavaSE平台提供的官方日志框架,也被称为JUL。配置相对简单,但不够灵活,性能较差。

Log4j:一个流行的日志框架,提供了灵活的配置选项,支持多种输出目标。

Logback:基于Log4j升级而来,提供了更多的功能和配置选项,性能优于Log4j。

slf4j(Simple Logging Facade for Java):简单日志门面,提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架。

1、Logback入门程序

(1)准备工作

引入logback的依赖(springboot项目中该依赖已传递)、配置文件logback.xml。

依赖已引入

resources文件夹下创建logback.xml文件

其中,将<root level="debug">改为<root level="off">即可关闭日志记录

该日志配置文件为简易配置文件,详细配置文件在下一节

XML 复制代码
<?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{50}: 最长50个字符(超出.切割)  %msg:日志消息,%n是换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 日志输出级别 -->
    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

(2)记录日志

定义日志记录对象Logger,记录日志。

在test文件夹下创建LogTest.java文件

java 复制代码
package com.itheima;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDateTime;

public class LogTest {

    private static final Logger log = LoggerFactory.getLogger(LogTest.class);
    @Test
    public void testLog(){
        //System.out.println(LocalDateTime.now() + " : 开始计算...");
        log.debug("开始计算...");

        int sum = 0;
        int[] nums = {1, 5, 3, 2, 1, 4, 5, 4, 6, 7, 4, 34, 2, 23};
        for (int num : nums) {
            sum += num;
        }
        
        //System.out.println("计算结果为: "+sum);
        log.info("计算结果为: ", sum);
        //System.out.println(LocalDateTime.now() + "结束计算...");
        log.debug("结束计算...");
    }

}

用private static final Logger log = LoggerFactory.getLogger(LogTest.class);来创建日志记录器;

用log.debug("开始计算...");、log.info("计算结果为: ", sum);来记录日志

2、Logback配置文件

配置文件名:logback.xml

该配置文件是对Logback日志框架输出的日志进行控制的,可以来配置输出的格式、位置及日志开关等。

常用的两种输出日志的位置:控制台、系统文件

完整的日志配置文件

XML 复制代码
<?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显示日志记录器的名称,50代表名称长度, %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="ALL">
       <appender-ref ref="STDOUT" />
       <appender-ref ref="FILE" />
    </root>
</configuration>

在"日志输出级别"块中修改要输出哪些日志(控制台或文件)

日志输出效果

3、Logback日志级别

日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下(级别由低到高):

可以在配置文件中,灵活的控制输出那些类型的日志。

XML 复制代码
<!-- 日志输出级别 -->
<!--比info级别高的日志可以输出-->
<root level="info">
   <appender-ref ref="STDOUT" />
   <appender-ref ref="FILE" />
</root>

回归项目,在类上加上该行代码,引入日志记录器

java 复制代码
private static final Logger log = LoggerFactory.getLogger(LogTest.class);

或直接在类上加上@Slf4j注解

四、多表关系

1、一对多

在多的一方加一个字段,关联一的一方的主键即可

2、外键约束

(1)问题与解决方案

现象:部门数据可以直接删除,然而还有部分员工归属于该部门下,此时就出现了数据的不完整、不一致问题。

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

解决方案:外键约束。

数据库中的外键约束的作用?

多表操作中保证数据的一致性、完整性和正确性

(2)SQL语法

sql 复制代码
-- 创建表时指定
create table 表名(
	字段名 数据类型,
    ...
	[constraint] [外键名称] foreign key (外键字段名) references 主表 (字段名)
);

-- 建表示例,并添加唯一约束
    user_id int unsigned not null unique comment '用户ID',
    constraint fk_user_id foreign key (user_id) references tb_user(id)
sql 复制代码
-- 建完表后,添加外键
alter table 表名 add constraint 外键名称 foreign key (外键字段名) references 主表 (字段名);

alter table emp add constraint fk_emp_dept_id foreign key (dept_id) references dept(id);

(3)图形化界面添加外键约束

看到表字段前有一个小钥匙标识,代表外键添加成功

在这里可以看到外键关联情况的图标

(4)企业需求

  • 物理外键

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

    • 缺点:

      • 影响增、删、改的效率(需要检查外键关系)。

      • 仅用于单节点数据库,不适用与分布式、集群场景。

      • 容易引发数据库的死锁问题,消耗性能。

  • 逻辑外键

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

    • 通过逻辑外键,就可以很方便的解决上述问题。

企业开发中不用物理外键,而是在业务逻辑的代码中实现外键约束

3、一对一

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

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

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

sql 复制代码
-- 建表时创建唯一约束和外键
user_id int unsigned not null unique comment '用户ID',
constraint fk_user_id foreign key (user_id) references tb_user(id)

4、多对多

案例:学生与课程的关系

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

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

sql 复制代码
-- 在学生-课程关系表中
constraint fk_courseid foreign key (course_id) references tb_course (id),
constraint fk_studentid foreign key (student_id) references tb_student (id)

5、案例

在新增员工的表单 中

可以发现,员工和工作经历是一对多的关系

创建员工工作经历表

sql 复制代码
create table emp_expr(
    id int unsigned primary key auto_increment comment 'ID,主键',
    begin date comment '开始时间',
    end date comment '结束时间',
    company varchar(50) comment '公司名称',
    job varchar(50) comment '职位',
    emp_id int unsigned comment '关联的员工ID'
)comment '工作经历表';

五、多表查询

1、概述

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

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

  • 连接查询

    • 内连接:相当于查询A、B交集部分数据

    • 外连接

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

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

  • 子查询

2、内连接

内连接查询的是两张表交集部分的数据。具体语法为:

sql 复制代码
-- 1.隐式内连接(常见)
select 字段列表 from 表1,表2 where 连接条件...;
-- 2.显式内连接
select 字段列表 from 表1 [inner] join 表2 on 连接条件...;

-- 1.给表起别名,来简化书写
select 字段列表 from 表1 [as] 别名1,表2 [as] 别名2 where 条件...;

语法中的\[\]表示可以省略的语法

注意: 一旦为表指定了别名,就要通过别名来指定字段名,而不能再使用表名了。

sql 复制代码
-- ============================= 内连接 ==========================
-- A. 查询所有员工的ID, 姓名 , 及所属的部门名称 (隐式、显式内连接实现)
-- 隐式
select emp.id,emp.name,dept.name from emp,dept where emp.dept_id = dept.id;
-- 显式
select emp.id,emp.name,dept.name from emp inner join dept on emp.dept_id = dept.id;
select emp.id,emp.name,dept.name from emp join dept on emp.dept_id = dept.id;


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


-- 起别名
select e.id,e.name,d.name from emp as e inner join dept as d on e.dept_id = d.id where e.gender = 1 and e.salary > 8000;

3、外连接

sql 复制代码
-- 1.左外连接(常见)
select 字段列表 from 表1 left [outer] join 表2 on 连接条件...;

-- 2.右外连接
select 字段列表 from 表1 right [outer] join 表2 on 连接条件 ...;

提示: 对于外连接,常用的是左外连接,因为右外连接的SQL也可以改造成为左外连接(两张表调换个顺序)。

左外连接就是保留左表的全部记录,右表无匹配数据时对应字段填NULL;

实现左外连接就把左表 写在left join左边

实现右外连接就把右表 写在right join左边

sql 复制代码
-- =============================== 外连接 ============================
-- A. 查询员工表 所有 员工的姓名, 和对应的部门名称 (左外连接)
select e.name,d.name from emp e left join dept d on d.id = e.dept_id;

-- B. 查询部门表 所有 部门的名称, 和对应的员工名称 (右外连接)
select d.name,e.name from emp e right outer join dept d on e.dept_id = d.id;
select d.name,e.name from emp e right join dept d on e.dept_id = d.id;

-- C. 查询工资 高于8000 的 所有员工的姓名, 和对应的部门名称 (左外连接)
select e.name, d.name from emp e left join dept d on d.id = e.dept_id where salary > 8000;

4、子查询

介绍:SQL语句中嵌套select语句,称为嵌套查询,又称子查询。

形式:select * from t1 where column1 = (select column1 from t2..);

说明:子查询外部的语句可以是insert/update/delete/select的任何一个,最常见的是select。

分类:

1.标量子查询:子查询返回的结果为单个值

2.列子查询:子查询返回的结果为一列

3.行子查询:子查询返回的结果为一行

4.表子查询:子查询返回的结果为多行多列

提示:子查询的要点是,先对需求做拆分 ,明确具体的步骤 ,然后再逐步编写SQL语句。

sql 复制代码
-- ========================= 子查询 ================================
-- 标量子查询
-- A. 查询 最早入职 的员工信息
-- a.获取到最早入职时间
    select min(emp.entry_date) from emp;
-- b.查询 最早入职 的员工信息
    select * from  emp where entry_date = (select min(emp.entry_date) from emp);

-- B. 查询在 "阮小五" 入职之后入职的员工信息
-- a.查询"阮小五"的入职时间
    select emp.entry_date from emp where name = '阮小五';
-- b.查询在 该时间之后 入职的员工信息
    select * from emp where entry_date > (select emp.entry_date from emp where name = '阮小五');

-- 列子查询
-- A. 查询 "教研部" 和 "咨询部" 的所有员工信息
-- a.查询"教研部"和"咨询部"的部门ID
select id from dept where name = '教研部' or name = '咨询部';
-- b.查询指定部门ID的员工信息
select  * from emp where dept_id in (select id from dept where name = '教研部' or name = '咨询部');

-- 行子查询
-- A. 查询与 "李忠" 的薪资 及 职位都相同的员工信息 ;
-- a.查询"李忠" 的薪资及职位
    select emp.salary,emp.job from emp where name = '李忠';
-- b.查询指定薪资和职位的员工信息
    select * from emp where salary = (select emp.salary from emp where name = '李忠') and job = (select emp.job from emp where name = '李忠');
-- 优化
    select * from emp where (salary,job) = (select emp.salary,emp.job from emp where name = '李忠');

-- 表子查询
-- A. 获取每个部门中薪资最高的员工信息
-- 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;

5、案例

sql 复制代码
-- 需求:
-- 1.查询 "教研部" 性别为 男,且在 "2011-05-01" 之后入职的员工信息。
-- 表: dept , emp
select e.* from emp e , dept d where e.dept_id = d.id and d.name = '教研部' and e.gender = 1 and e.entry_date > '2011-05-01';


-- 2.查询工资 低于公司平均工资的 且 性别为男 的员工信息。
-- 表: emp
-- 2.1 查询 公司平均工资
select avg(salary) from emp;

-- 2.2 查询工资 低于公司平均工资的 且 性别为男 的员工信息。
select * from emp where salary < 7548.2759 and gender = 1;

select * from emp where salary < (select avg(salary) from emp) and gender = 1;

-- 3.查询部门人数超过 10 人的部门名称。
-- 表: dept, emp
select d.name, count(*) from emp e, dept d where e.dept_id = d.id group by d.name having count(*) > 10;


-- 4.查询在 "2010-05-01" 后入职,且薪资高于 10000 的 "教研部" 员工信息,并根据薪资倒序排序。
-- 表: dept, emp
select e.* from emp e , dept d where e.dept_id = d.id and e.entry_date > '2010-05-01'
        and e.salary > 10000 and d.name = '教研部' order by e.salary desc;


-- 5.查询工资 低于本部门平均工资 的员工信息。
-- 5.1 查询每个部门的平均工资
select dept_id, avg(salary) avg_sal from emp group by dept_id;

-- 5.2 查询工资 低于本部门平均工资 的员工信息。
select e.* from emp e , (select dept_id, avg(salary) avg_sal from emp group by dept_id) as a
    where e.dept_id = a.dept_id and e.salary < a.avg_sal;

六、员工管理

1、准备工作

准备员工表和员工经历表,准备pojo实体类

其中数据库表使用下划线命名,实体类使用驼峰命名

准备数据访问层接口EmpMapper、EmpExprMapper

加上@Mapper注解

准备业务层接口和实现类EmpService、EmpServiceImpl

实现类加上@Service注解并继承接口

java 复制代码
@Autowired
private EmpMapper empMapper;

准备控制层EmpController

加上@Slf4j、@RestController注解

java 复制代码
@Autowired
private EmpService empService;
sql 复制代码
-- 查询所有员工信息,体积该员工归属的部门名称
select e.*,d.name from emp e left join dept d on e.id = d.id

2、分页查询-原理分析

sql 复制代码
-- 分页查询
-- 查询第1页,每页展示5条
select e.*,d.name from emp e left join dept d on e.id = d.id limit 0,5;

-- 总数据量
select count(*) from emp left join dept d on e.dept_id =d.id;

前后端需要传递的数据

后端创建实体类PageResult

java 复制代码
@Data
public class PageResult {
    private Long total;//总记录数
    private List<T> rows;//当前页数据
}

3、分页查询-代码实现

数据访问层

java 复制代码
@Select("select count(*) from emp e left join dept d on e.dept_id =d.id")
public Long count();

@Select("select e.*,d.name deptName from emp e left join dept d on e.id = d.id "+
        "order by e.update_time desc limit #{start},#{pageSize}")
public List<Emp> list(Integer start,Integer pageSize);

控制层

@RequestParam注解可设默认值

java 复制代码
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
                   @RequestParam(defaultValue = "10") Integer pageSize){
    log.info("分页查询:{},{}",page,pageSize);
    PageResult<Emp> pageResult = empService.page(page,pageSize);
    return Result.success(pageResult);
}

业务层实现类

java 复制代码
@Override
public PageResult page(Integer page, Integer pageSize) {
    //1、调用mapper接口,查询总记录数
    Long total = empMapper.count();
    //2、调用mapper接口,查询结果列表
    Integer start = (page - 1)*pageSize;
    List<Emp> rows = empMapper.list(start, pageSize);
    //3、封装结果 PageResult
    return new PageResult<Emp>(total,rows);
}

4、分页查询-PageHelper分页插件

使用步骤:

1.引入PageHelper的依赖

XML 复制代码
<!--分页插件PageHelper-->
<dependency>
       <groupId>com.github.pagehelper</groupId>
       <artifactId>pagehelper-spring-boot-starter</artifactId>
       <version>1.4.7</version>
</dependency>

2.定义Mapper接口的查询方法(无需考虑分页)

去掉了limit #{start},#{pageSize}"

java 复制代码
@Select("select e.*,d.name deptName from emp e left join dept d on e.id = d.id "+
        "order by e.update_time desc")
public List<Emp> list();

3.在Service方法中实现分页查询

java 复制代码
@Override
public PageResult page(Integer page, Integer pageSize) {
    //1. 设置分页参数
    PageHelper.startPage(page, pageSize);

    //2. 执行查询
    List<Emp> empList = empMapper.list();//调用数据访问层查询
    Page<Emp> p = (Page<Emp>) empList;//list类型强转为page类型

    //3. 封装分页结果
    return new PageResult<>(p.getTotal(), p.getResult());
}

5、分页查询-PageHelper实现机制

1.PageHelper实现机制

sql 复制代码
select from emp e ...
-- 对sql语句做了以下改造

a.select count(o) from emp e ...
-- 和
b. select... from emp e...limit ?,?

2.注意事项

a.SQL语句结尾不要加分号(;)

b.PageHelper只会对紧跟在其后的第一条SQL语句进行分页处理

6、条件分页查询-基本实现

实现条件查询的sql语句样例

sql 复制代码
select e.*,d.name deptName from emp e left join dept d on e.id = d.id
                           where e.name like '%阮%' and e.gender = 1 and entry_date between '2010-01-01' and '2020-01-01'
                           order by e.update_time desc

控制层

@DateTimeFormat用于指定传入的日期时间格式

java 复制代码
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
                   @RequestParam(defaultValue = "10") Integer pageSize,
                   String name, Integer gender,
                   @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
                   @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
    log.info("分页查询:,{},{},{},{},{},{}",page, pageSize, name, gender, begin, end);
    PageResult<Emp> pageResult = empService.page(page, pageSize, name, gender, begin, end);
    return Result.success(pageResult);
}

业务层

java 复制代码
//加上控制层传递下来的形参
public PageResult<Emp> page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end)

    //调用数据访问层查询加上向下传递的实参
    List<Emp> empList = empMapper.list(name, gender, begin, end);

数据访问层

可以用Mapper的映射文件写较为复杂的SQL语句

resources下创建目录com/itheima/mapper(创建目录用/分隔)

目录下创建EmpMapper.xml

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

    <select id="list" resultType="com.itheima.pojo.Emp">
        select e.*,d.name deptName from emp e left join dept d on e.id = d.id
        where e.name like concat('%',#{name},'%') and e.gender = #{gender} and entry_date between #{begin} and #{end}
        order by e.update_time desc
    </select>
</mapper>

由于#{}不能出现在''之间,编译器不可解析,所以

'%#{}%'占位符应替换为sql语句的一个函数concat('%',#{name},'%')

这样就可以拼接为占位符了

7、条件分页查询-程序优化一(参数接收)

实际开发中,参数过多不利于项目维护

可以将参数封装到一个实体类中

java 复制代码
@Data
public class EmpQueryParam {
    private Integer page = 1; //页码
    private Integer pageSize = 10; //每页展示记录数
    private String name; //姓名
    private Integer gender; //性别
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate begin; //入职时间-开始
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate end; //入职时间-结束
}

从而控制层

java 复制代码
@GetMapping
public Result page(EmpQueryParam empQueryParam){
    log.info("分页查询:{}",empQueryParam);
    PageResult<Emp> pageResult = empService.page(empQueryParam);
    return Result.success(pageResult);
}

业务层

java 复制代码
PageResult page(EmpQueryParam empQueryParam);
java 复制代码
@Override
public PageResult<Emp> page(EmpQueryParam empQueryParam) {
    //1. 设置分页参数
    PageHelper.startPage(empQueryParam.getPage(),empQueryParam.getPageSize());

    //2. 执行查询
    List<Emp> empList = empMapper.list(empQueryParam);//调用数据访问层查询

数据访问层

java 复制代码
public List<Emp> list(EmpQueryParam empQueryParam);

8、条件分页查询-程序优化二(动态SQL)

Mybatis中动态SQL的使用场景?

如果SQL语句是不固定的,是随着用户的输入或外部条件的变化而变化的

在数据访问层的映射文件中

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">

    <select id="list" resultType="com.itheima.pojo.Emp">
        select e.*,d.name deptName from emp e left join dept d on e.id = d.id
        <where>
            <if test = "name != null and name != ''">
                e.name like concat('%',#{name},'%')
            </if>
            <if test="gender != null">
                and e.gender = #{gender}
            </if>
            <if test="begin != null and end != null">
                and entry_date between #{begin} and #{end}
            </if>
        </where>
        order by e.update_time desc
    </select>
</mapper>

MyBatis中动态SQL的<if>及<where>标签的作用?

<if>标签:条件判断,如果条件成立,则拼接对应的sql片段

<where>:根据查询条件,来生成where关键字,并会自动去除条件前面多 余的and或or。

9、新增员工-思路分析

前端提供json数据,业务层调用两次数据访问层,分别向emp和emp_expr表中新增数据

10、新增员工-保存员工基本信息

在Emp实体类中加入这个字段

java 复制代码
//封装工作经历信息
private  List<EmpExpr> exprList;

控制层

java 复制代码
/**
 * 新增员工
 */
@PostMapping
public Result save(@RequestBody Emp emp){
    log.info("新增员工:{}",emp);
    empService.save(emp);
    return Result.success();
}

业务层

java 复制代码
@Override
public void save(Emp emp) {
    //1、保存员工基本信息
    emp.setCreateTime(LocalDateTime.now());
    emp.setUpdateTime(LocalDateTime.now());
    empMapper.insert(emp);
    //2、保存员工工作经历信息


}

数据访问层

记得属性名要改成字段名的驼峰命名形式

java 复制代码
@Insert("insert into emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time)" +
        " values (#{username}, #{name}, #{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})")
void insert(Emp emp);

11、新增员工-批量保存员工工作经历

控制层

不变(已有新增操作)

业务层

java 复制代码
@Override
public void save(Emp emp) {
    //1、保存员工基本信息
    emp.setCreateTime(LocalDateTime.now());
    emp.setUpdateTime(LocalDateTime.now());
    empMapper.insert(emp);
    //2、保存员工工作经历信息
    List<EmpExpr> exprList = emp.getExprList();
    if(!CollectionUtils.isEmpty(exprList)){//判断集合是否为空
        empExprMapper.insertBatch(exprList);
    }

}

数据访问层

java 复制代码
/**
 * 批量保存员工的工作经历信息
 */
void insertBatch(List<EmpExpr> exprList);

创建数据访问层的配置文件EmpExprMapper.xml

在数据访问层的方法名上alt回车回车

<foreach>动态sQL标签的作用?其中属性的含义?

  • 作用:遍历集合/数组

  • 属性:

    • collection:集合名称

    • item:集合遍历出来的元素/项

    • separator:每一次遍历使用的分隔符(可选)

    • open:遍历开始前拼接的片段(可选)

    • close:遍历结束后拼接的片段(可选)

XML 复制代码
<mapper namespace="com.itheima.mapper.EmpMapper">

<!--    批量保存员工工作经历信息
    foreach标签:
    collection:遍历的集合
    item:遍历出来的元素
    separator:每次循环之间的分隔符-->
    <insert id="insertBatch">
        insert into emp_expr(emp_id, begin, end, company, job)
        <foreach collection="exprList" item="expr" separator=",">
            (#{expr.empId}, #{expr.begin}, #{expr.end}, #{expr.company}, #{expr.job})
    </insert>
</mapper>

(2)获取员工ID

在新增员工信息的数据访问层上的插入语句上加上这样一行注解

在业务层执行到insert方法之后,id就有值了

java 复制代码
@Options(useGeneratedKeys = true, keyProperty = "id")//获取到生成的主键 --主键返回

在业务层插入工作经历之前加上这样一段,为empId赋值

java 复制代码
//遍历集合,为empId赋值
exprList.forEach(empExpr -> {
    empExpr.setEmpId(emp.getId());
});

(3)导入接口文档

(4)接口文档的使用

右上角管理环境

选择"开发环境"之后就可以使用本地的tomcat服务器了

七、事务管理

在添加员工信息和员工工作经历信息时,有可能会发生网络波动,导致提交信息失败,因此引入事务

1、介绍与操作

1.概念:事务是一组操作 的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体 一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

注意:默认MySQL的事务是自动提交的 ,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务。

当执行多条SQL语句,操作多次数据库时,建议使用事务。

2.如何控制事务?

开启事务:start transaction/begin;

提交事务:commit;(全部成功)

回滚事务:rollback;(只要有一项失败)

sql 复制代码
-- 开启事务
start transaction ;

-- 保存员工基本信息 - emp
insert into emp(id, username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time)
values (49, 'duanyu', '段誉', 1, '13356560022', 1, 6000, '1.jpg', '2020-01-01', 1, now(), now());

-- 批量保存员工工作经历信息 - emp_expr
insert into emp_expr(emp_id, begin, end, company, job)
values(49,'2020-01-01','2021-01-01','百度','java开发'),
(49,'2021-01-01','2023-01-01','字节','java开发');

-- 提交事务
commit ;

-- 回滚事务
rollback ;

3.场景? 银行转账; 下单扣减库存;

2、Spring事务管理-介绍

注解:@Transactional

作用:将当前方法交给spring进行事务管理,

方法执行前,开启事务;

成功执行完毕,提交事务;

出现异常,回滚事务

位置:业务(service)层的方法上、类上、接口上

同时,在application.yml中加上这样一项配置,将事务管理日志级别提高到Debug级别

TypeScript 复制代码
#配置事务管理日志级别
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug

(2)在业务层上

手动创建除0异常,并创建事务管理

java 复制代码
@Transactional//事务管理控制
@Override
public void save(Emp emp) {
    //1、保存员工基本信息
    emp.setCreateTime(LocalDateTime.now());
    emp.setUpdateTime(LocalDateTime.now());
    empMapper.insert(emp);

    //int i = 1/0;

    //2、保存员工工作经历信息
    List<EmpExpr> exprList = emp.getExprList();
    if(!CollectionUtils.isEmpty(exprList)){//判断集合是否为空
        //遍历集合,为empId赋值
        exprList.forEach(empExpr -> {
            empExpr.setEmpId(emp.getId());
        });
        empExprMapper.insertBatch(exprList);
    }

}

3、Spring事务管理-rollbackFor

Transactional异常默认出现运行时异常才会回滚

此时注解属性应该这样写

java 复制代码
@Transactional(rollbackFor = {Exception.class})//事务管理控制,默认出现运行时异常才会回滚

4、Spring事务管理-propagation

(1)介绍

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

该属性的几个值。前两个为常用值

(2)案例

需求:在新增员工信息时,无论是成功还是失败,都要记录操作日志

首先

创建 员工日志表

sql 复制代码
-- 创建员工日志表
create table emp_log(
    id int unsigned primary key auto_increment comment 'ID, 主键',
    operate_time datetime comment '操作时间',
    info varchar(2000) comment '日志信息'
) comment '员工日志表';

创建 员工日志实体类

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmpLog {
    private Integer id; //ID
    private LocalDateTime operateTime; //操作时间
    private String info; //详细信息
}

创建 员工日志数据访问层

java 复制代码
@Mapper
public interface EmpLogMapper {

    @Insert("insert into emp_log (operate_time, info) values (#{operateTime}, #{info})")
    public void insert(EmpLog empLog);

}

创建 员工日志业务层

java 复制代码
public interface EmpLogService {

    public void insertLog(EmpLog empLog);

}
java 复制代码
@Service
public class EmpLogServiceImpl implements EmpLogService {

    @Autowired
    private EmpLogMapper empLogMapper;
    
    @Override
    public void insertLog(EmpLog empLog) {
        empLogMapper.insert(empLog);
    }
}

再 在 员工管理的业务层 调用 员工日志管理业务层

为了使记录日志的方法 无论员工信息保存成功与否都运行,需要环绕try...finally

按住ctrl+alt+T,选择surround with try...finally

java 复制代码
@Autowired
private EmpLogService empLogService;

@Transactional(rollbackFor = {Exception.class})//事务管理控制,默认出现运行时异常才会回滚
@Override
public void save(Emp emp) {
    try{
        //1、保存员工基本信息
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        empMapper.insert(emp);

        int i = 1/0;

        //2、保存员工工作经历信息
        List<EmpExpr> exprList = emp.getExprList();
        if(!CollectionUtils.isEmpty(exprList)){//判断集合是否为空
            //遍历集合,为empId赋值
            exprList.forEach(empExpr -> {
                empExpr.setEmpId(emp.getId());
            });
            empExprMapper.insertBatch(exprList);
        }
    }finally{
        //记录操作日志
        EmpLog empLog = new EmpLog(null,LocalDateTime.now(),"新增员工:" + emp);
        empLogService.insertLog(empLog);
    }
}

此时运行会发现,调用该方法的新增员工方法 会连带记录日志的方法 一起回滚,因此要在 日志管理的业务层的实现类上 也要加上 事务管理的propagation属性的REQUIRES_NEW值

使该方法在新的事务中运行

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insertLog(EmpLog empLog) {
    empLogMapper.insert(empLog);
}

5、事务四大特征(ACID)

(1)原子性 Atomicity

事务是不可分割的最小单元,要么全部成功,要么全部失败

(2)一致性 Consistency

事务完成时,必须使所有的数据都保持一致状态

(3)隔离性 Isolation

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

(4)持久性 Durability

事务一旦提交或回滚,它对数据库中的数据的改变就是永久的

八、文件上传

1、介绍

简介 文件上传:是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。 文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

前端三要素

若未设置enctype,只能提交文件的名字不能提交文件的内容

后端的控制层

java 复制代码
@PostMapping("/upload")
public Result upload(String name, Integer age, MultipartFile file){
    log.info("接收参数:{},{},{}",name,age,file);
    return  Result.success();
}

2、本地存储

在控制层,使用这两行代码获取并保存文件

java 复制代码
//获取原始文件名
String originalFilename = file.getOriginalFilename();

//保存文件
file.transferTo(new File("D:/images/" + originalFilename));

为了上传的文件名不重复,需要重新赋文件名

java 复制代码
@Slf4j
@RestController
public class UploadController {

    @PostMapping("/upload")
    public Result upload(String name, Integer age, MultipartFile file){
        log.info("接收参数:{},{},{}",name,age,file);
        //获取原始文件名
        String originalFilename = file.getOriginalFilename();

        //新的文件名
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));//获取最后一个"."及其后面的字符串
        String newFileName = UUID.randomUUID().toString() + extension;
        //保存文件
        file.transferTo(new File("D:/images/" + newFileName));
        return  Result.success();
    }

}

上传文件大小限制 配置文件中

TypeScript 复制代码
spring:
  servlet:
    multipart:
      # 最大单个文件大小
      max-file-size: 10MB
      # 最大请求大小(包括所有文件和表单数据)
      max-request-size: 100MB

如何将上传的文件存储在服务器本地?

复制代码
multipartFile.getoriginalFilename():获取原始文件名
multipartFile.transferTo(Filedest):将文件转存到磁盘文件

3、阿里云OSS-准备工作

阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商。

阿里云对象存储OSS(Object StorageService),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

SDK:Software Development Kit的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。

具体的准备工作

Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。

4、阿里云OSS-入门程序

注意:在使用第三方提供的云服务或技术时,一定要参照对应的官方文档进行开发和测试。

在阿里云官方找到SDK

找到Maven依赖

XML 复制代码
<!--阿里云OSS依赖-->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</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>

测试代码,上传到阿里云OSS

java 复制代码
package com.itheima;

import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.file.Files;

public class Demo {

    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "java-ai-swb";
        // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
        String objectName = "001.jpg";
        // 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
        String region = "cn-beijing";

        // 创建OSSClient实例。
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialsProvider)
                .clientConfiguration(clientBuilderConfiguration)
                .region(region)
                .build();

        try {
            File file = new File("D:\\真图片\\桌面\\1\\ (2).jpg");
            byte[] content = Files.readAllBytes(file.toPath());//将文件读取成字节数组
            //传递到阿里云OSS
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}

5、阿里云OSS-案例集成

在com.itheima目录下创建工具类包utils

java 复制代码
package com.itheima.utils;

import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

@Component//交给他管理,若以后想用到这个工具类,只需要Autowired注入进来即可
public class AliyunOSSOperator {

    private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    private String bucketName = "java-ai-swb";
    private String region = "cn-beijing";
//参数为文件的字节数组和文件的原始名称
    public String upload(byte[] content, String originalFilename) throws Exception {
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();

        // 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。
        //获取当前系统日期的字符串,格式为 yyyy/MM
        String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
        //生成一个新的不重复的文件名
        String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
        String objectName = dir + "/" + newFileName;

        // 创建OSSClient实例。官方代码,不用动,也不用懂
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialsProvider)
                .clientConfiguration(clientBuilderConfiguration)
                .region(region)
                .build();

        try {//将文件上传到bucket中
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
        } finally {
            ossClient.shutdown();
        }
        //组装成文件访问路径
        return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
    }

}

上传员工信息的控制层代码

java 复制代码
@Autowired
private AliyunOSSOperator aliyunOSSOperator;
/**
 * 阿里云OSS存储方案
 * @param file
 * @return
 */
@PostMapping("/upload")
public Result upload(MultipartFile file) throws Exception {
    log.info("文件上传:{}",file);
    //将文件交给OSS存储管理
    String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());
    log.info("文件上传成功:{}",url);
    return Result.success(url);
}

6、阿里云OSS-程序优化(@Value)

为了使这些灵活变动的参数便于管理,应该把他们放到配置文件中

TypeScript 复制代码
# 阿里云OSS
aliyun:
  oss:
    endpoint: https://oss-cn-beijing.aliyuncs.com
    bucketName: java-ai-swb
    region: cn-beijing

使用Value注解导入参数

java 复制代码
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
@Value("${aliyun.oss.region}")
private String region;

7、阿里云OSS-程序优化(@ConfigurationProperties)

在utils包下创建AliyunOSSProperties类,封装三个实体属性

java 复制代码
@Data
@Component//交给IOC容器管理,可以AutoWired注入
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
    private String endpoint;
    private String bucketName;
    private String region;
}

在方法外注入该类,并在方法内声明类中的属性

java 复制代码
 //方式二:通过@ConfigurationProperties注解批量注入
    @Autowired
    private AliyunOSSProperties aliyunOSSProperties;


//参数为文件的字节数组和文件的原始名称
    public String upload(byte[] content, String originalFilename) throws Exception {
        String endpoint = aliyunOSSProperties.getEndpoint();
        String bucketName = aliyunOSSProperties.getBucketName();
        String region = aliyunOSSProperties.getRegion();

九、员工管理

1、删除员工-请求参数接收

通过数组和集合(推荐)两种方式请求参数接收

java 复制代码
/**
 * 删除员工-数组
 */
@DeleteMapping
public Result delete(Integer[] ids){
    log.info("删除员工:{}", Arrays.toString(ids));
    empService.delete(ids);
    return Result.success();
}

/**
 * 删除员工-集合
 */
@DeleteMapping
public Result delete(@RequestParam List<Integer> ids){
    log.info("删除员工:{}", ids);
    empService.delete(ids);
    return Result.success();
}

2、删除员工-逻辑实现

业务层逻辑

java 复制代码
@Transactional(rollbackFor = {Exception.class})
@Override
public void delete(List<Integer> ids) {
    empMapper.deleteByIds(ids);
    empMapper.deleteByEmpIds(ids);
}

数据访问层逻辑

java 复制代码
    void deleteByIds(List<Integer> ids);
//根据员工id批量删除员工工作经历
    void deleteByEmpIds(List<Integer> empIds);

在EmpMapper.xml文件中,实现数据访问的SQL语句

XML 复制代码
<!--    批量删除员工的基本信息-->
    <delete id="deleteByIds">
        delete from emp where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>
<!--    根据员工id批量删除员工工作经历-->
    <delete id="deleteByEmpIds">
        delete from emp_expr where emp_id in
        <foreach collection="empIds" item="empId" separator="," open="(" close=")">
            #{empId}
        </foreach>
    </delete>

3、修改员工-查询回显

控制层逻辑

java 复制代码
/**
 * 根据id查询员工
 */
@GetMapping("/{id}")
public Result getOne(@PathVariable Integer id){//路径参数注解
    log.info("根据id查询员工:{}", id);
    Emp emp = empService.getInfo(id);
    return Result.success(emp);
}

业务层逻辑

java 复制代码
/**
 * 根据ID查询员工信息
 */
@Override
public Emp getInfo(Integer id) {
    return empMapper.getById(id);
}

数据访问层逻辑

java 复制代码
Emp getById(Integer id);

数据访问层配置文件的逻辑

XML 复制代码
<!--定义ResultMap-->
<!--基于ResultMap来完成复杂属性的封装-->
<resultMap id="empResultMap" type="com.itheima.pojo.Emp">
    <!--        column代表数据库字段名,property代表实体类属性名,字段映射到属性-->
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <result column="name" property="name"/>
    <result column="gender" property="gender"/>
    <result column="phone" property="phone"/>
    <result column="job" property="job"/>
    <result column="salary" property="salary"/>
    <result column="image" property="image"/>
    <result column="entry_date" property="entryDate"/>
    <result column="dept_id" property="deptId"/>
    <result column="create_time" property="createTime"/>
    <result column="update_time" property="updateTime"/>
    <!--封装工作经历信息-->
    <!--property属性名 ofType泛型-->
    <collection property="exprList" ofType="com.itheima.pojo.EmpExpr">
        <id column="ee_id" property="id"/>
        <result column="ee_empid" property="empId"/>
        <result column="ee_begin" property="begin"/>
        <result column="ee_end" property="end"/>
        <result column="ee_company" property="company"/>
        <result column="ee_job" property="job"/>
    </collection>
</resultMap>

<!--    根据ID查询员工基本信息以及工作经历信息-->
<select id="getById" resultType="com.itheima.pojo.Emp">
    select
        e.*,
        ee.id ee_id,
        ee.emp_id ee_empid,
        ee.begin ee_begin,
        ee.end ee_end,
        ee.company ee_company,
        ee.job ee_job
    from emp e left join emp_expr ee on e.id = ee.emp_id
    where e.id = #{id};
</select>

Mybatis中封装查询结果,什么时候用resultType,什么时候用resultMap?

如果查询返回的字段名与实体的属性名 可以直接对应上,用resultType。

如果查询返回的字段名与实体的属性名对应不上 ,或实体属性比较复杂,可以通过resultMap手 动封装。

4、修改员工-修改数据

先删再加

控制层逻辑

java 复制代码
/**
 * 修改员工
 */
@PutMapping
public Result update(@RequestBody Emp emp){
    log.info("修改员工:{}", emp);
    empService.update(emp);
    return Result.success();
}

业务层逻辑

java 复制代码
@Transactional(rollbackFor = {Exception.class})
@Override
public void update(Emp emp) {
    //1、根据ID修改员工的基本信息
    emp.setUpdateTime(LocalDateTime.now());
    empMapper.updateById(emp);

    //2、根据ID修改员工的工作经历信息
    //2.1、先删除
    empExprMapper.deleteByEmpIds(Arrays.asList(emp.getId()));
    //2.2 再添加
    List<EmpExpr> exprList = emp.getExprList();
    //为exprList赋值,设置empId
    if(!CollectionUtils.isEmpty(exprList)){
        exprList.forEach(empExpr -> {
            empExpr.setEmpId(emp.getId());
        });
        empExprMapper.insertBatch(exprList);
    }
}

数据访问层

java 复制代码
/**
 * 根据ID修改员工信息
 * @param emp
 */
void updateById(Emp emp);

数据访问映射文件

5、修改员工-程序优化(动态更新)

修改更新语句

XML 复制代码
<!--    根据ID更新员工基本信息-->
<!--    set标签:会自动生成set关键字;会自动删除最后一个逗号-->
    <update id="updateById">
        update emp
        <set>
            <if test="username != null and username != ''">username = #{username},</if>
            <if test="password != null and password != ''">password = #{password},</if>
            <if test="name != null and name != ''">name = #{name},</if>
            <if test="gender != null">gender = #{gender},</if>
            <if test="phone != null and phone != ''">phone = #{phone},</if>
            <if test="job != null"> job = #{job},</if>
            <if test="salary != null"> salary = #{salary},</if>
            <if test="image != null and image != ''">  image = #{image},</if>
            <if test="entryDate != null"> entry_date = #{entryDate},</if>
            <if test="deptId != null">  dept_id = #{deptId},</if>
            <if test="updateTime != null"> update_time = #{updateTime}</if>
        </set>
        where id = #{id}
    </update>

十、全局异常处理器

在项目包下创建exception包,再创建GlobalExceptionHandler.java文件

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

java 复制代码
/**
 * 全局异常处理器
 */
@Slf4j
@RestControllerAdvice//表示这是一个全局异常处理器
public class GlobalExceptionHandler {

    @ExceptionHandler//表示这是一个异常处理器
    public Result handleException(Exception e) {
        log.error("程序出错啦~", e);
        return Result.error("出错啦,请联系管理员~");
    }
    
//异常会按照继承关系,从下往上匹配
//当数据库中违反了唯一约束就会报DuplicateKeyException异常
    @ExceptionHandler
    public Result handleDuplicateKeyException(DuplicateKeyException e) {
        log.error("程序出错啦~", e);
        String message = e.getMessage();//获取错误信息
        int i = message.indexOf("Duplicate entry");//获取异常信息中的"Duplicate entry"位置
        String errMsg = message.substring(i);//获取异常信息中的"Duplicate entry"之后的内容
        String[] arr = errMsg.split(" ");//将异常信息中的"Duplicate entry"之后的内容按空格分割
        return Result.error(arr[2] + "已存在");//返回值包括错误信息的重复的值
    }
}

十一、员工信息统计

1、职位统计-分析

创建实体类jobOption

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JobOption {

    private List jobList;
    private List dataList;
}

统计每一种职位的人数的SQL语句

sql 复制代码
-- 统计每一种职位对应的人数
-- case函数:case表达式 when val1 then result1 when val2 then result2... else...end
-- 只能进行等值匹配
select
    (case job
        when 1 then '班主任'
        when 2 then '讲师'
        when 3 then '学工主管'
        when 4 then '教研主管'
        when 5 then '咨询师'
        else '其他' end) pos,
     count(*) num from emp group by job order by num;

-- case函数2:case表达式 when val1 then result1 when val2 then result2... else...end
-- 可以进行范围匹配
select
    (case
        when job=1 then '班主任'
        when job=2 then '讲师'
        when job=3 then '学工主管'
        when job=4 then '教研主管'
        when job=5 then '咨询师'
        else '其他' end) pos,
     count(*) num from emp group by job order by num;

2、职位统计-实现

数据访问层逻辑

java 复制代码
/**
 * 统计员工职位数据
 * @return
 */
@MapKey("pos")//可以不用,只是标注主键
List<Map<String,Object>> countEmpJobData();

数据访问配置文件逻辑

XML 复制代码
<select id="countEmpJobData" resultType="java.util.Map">
    select
        (case
             when job=1 then '班主任'
             when job=2 then '讲师'
             when job=3 then '学工主管'
             when job=4 then '教研主管'
             when job=5 then '咨询师'
             else '其他' end) pos,
        count(*) num from emp group by job order by num;
</select>

新建一个控制层ReportController,因为访问路径不一样

java 复制代码
@Slf4j
@RequestMapping("/report")
@RestController
public class ReportController
{
    @Autowired
    private ReportService reportService;
    /**
     * 统计员工职位人数
     */
    @GetMapping("/empJobData")
    public Result empJobData()
    {
        log.info("统计员工职位人数");
        JobOption jobOption = reportService.getEmpJobData();
        return Result.success(jobOption);
    }

}

业务层逻辑

java 复制代码
public interface ReportService {

    /**
     * 统计员工职位人数
     * @return
     */
    JobOption getEmpJobData();
}
java 复制代码
@Service
public class ReportServiceImpl implements ReportService {

    @Autowired
    private EmpMapper empMapper;

    @Override
    public JobOption getEmpJobData() {
        //调用mapper接口,获取统计数据
        List<Map<String, Object>> list = empMapper.countEmpJobData();
        //组装结果,并返回
        List<Object> jobList = list.stream().map(dataMap -> dataMap.get("pos")).toList();
        List<Object> dataList = list.stream().map(dataMap -> dataMap.get("num")).toList();

        return new JobOption(jobList, dataList);
    }
}

3、性别统计

数据库操作逻辑

sql 复制代码
-- 统计员工性别人数
-- if(条件,true_value,false_value)
select if(gender = 1,'男性员工','女性员工') name ,count(*) value from emp group by gender;
-- ifnull(expr,val1):如果expr不为null,取自身,否则取val1

控制层逻辑

java 复制代码
@GetMapping("/empGenderData")
public  Result gettEmpGenderData(){
     log.info("统计员工性别人数");
    List<Map<String, Object>> genderList = reportService.getEmpGenderData();
    return Result.success(genderList);
}

业务层逻辑

java 复制代码
    /**
     * 统计员工性别人数
     * @return
     */
    List<Map<String, Object>> getEmpGenderData();
}
java 复制代码
@Override
public List<Map<String, Object>> getEmpGenderData() {
    return empMapper.countEmpGenderData();
}

数据访问层逻辑

java 复制代码
/**
 * 统计员工性别人数
 * @return
 */
@MapKey("name")
List<Map<String, Object>> countEmpGenderData();

数据访问层实现类逻辑

XML 复制代码
<select id="countEmpGenderData" resultType="java.util.Map">
    select if(gender = 1,'男性员工','女性员工') name ,count(*) value from emp group by gender;
</select>

十二、注解总结

java 复制代码
//IOC容器
    //自动从IOC容器注入
    @Autowired
    //将这几层交给IOC容器管理
    @RestController
    @Service
    @Mapper
​
​
//控制层注解
    //总访问路径
    @RequestMapping
​
    @GetMapping
    @DeleteMapping
​
    //参数注解:路径参数
    @PathVariable
    //参数注解:请求时其后参数必须传递
    @RequestParam
    //参数注解:设默认值(分页参数)
    @RequestParam(defaultValue = "1")
    //参数注解:设日期形式
    @DateTimeFormat(pattern = "yyyy-MM-dd")
​
​
//业务层注解
    //用于业务层的实现类覆盖业务层的方法
    @Override
    //获取到生成的主键 --主键返回
    @Options(useGeneratedKeys = true, keyProperty = "id")
    //事务管理控制
    @Transactional(rollbackFor = {Exception.class})
​
​
//数据访问层注解
    //为数据访问层的属性名起别名
    @Results
    @Result
​
    //可以不用,只是标注主键
    @MapKey("pos")
​
    //数据访问操作
    @Select
    @Delete
​
​
//实体类
    //引入Lombok依赖后
    //自动创建get、set方法
    @Data
    //自动创建无参构造
    @NoArgsConstructor
    //自动创建带参构造
    @AllArgsConstructor
​
​
//工具类(阿里云)
    //将该工具类交给IOC容器管理
    @Component
    //使用该注解从配置文件导入参数(阿里云配值)
    @Value
    通过该注解批量注入三个实体属性endpoint、桶名、地区
    @ConfigurationProperties
​
        
//异常处理类
    //表示这是一个全局异常处理器
    @RestControllerAdvice
    //表示这是一个异常处理器
    @ExceptionHandler
相关推荐
IT枫斗者枫哥6 分钟前
Java 文件导出:写完了,为什么还不能标记成功?
java
陈皮波比茶12 分钟前
java笔记-typora快捷键
java
蚂蚁雅嘿20 分钟前
maven标签日常总结
java·maven
洋不写bug32 分钟前
二叉树(二) 常见基础操作解析|结点数、树高、查找结点、判断完全二叉树
java·开发语言·数据结构·完全二叉树·二叉树结点数·树高·查找结点
菠萝加点糖42 分钟前
Maven 坐标系统说明
java·maven
hm宋1 小时前
Canal 升级 jar 包操作文档(以升级 FastJSON 为例)
java·canal·jar
后台模板学习1 小时前
学习的心态高频面试题
java·数据库·学习
用户3126874877201 小时前
线程池到底怎么管理线程的?从 ThreadPoolExecutor 到拒绝策略全链路拆解
java
cpolar技术支持2 小时前
Kafka Streams 窗口统计怎么验收:本地跑订单流聚合,用 cpolar 给同事看只读结果页
java·docker·kafka·cpolar·kafka streams
爱读源码的大都督2 小时前
DeepSeek面试官问:生产RAG系统回答不准确,该如何定位和优化?这样回答,能让面试官当场给你Offer!
java·后端·python