【Javaweb学习|Day6】日志技术、多表查询、分页查询及优化(动态SQL)

终于放寒假了,继续后端进度

文章目录

一.日志

1)概述

日志:用来记录应用程序的运行信息、状态信息、错误信息

为什么用sout直接输出不行?

因为:

  • 硬编码。所有的记录日志的代码,都是硬编码,没有办法做到灵活控制,要想不输出这个日志了,只能删除掉记录日志的代码。
  • 只能输出日志到控制台。
  • 不便于程序的扩展、维护

2)日志框架

  • JUL:javase官方提供的日志框架,配置简单,性能较差
  • Log4j:较为流行的日志框架,提供了灵活的配置选项,支持多种输出目标
  • **Logback:**基于Log4j升级而来,提供了更多的功能和配置选项,性能由于Log4j
  • Slf4j:一套日志的规范,提供了日志操作的标准接口及抽象类

3)Logback教程

  1. 准备工作:引入logback依赖(springboot中无需引入,在springboot中已经传递了此依赖)

    xml 复制代码
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.4.11</version>
    </dependency>
  2. 引入配置文件logback.xml,放在src/main/resources包下

    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个字符宽度  %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" />
        </root>
    </configuration>
  3. 定义日志记录对象Logger,记录日志

    eg:

    java 复制代码
    public class LogTest {
        
        //定义日志记录对象
        private static final Logger log = LoggerFactory.getLogger(LogTest.class);
    
        @Test
        public void testLog(){
            log.debug("开始计算...");
            int sum = 0;
            int[] nums = {1, 5, 3, 2, 1, 4, 5, 4, 6, 7, 4, 34, 2, 23};
            for (int i = 0; i < nums.length; i++) {
                sum += nums[i];
            }
            log.info("计算结果为: "+sum);
            log.debug("结束计算...");
        }
    
    }

4)Logback配置文件

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

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

  1. 输出到控制台

    xml 复制代码
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <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>
  2. 输出到文件

    xml 复制代码
    <!-- 按照每天生成日志文件 -->
    <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>
  3. 日志的开关配置(开启ALL、取消OFF)

    xml 复制代码
    <!-- 日志输出级别 -->
    <root level="ALL">
        <!--输出到控制台-->
        <appender-ref ref="STDOUT" />
        <!--输出到文件-->
        <appender-ref ref="FILE" />
    </root>

可以在配置文件logback.xml中,灵活的控制输出那些类型的日志。(大于等于配置的日志级别的日志才会输出)

XML 复制代码
<!-- 日志输出级别 -->
<root level="info">
    <!--输出到控制台-->
    <appender-ref ref="STDOUT" />
    <!--输出到文件-->
    <appender-ref ref="FILE" />
</root>

在日常开发中建议把日志输出级别设置为info或者debug

如果级别太低,会将源码的日志也输出,影响日志查看

6)案例

回到tlias案例中,把先前用sout输出的方式全部改为Logback日志

java 复制代码
/**
 * 部门管理控制器
 */
@Slf4j
@RequestMapping("/depts")
@RestController
public class DeptController {

    @Autowired
    private DeptService deptService;

    /**
     * 查询部门列表
     */
    //@RequestMapping(value = "/depts", method = RequestMethod.GET)
    @GetMapping
    public Result list(){
        //System.out.println("查询部门列表");
        log.info("查询部门列表");
        List<Dept> deptList = deptService.findAll();
        return Result.success(deptList);
    }

    /**
     * 根据id删除部门 - delete http://localhost:8080/depts?id=1
     */
    @DeleteMapping
    public Result delete(Integer id){
        //System.out.println("根据id删除部门, id=" + id);
        log.info("根据id删除部门, id: {}" , id);
        deptService.deleteById(id);
        return Result.success();
    }

    /**
     * 新增部门 - POST http://localhost:8080/depts   请求参数:{"name":"研发部"}
     */
    @PostMapping
    public Result save(@RequestBody Dept dept){
        //System.out.println("新增部门, dept=" + dept);
        log.info("新增部门, dept: {}" , dept);
        deptService.save(dept);
        return Result.success();
    }

    /**
     * 根据ID查询 - GET http://localhost:8080/depts/1
     */
    @GetMapping("/{id}")
    public Result getById(@PathVariable Integer id){
        //System.out.println("根据ID查询, id=" + id);
        log.info("根据ID查询, id: {}" , id);
        Dept dept = deptService.getById(id);
        return Result.success(dept);
    }

    /**
     * 修改部门 - PUT http://localhost:8080/depts  请求参数:{"id":1,"name":"研发部"}
     */
    @PutMapping
    public Result update(@RequestBody Dept dept){
        //System.out.println("修改部门, dept=" + dept);
        log.info("修改部门, dept: {}" , dept);
        deptService.update(dept);
        return Result.success();
    }
}

5)Logback日志级别

日志级别:日志信息的类型

由低到高依次是:

日志级别 说明 记录方式
trace 追踪,记录程序运行轨迹 【使用很少】 log.trace("...")
debug 调试,记录程序调试过程中的信息,实际应用中一般将其视为最低级别 【使用较多】 log.debug("...")
info 记录一般信息,描述程序运行的关键事件,如:网络连接、io操作 【使用较多】 log.info("...")
warn 警告信息,记录潜在有害的情况 【使用较多】 log.warn("...")
error 错误信息 【使用较多】 log.error("...")

==注:==可以在类上注释@Slf4j,就可以不用在类内定义日志记录对象,便于直接使用

添加了该注解,就相当于在类中定义了日志记录器:

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

二.多表查询

细节部分可见MySQL部分笔记

这里重点说物理外键和逻辑外键

  • 物理外键

    • 使用foreign key定义外键关联另一张表

    • 缺点:

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

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

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

  • 逻辑外键

    • 在业务层中解决外键关联

现实开发中,大多使用逻辑外键而紧张使用物理外键

三.分页查询

在tlias案例中,若要实现分页查询,我们需要向前端传递:查询的总条数、查询结果列表;前端在请求服务器时,传递当前页码、每页条数

使用LIMIT进行分页查询

1). 查询第1页数据的SQL语句是:

Java 复制代码
select * from emp  limit 0,10;

2). 查询第2页数据的SQL语句是:

Java 复制代码
select * from emp  limit 10,10;

3). 查询第3页的数据的SQL语句是:

Java 复制代码
select * from emp  limit 20,10;

所以,开始索引的计算公式: 开始索引 = (当前页码 - 1) * 每页显示条数

1)原始方式

1). EmpController

java 复制代码
@Slf4j
@RequestMapping("/emps")
@RestController
public class EmpController {
        
    @Autowired
    private EmpService empService;
        
    @GetMapping
    public Result page(@RequestParam(defaultValue = "1") Integer page ,
                       @RequestParam(defaultValue = "10") Integer pageSize){
        log.info("查询员工信息, page={}, pageSize={}", page, pageSize);
        PageResult pageResult = empService.page(page, pageSize);
        return Result.success(pageBean);
    }
        
}

@RequestParam(defaultValue="默认值") //设置请求参数默认值

2). EmpService

java 复制代码
public interface EmpService {
    /**
     * 分页查询
     * @param page 页码
     * @param pageSize 每页记录数
     */
    PageResult page(Integer page, Integer pageSize);
}

3). EmpServiceImpl

java 复制代码
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpMapper empMapper;

    @Override
    public PageResult page(Integer page, Integer pageSize) {
        //1. 获取总记录数
        Long total = empMapper.count();

        //2. 获取结果列表
        Integer start = (page - 1) * pageSize;
        List<Emp> empList = empMapper.list(start, pageSize);

        //3. 封装结果
        return new PageResult(total, empList);
    }
}

4). EmpMapper

java 复制代码
@Mapper
public interface EmpMapper {

    /**
     * 查询总记录数
     */
    @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 as e left join dept as d on e.dept_id = d.id limit #{start}, #{pageSize}")
    public List<Emp> list(Integer start , Integer pageSize);

}

2)PageHelper分页插件

使用原始方法较为繁琐,Mybatis提供了一个分页查询的插件PageHelper

使用前后对比

  • Mapper接口层:
    • 原始的分页查询功能中,我们需要在Mapper接口中定义两条SQL语句。
    • PageHelper实现分页查询之后,只需要编写一条SQL语句,而且不需要考虑分页操作,就是一条正常的查询语句。
  • Service层:
    • 需要根据页码、每页展示记录数,手动的计算起始索引。
    • 无需手动计算起始索引,直接告诉PageHelper需要查询那一页的数据,每页展示多少条记录即可。

使用了PageHelper,就无需再Mapper中进行手动分页了。

在Mapper中我们只需要进行正常的列表查询即可。

在Service层中,调用Mapper的方法之前设置分页参数,在调用Mapper方法执行查询之后,解析分页结果,并将结果封装到PageResult对象中返回。

1). 在pom.xml引入依赖

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

2). EmpMapper

java 复制代码
/**
 * 查询所有的员工及其对应的部门名称
 */
@Select("select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id")
public List<Emp> list();

3). EmpServiceImpl

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;

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

附:解决页码为负数的问题:

在PageHelper中,我们可以通过合理化参数配置,来解决这个问题。直接在application.yml中,引入如下配置即可:

java 复制代码
pagehelper:
  reasonable: true
  helper-dialect: mysql

reasonable:分页合理化参数,默认值为false。

当该参数设置为true时,pageNum<=0时会查询第一页,pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。
PageHelper底层逻辑

为啥empList可以强转为Page

PageHelper 做了以下事情:

  1. 拦截 MyBatis 的查询(通过插件机制)。
  2. 先执行一次 COUNT 查询 ,获取总记录数(比如 SELECT COUNT(*) FROM user)。
  3. 再执行你的原 SQL,并加上分页语句 (如 LIMIT 0,10)。
  4. 将查询结果(List 数据) + 总记录数等信息,封装到一个 Page 对象中
  5. 把这个 Page 对象作为查询结果返回 (虽然方法签名是 List<User>,但实际返回的是 Page<User> 实例)。

所以你拿到的 list 虽然声明为 List,但运行时是 Page,你可以安全地强转

3)实现机制

PageHelper在进行分页查询时,会执行上述两条SQL语句

  • select count(*) from ... 查询总条数
  • select ... from ... 查询结果列表

并将查询到的总记录数,与数据列表封装到了 Page<Emp> 对象中,我们再获取查询结果时,只需要调用Page对象的方法就可以获取

注意事项:

  • PageHelper实现分页查询时,SQL语句的结尾一定一定一定不要加分号(😉
  • PageHelper只会对紧跟在其后的第一条SQL语句进行分页处理

四.条件分页查询

1)普通实现

1). 在EmpController方法中通过多个方法形参,依次接收这几个参数

java 复制代码
@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {

    @Autowired
    private EmpService empService;

    @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 pageResult = empService.page(page, pageSize);
        return Result.success(pageResult);
    }
}

2). 修改EmpService及EmpServiceImpl中的代码逻辑

EmpService:

java 复制代码
public interface EmpService {
    /**
     * 分页查询
     */
    PageResult page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end);
}

EmpServiceImpl:

java 复制代码
/**
 * 员工管理
 */
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpMapper empMapper;

    @Override
    public PageResult page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end) {
        //1. 设置PageHelper分页参数
        PageHelper.startPage(page, pageSize);
        //2. 执行查询
        List<Emp> empList = empMapper.list(name, gender, begin, end);
        //3. 封装分页结果
        Page<Emp> p = (Page<Emp>) empList;
        return new PageResult(p.getTotal(), p.getResult());
    }
}

3). 调整EmpMapper接口方法

java 复制代码
@Mapper
public interface EmpMapper {
    
    /**
     * 查询所有的员工及其对应的部门名称
     */
    public List<Emp> list(String name, Integer gender, LocalDate begin, LocalDate end);
    
}

由于SQL语句比较复杂,建议将SQL语句配置在XML映射文件中

4). 新增Mapper映射文件EmpMapper.xml

xml 复制代码
<!--定义Mapper映射文件的约束和基本结构-->
<!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 as e left join dept as d on e.dept_id = d.id
        where e.name like concat('%',#{name},'%')
          and e.gender = #{gender}
          and e.entry_date between #{begin} and #{end}
    </select>
</mapper>

2)优化1:传递参数封装为对象

优化思路:定义一个实体类,来封装这几个请求参数。

【需要保证,前端传递的请求参数和实体类的属性名是一样的】

1). 定义实体类:EmpQueryParam

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

import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;

@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; //入职结束时间
    
}

2). EmpController接收请求参数

java 复制代码
@GetMapping
public Result page(EmpQueryParam empQueryParam) {
    log.info("查询请求参数: {}", empQueryParam);
    PageResult pageResult = empService.page(empQueryParam);
    return Result.success(pageResult);
}

3). 修改EmpService接口方法

java 复制代码
public interface EmpService {
    /**
     * 分页查询
     */
    //PageResult page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end);
    PageResult page(EmpQueryParam empQueryParam);
}

4). 修改EmpServiceImpl中的page方法

java 复制代码
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpMapper empMapper;

    /*@Override
    public PageResult page(Integer page, Integer pageSize, String name, Integer gender, LocalDate begin, LocalDate end) {
        //1. 设置PageHelper分页参数
        PageHelper.startPage(page, pageSize);
        //2. 执行查询
        List<Emp> empList = empMapper.list(name, gender, begin, end);
        //3. 封装分页结果
        Page<Emp> p = (Page<Emp>) empList;
        return new PageResult(p.getTotal(), p.getResult());
    }*/

    public PageResult page(EmpQueryParam empQueryParam) {
        //1. 设置PageHelper分页参数
        PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize());
        //2. 执行查询
        List<Emp> empList = empMapper.list(empQueryParam);
        //3. 封装分页结果
        Page<Emp> p = (Page<Emp>)empList;
        return new PageResult(p.getTotal(), p.getResult());
    }
}

5). 修改EmpMapper接口方法

java 复制代码
@Mapper
public interface EmpMapper {

    /**
     * 查询所有的员工及其对应的部门名称
     */
//    @Select("select e.*, d.name as deptName from emp e left join dept d on e.dept_id = d.id")
//    public List<Emp> list(String name, Integer gender, LocalDate begin, LocalDate end);
    
    /**
     * 根据查询条件查询员工
     */
    List<Emp> list(EmpQueryParam empQueryParam);
}

EmpMapper.xml 中的配置无需修改

3)优化2:动态SQL解决参数个数多变

在查询时,Mapper映射配置文件中的SQL语句中,查询条件是写死的

而在员工管理中,根据条件查询员工信息时,查询条件是可选的,可以输入也可以不输入

所谓动态SQL,指的就是随着用户的输入或外部的条件的变化而变化的SQL语句

具体实现

xml 复制代码
<!--定义Mapper映射文件的约束和基本结构-->
<!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 as e left join dept as d on e.dept_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 e.entry_date between #{begin} and #{end}
            </if>
        </where>
    </select>
</mapper>
  • <if>:判断条件是否成立,如果条件为true,则拼接SQL
  • <where>:根据查询条件,来生成where关键字,并会自动去除条件前面多余的and或or
相关推荐
七夜zippoe2 小时前
Elasticsearch核心概念与Java客户端实战 构建高性能搜索服务
java·大数据·elasticsearch·集群·索引·分片
深念Y2 小时前
easylive仿B站项目 后端 单体版 项目构建
java·开发语言
阿杰 AJie2 小时前
Java Stream API详细用法
java·windows·python
蒜香拿铁2 小时前
【第五章】python判断语句if
java·服务器·python
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 公寓楼设备报修管理系统为例,包含答辩的问题和答案
java·eclipse
qq_12498707532 小时前
基于微信小程序的宠物寄领养系统(源码+论文+部署+安装)
java·spring boot·后端·微信小程序·小程序·宠物·计算机毕业设计
那我掉的头发算什么2 小时前
【SpringBoot】从创建第一个spring项目开始
spring boot·后端·spring
独自破碎E2 小时前
说说Java中的JIT
java·开发语言
齐鲁大虾2 小时前
如何通过Java调取打印机打印图片和文本
java·开发语言·python