JavaWeb笔记_maven环境变量控制-程序员宅基地

技术标签: 笔记  

一.Maven

  • maven是apache旗下的一个开源项目,是一款用于管理和构建java项目的工具

  • Apache 软件基金会,成立于1999年7月,是目前世界上最大的最受欢迎的开源软件基金会,也是一个专门为支持开源项目而生的非盈利性组织。

  • 开源项目:https://www.apache.org/index.html#projects-list

1.作用

依赖管理:方便快捷的管理项目依赖的资源(jar包),避免版本冲突问题

 

统一项目结构:提供标准、统一的项目结构  

  • 项目构建: 标准跨平台(Linux、Windows、MacOS)的自动化项目构建方式

2.Maven概述

01 概述

  • Apache Maven 是一个项目管理和构建工具,它基于项目对象模型(POM)的概念,通过一小段描述信息来管理项目的构建。

  • 作用:

    • 方便的依赖管理

    • 统一的项目结构

    • 标准的项目构建流程

  • 官网:Maven – Welcome to Apache Maven

  • 仓库:用于存储资源,管理各种jar包。

    • 本地仓库:自己计算机上的一个目录。

    • 中央仓库:由Maven团队维护的全球唯一的。 仓库地址:Central Repository:

    • 远程仓库(私服):一般由公司团队搭建的私有仓库。

02 安装

2.4.1 下载

下载地址:Maven – Download Apache Maven

在提供的资料中,已经提供了下载好的安装包。如下:

2.4.2 安装步骤

Maven安装配置步骤:

  1. 解压安装

  2. 配置仓库

  3. 配置Maven环境变量

1、解压 apache-maven-3.6.1-bin.zip(解压即安装)

建议解压到没有中文、特殊字符的路径下。如课程中解压到 E:\develop 下。

解压缩后的目录结构如下:  

  • bin目录 : 存放的是可执行命令。(mvn 命令重点关注)

  • conf目录 :存放Maven的配置文件。(settings.xml配置文件后期需要修改)

  • lib目录 :存放Maven依赖的jar包。(Maven也是使用java开发的,所以它也依赖其他的jar包)

2、配置本地仓库

2.1、在自己计算机上新一个目录(本地仓库,用来存储jar包)

2.2、进入到conf目录下修改settings.xml配置文件

1). 使用超级记事本软件,打开settings.xml文件,定位到53行

2). 复制<localRepository>标签,粘贴到注释的外面(55行)

3). 复制之前新建的用来存储jar包的路径,替换掉<localRepository>标签体内容

 

3、配置阿里云私服

由于中央仓库在国外,所以下载jar包速度可能比较慢,而阿里公司提供了一个远程仓库,里面基本也都有开源项目的jar包。

进入到conf目录下修改settings.xml配置文件:

1). 使用超级记事本软件,打开settings.xml文件,定位到160行左右

2). 在<mirrors>标签下为其添加子标签<mirror>,内容如下:

<mirror>  
    <id>alimaven</id>  
    <name>aliyun maven</name>  
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
    <mirrorOf>central</mirrorOf>          
</mirror>

注意配置的位置,在<mirrors> ... </mirrors> 中间添加配置。如下图所示:

 

==注: 只可配置一个<mirror>(另一个要注释!) ,不然两个可能发生冲突,导致jar包无法下载!!!!!!!==

4、配置环境变量

Maven环境变量的配置类似于JDK环境变量配置一样

1). 在系统变量处新建一个变量MAVEN_HOME

  • MAVEN_HOME环境变量的值,设置为maven的解压安装目录

2). 在Path中进行配置

  • PATH环境变量的值,设置为:%MAVEN_HOME%\bin

3). 打开DOS命令提示符进行验证,出现如图所示表示安装成功

3.IDEA集成Maven

3.1 配置Maven环境

3.1.1 当前工程设置

1、选择 IDEA中 File => Settings => Build,Execution,Deployment => Build Tools => Maven

2、设置IDEA使用本地安装的Maven,并修改配置文件及本地仓库路径

Maven home path :指定当前Maven的安装目录

User settings file :指定当前Maven的settings.xml配置文件的存放路径

Local repository :指定Maven的本地仓库的路径 (如果指定了settings.xml, 这个目录会自动读取出来, 可以不用手动指定)

3、配置工程的编译版本为11

  • Maven默认使用的编译版本为5(版本过低)

 

上述配置的maven环境,只是针对于当前工程的,如果我们再创建一个project,又恢复成默认的配置了。 要解决这个问题, 我们就需要配置全局的maven环境。

3.1.2 全局设置

1、进入到IDEA欢迎页面

  • 选择 IDEA中 File => close project

 

 2、打开 All settings , 选择 Build,Execution,Deployment => Build Tools => Maven 3、配置工程的编译版本为11 这里所设置的maven的环境信息,并未指定任何一个project,此时设置的信息就属于全局配置信息。 以后,我们再创建project,默认就是使用我们全局配置的信息。

3.2 Maven项目

3.2.1 创建Maven项目

1、创建一个空项目

2、创建模块,选择Maven,点击Next  

3、填写模块名称,坐标信息,点击finish,创建完成

4、在Maven工程下,创建HelloWorld类  

  • Maven项目的目录结构:

    maven-project01

            |--- src (源代码目录和测试代码目录)

                    |--- main (源代码目录)

                            |--- java (源代码java文件目录)

                            |--- resources (源代码配置文件目录)

                    |--- test (测试代码目录)

                            |--- java (测试代码java目录)

                            |--- resources (测试代码配置文件目录)

            |--- target (编译、打包生成文件存放目录)

5、编写 HelloWorld,并运行  

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello Maven ...");
    }
}
3.2.2 POM配置详解

POM (Project Object Model) :指的是项目对象模型,用来描述当前的maven项目。

  • 使用pom.xml文件来实现

pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- POM模型版本 -->
    <modelVersion>4.0.0</modelVersion>

    <!-- 当前项目坐标 -->
    <groupId>com.itheima</groupId>
    <artifactId>maven_project1</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <!-- 打包方式 -->
    <packaging>jar</packaging>
 
</project>

pom文件详解:

  • <project> :pom文件的根标签,表示当前maven项目

  • <modelVersion> :声明项目描述遵循哪一个POM模型版本

    • 虽然模型本身的版本很少改变,但它仍然是必不可少的。目前POM模型版本是4.0.0

  • 坐标 :<groupId>、<artifactId>、<version>

    • 定位项目在本地仓库中的位置,由以上三个标签组成一个坐标

  • <packaging> :maven项目的打包方式,通常设置为jar或war(默认值:jar)

3.2.3 Maven坐标详解

什么是坐标?

  • Maven中的坐标是==资源的唯一标识== , 通过该坐标可以唯一定位资源位置

  • 使用坐标来定义项目或引入项目中需要的依赖

Maven坐标主要组成

  • groupId:定义当前Maven项目隶属组织名称(通常是域名反写,例如:com.itheima)

  • artifactId:定义当前Maven项目名称(通常是模块名称,例如 order-service、goods-service)

  • version:定义当前项目版本号

如下图就是使用坐标表示一个项目:

注意:

  • 上面所说的资源可以是插件、依赖、当前项目。

  • 我们的项目如果被其他的项目依赖时,也是需要坐标来引入的。

3.3 导入Maven项目

  • 方式1:使用Maven面板,快速导入项目

打开IDEA,选择右侧Maven面板,点击 + 号,选中对应项目的pom.xml文件,双击即可

说明:如果没有Maven面板,选择 View => Appearance => Tool Window Bars

  • 方式2:使用idea导入模块项目

File => Project Structure => Modules => + => Import Module

找到要导入工程的pom.xml

4.依赖管理

01 依赖配置

  • 依赖:指当前项目运行所需要的jar包.一个项目中可以引入多个依赖

<dependencies>
    <!-- 第1个依赖 : logback -->
    <dependency><!-- 引入坐标 -->
        <!-- 定义坐标的groupId,artifactId,version -->
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.11</version>
    </dependency>
    <!-- 第2个依赖 : junit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

<!-- 点击刷新按钮,引入最新加入的坐标 -->

  • 注意事项:

    1. 如果引入的依赖,在本地仓库中不存在,将会连接远程仓库 / 中央仓库,然后下载依赖(这个过程会比较耗时,耐心等待)

    2. 如果不知道依赖的坐标信息,可以到mvn的中央仓库(https://mvnrepository.com/)中搜索

02 依赖传递

001 依赖具有传递性

使用了maven,当项目中需要使用logback-classic时,只需要在pom.xml配置文件中,添加logback-classic的依赖坐标即可。

在pom.xml文件中只添加了logback-classic依赖,但由于maven的依赖具有传递性,所以会自动把所依赖的其他jar包也一起导入。

  • 依赖传递可以分为:

    • 直接依赖: 当前项目中通过依赖配置建立的依赖关系

    • 间接依赖: 被依赖的资源如果依赖其他资源,当前项目间接依赖其他资源

  • 如上图中:

    • 直接依赖: projectB和第一个jar包就是projectA的直接依赖

    • 间接依赖: projectC以及其他jar对于projectA而言就是间接依赖

002 依赖排除
  • 依赖排除: 指主动断开依赖的资源.(被排除的资源无需指定版本)

<dependency>
    <groupId>com.itheima</groupId>
    <artifactId>maven-projectB</artifactId>
    <version>1.0-SNAPSHOT</version>
   
    <!--排除依赖, 主动断开依赖的资源-->
    <exclusions>
    	<exclusion>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </exclusion>
    </exclusions>
</dependency>

依赖排除示例:

  • maven-projectA依赖了maven-projectB,maven-projectB依赖了Junit。基于依赖的传递性,所以maven-projectA也依赖了Junit

  • 使用排除依赖后

 

03 依赖范围

  • 在项目中导入依赖的jar包后,默认情况下,可以在任何地方使用。

  • 作用范围:

    1. 主程序范围有效(main文件夹范围内)

    2. 测试程序范围有效(test文件夹范围内)

    3. 是否参与打包运行(package指令范围内)

provided: 编译通过,运行不通过

04 生命周期

  • Maven的生命周期就是为了对所有的maven项目构建过程进行抽象和统一。

  • Maven中有3套相互独立的生命周期:

    • clean:清理工作。

    • default:核心工作,如:编译、测试、打包、安装、部署等。

    • site:生成报告、发布站点等。

重点掌握

•clean:移除上一次构建生成的文件

•compile:编译项目源代码

•test:使用合适的单元测试框架运行测试(junit)

•package:将编译后的文件打包,如:jar、war等

•install:安装项目到本地仓库

注意: 在同一套生命周期中,当运行后面的阶段时,前面的阶段都会运行。

二.SpringBoot

  • 通过springboot就可以快速的帮我们构建应用程序,所以springboot呢,最大的特点有两个 :

    • 简化配置

    • 快速开发

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

Spring发展到今天已经形成了一种开发生态圈,Spring提供了若干个子项目,每个项目用于完成特定的功能。而我们在项目开发时,一般会偏向于选择这一套spring家族的技术,来解决对应领域的问题,那我们称这一套技术为spring全家桶

1.SpringBoot快速入门

1.1 需求

需求:基于SpringBoot的方式开发一个web应用,浏览器发起请求/hello后,给浏览器返回字符串 “Hello World ~”。

1.2 步骤

1.2.1 创建springboot工程
  • 基于Spring官方骨架,创建SpringBoot工程。

  • 基本信息描述完毕之后,勾选web开发相关依赖。

点击Finish之后,就会联网创建这个SpringBoot工程,创建好之后,结构如下:

  • ==注意:在联网创建过程中,会下载相关资源(请耐心等待)==

1.2.2 定义请求处理类
  • 在com.itheima这个包下创建一个子包controller

  • 然后在controller包下新建一个类:HelloController

package com.xh.controller;
import org.springframework.web.bind.annotation.*;

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        System.out.println("Hello World ~");
        return "Hello World ~";
    }
    
}   
1.2.3 运行测试

运行SpringBoot自动生成的引导类

 打开浏览器,输入 http://localhost:8080/hello

注意:

@RequestMapping后面才是跟着访问的路径,且在类的上面也可以加注解

1.3 Web分析

浏览器:

  • 输入网址:http://192.168.100.11:8080/hello

    • 通过IP地址192.168.100.11定位到网络上的一台计算机

      我们之前在浏览器中输入的localhost,就是127.0.0.1(本机)

    • 通过端口号8080找到计算机上运行的程序

      localhost:8080 , 意思是在本地计算机中找到正在运行的8080端口的程序

    • /hello是请求资源位置

      • 资源:对计算机而言资源就是数据

        • web资源:通过网络可以访问到的资源(通常是指存放在服务器上的数据)

      localhost:8080/hello ,意思是向本地计算机中的8080端口程序,获取资源位置是/hello的数据

      • 8080端口程序,在服务器找/hello位置的资源数据,发给浏览器

服务器:(可以理解为ServerSocket)

  • 接收到浏览器发送的信息(如:/hello)

  • 在服务器上找到/hello的资源

  • 把资源发送给浏览器

我们在JavaSE阶段学习网络编程时,有讲过网络三要素:

  • IP :网络中计算机的唯一标识

  • 端口 :计算机中运行程序的唯一标识

  • 协议 :网络中计算机之间交互的规则

问题:浏览器和服务器两端进行数据交互,使用什么协议?

答案:http协议

2.HTTP协议

01 介绍

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

  • 特点

    • 基于TCP协议: 面向连接,安全

    • 基于请求-响应模型的:一次请求对应一次响应

    • HTTP协议是无状态的协议:对于事务处理没有记忆能力。每次请求-响应都是独立的。

      •缺点:多次请求间不能共享数据。

      •优点:速度快

02 HTTP-请求数据的格式

  • 请求行(第一行)

  • 请求头(格式key; value)

  • 请求体(get请求没有请求体)

常见请求头

  • 面试题

    • 请求方式-GET: 请求参数在请求行中,没有请求体,如:/brand/findAll?name=OPPO&status=1。GET请求大小是有限制的。

    • 请求方式-POST: 请求参数在请求体中,POST请求大小是没有限制的。

03 HTTP-响应协议

  • 响应行(第一行)

  • 响应头

  • 响应体

  • 状态码和常见响应头

3.Maven改造成SpringBoot项目

修改pom.xml文件

    <!--springboot版本-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <!--java版本-->
    <properties>
        <java.version>11</java.version>
    </properties>
    <!--启动依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

4.WEB服务器-Tomcat(SpringBoot已经内置了Tomcat)

如何访问?

01 简介

服务器硬件
  • 指的是计算机,只不过服务器要比我们日常使用的计算机大很多

服务器,也称伺服器。是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。

服务器的构成包括处理器、硬盘、内存、系统总线等,和通用的计算机架构类似,但是由于需要提供高可靠的服务,因此在处理能力、稳定性、可靠性、安全性、可扩展性、可管理性等方面要求较高。

在网络环境下,根据服务器提供的服务类型不同,可分为:文件服务器,数据库服务器,应用程序服务器,WEB服务器等。

服务器只是一台设备,必须安装服务器软件才能提供相应的服务。

服务器软件

服务器软件: 基于ServerSocket编写的程序

  • 服务器软件本质是一个运行在服务器设备上的应用程序

  • 能够接受客户端请求,并根据请求给客户端响应数据

02 Web服务器

Web服务器是一个应用程序(软件),对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作(不用程序员自己写代码去解析http协议规则),让Web开发更加便捷。主要功能是"提供网上信息浏览服务"。

Web服务器是安装在服务器端的一款软件,将来我们把自己写的Web项目部署到Tomcat服务器软件中,当Web服务器软件启动后,部署在Web服务器软件中的页面就可以直接通过浏览器来访问了。

使用步骤(了解)
  • 准备静态资源

  • 下载安装Web服务器软件

  • 将静态资源部署到Web服务器上

  • 启动Web服务器使用浏览器访问对应的资源

第1步:准备静态资源

  • 在提供的资料中找到静态资源文件

 第2步:下载安装Web服务器软件

第3步:将静态资源部署到Web服务器上  第4步:启动Web服务器使用浏览器访问对应的资源 浏览器输入:http://localhost:8080/demo/index.html

上述内容在演示的时候,使用的是Apache下的Tomcat软件,至于Tomcat软件如何使用,后面会详细的讲到。而对于Web服务器来说,实现的方案有很多,Tomcat只是其中的一种,而除了Tomcat以外,还有很多优秀的Web服务器,比如:

Tomcat就是一款软件,我们主要是以学习如何去使用为主。具体我们会从以下这些方向去学习:

  1. 简介:初步认识下Tomcat

  2. 基本使用: 安装、卸载、启动、关闭、配置和项目部署,这些都是对Tomcat的基本操作

  3. IDEA中如何创建Maven Web项目

  4. IDEA中如何使用Tomcat,后面这两个都是我们以后开发经常会用到的方式

5.SpringBootWeb请求响应

01 请求

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

    Postman原是Chrome浏览器的插件,可以模拟浏览器向后端服务器发起任何形式(如:get、post)的HTTP请求

    使用Postman还可以在发起请求时,携带一些请求参数、请求头等信息

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

  • 特征

    • 简单

    • 实用

    • 美观

    • 大方

Postman使用步骤

双击资料中提供的Postman-win64-8.3.1-Setup.exe即可自动安装。

安装完成之后,进入页面中会提示有新版本可以升级(无需升级)

 界面介绍: 如果我们需要将测试的请求信息保存下来,就需要创建一个postman的账号,然后登录之后才可以。

登录完成之后,可以创建工作空间:

创建请求:

点击"Save",保存当前请求

002 简单参数
  • 1.原始方式(了解)
@RequestMapping(“/xh”)
public String xh(HttpServletRequest request){
    String name = request.getParameter("name");
    String age = request.getParameter("age");
    System.out.println(name + "-----" + age);
    return "ok";
}
2.SpringBoot方式

@RequestMapping(“/xh”)
    //参数名与形参变量名要一致,否则不能接受数据
public String xh(String name, Integer age){
    System.out.println(name + "-----" + age);
    return "ok";
}
 3.参数名不一致(@RequestParam)
@RestController
public class RequestController {
    // http://localhost:8080/simpleParam?name=Tom&age=20
    // 请求参数名:name

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

//注意对注解不做任何改变,会将参数变成必传参数。
//解决办法
@RequestParam(value = "name",required = false )
RequestParam(value = "name", defaultValue = “ ”)

注意:

@RequestParam中的required属性默认为true,代表该请求参数必须传递,如果不传递将报错。 如果该参数是可选的,可以将required属性设置为false

@RequestParam(value = "name", required = false)
//或者
@RequestParam(value = "name", defaultValue = “xxx”)
003 实体参数

简单实体对象:请求参数名与形参对象属性名相同,定义POJO(Palin Ordinary Java Object 普通java对象)接收即可

@RequestMapping("/simplePojo")
public String simplePojo(User user){
    System.out.println(user);    
    return "OK";
}


public class User {
    private String name;
    private Integer age;
}

复杂对象:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套POJO属性参数.

@RequestMapping("/complexPojo")
public String complexPojo(User user){
    System.out.println(user);
    return "OK";
}

public class User {
    private String name;
    private Integer age;
    private Address address;
}

public class Address {
    private String province;
    private String city;
}

例如上述中,User对象中有一个Address(Address为一个实体类)类型的属性(这种就是复杂对象)

在请求时的参数传递格式如下(Postman):

004 数组集合参数
0001 数组

请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数

在前端传输数组数据时,有两种传输方式(两种方式都可以被java的数组和集合所接受)

方式一: xxxxxxxxxx?hobby=game&hobby=java

 方式二:xxxxxxxxxxxxx?hobby=game,java

0002 集合

1.请求参数名与形参集合对象名要相同且请求为多个

2.需要在形参前面加上@RequestParam注解,绑定参数关系

005 日期参数

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

  • @DateTimeFormat注解的pattern属性中指定了哪种日期格式,前端的日期参数就必须按照指定的格式传递。

  • 后端controller方法中,需要使用Date类型或LocalDateTime类型,来封装传递的参数。

注意:

如果时间属性被封装为一个对象的属性,此时就可以将@DateTimeFormat注解加在属性前面

public class User{
    @DateTimeFormat(yyyy-MM-dd HH:mm:ss)
    private LocalDateTime upDateTime;
}
006 JOSN参数

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

007 路径参数

路径参数:

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

 传递多个路径

@RestController
public class RequestController {
    //路径参数
    @RequestMapping("/path/{id}/{name}")
    public String pathParam2(@PathVariable Integer id, @PathVariable String name){
        System.out.println(id+ " : " +name);
        return "OK";
    }
}

02 响应

001 @ResponseBody注解
  • 类型:方法注解、类注解

  • 位置:书写在Controller方法上或类上

  • 作用:将方法返回值直接响应给浏览器

    • 如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器

一般我们不会直接写该注解,因为在类上加的@RestController注解是一个组合注解,内部已经有 @ResponseBody注解

@RestController注解原码

@Target({ElementType.TYPE})   //元注解(修饰注解的注解)
@Retention(RetentionPolicy.RUNTIME)  //元注解
@Documented    //元注解
@Controller   
@ResponseBody 
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}
  • 类上有@RestController注解或@ResponseBody注解时:表示当前类下所有的方法返回值做为响应数据

    • 方法的返回值,如果是一个POJO对象或集合时,会先转换为JSON格式,在响应给浏览器

002 统一响应结果

在真实的项目开发中,无论是哪种方法,我们都会定义一个统一的返回结果。方案如下:

前端:只需要按照统一格式的返回结果进行解析(仅一种解析方案),就可以拿到数据。

各个参数的含义:

1)响应状态码(code):当前请求是否成功

2)状态码信息(msg):给页面的提示信息

3)返回的数据: 给前端响应的数据(字符串,对象,集合...)

总结

03 分层解耦

001 三层架构

在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。

单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。

这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。

我们之前开发的程序呢,并不满足单一职责原则。下面我们来分析下之前的程序:

那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:

  • 数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。

  • 逻辑处理:负责业务逻辑处理的代码。

  • 请求处理、响应数据:负责,接收页面的请求,给页面响应数据。

按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层:

- Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
- Service:业务逻辑层。处理具体的业务逻辑。
- Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。

基于三层架构的程序执行流程:

- 前端发起的请求,由Controller层接收(Controller响应数据给前端)
- Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
- Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
- Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)

> 思考:按照三层架构的思想,如果要对业务逻辑(Service层)进行变更,会影响到Controller层和Dao层吗? 
>
> 答案:不会影响。 (程序的扩展性、维护性变得更好了)

002 分层解耦
1)耦合问题
  • 内聚:软件中各个功能模块内部的功能联系。

  • 耦合:衡量软件中各个层/模块之间的依赖、关联的程度。

软件设计原则:高内聚、低耦合

  • 高内聚:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句,程序段)之间的联系成都越高,贼内聚性越高(可以简单理解为,每个模块应当只做一件事(专注于某一个功能))

  • 低耦合:软件中各个层、模块之间的依赖关系程序越低越好

程序中高内聚的体现:

- EmpServiceA类中只编写了和员工相关的逻辑处理代码

 2)解耦思路

层与层之间的高耦合,使其中某一层的改变导致全局的出错.

  • 控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。

    对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器

  • 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。

    程序运行时需要某个资源,此时容器就为其提供这个资源。

    例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象

IOC容器中创建、管理的对象,称之为:bean对象

003 IOC&DI
1)IOC&DI入门

任务:完成Controller层、Service层、Dao层的代码解耦

第1步:删除Controller层、Service层中new对象的代码

第2步:Service层及Dao层的实现类,交给IOC容器管理

- 使用Spring提供的注解:@Component ,就可以实现类交给IOC容器管理

第3步:为Controller及Service注入运行时依赖的对象

- 使用Spring提供的注解:@Autowired ,就可以实现程序运行时IOC容器自动注入需要的依赖对象

其中@Component注解给类上,使其能在服务器运行时就创建出一个实体放入Spring的IOC容器中

@Autowired注解,放入要依赖的变量上,在需要时Spring会帮助我们自动给其赋值

2)IOC详情

bean声明

IOC控制反转:就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象.IOC容器创建的对象被称为bean对象

为了更好的管理项目,处理@Component注解外,Spring还为我们提供其他衍生注解

        @controller/@RestController注解(一般在控制层类上)

        @Service        (一般在业务层类上)

        @Repository   (一般在数据访问层类上)

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

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

在IOC容器中,每一个Bean都有一个属于自己的名字,可以通过注解的value属性指定bean的名字。如果没有指定,默认为类名首字母小写。 

注意事项: 

- 声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
- 使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。

组件扫描

注解声明的bean要想生效必须要被组件扫描

以下为错误展示

运行程序后,报错:  使用四大注解声明的bean,要想生效,还需要被组件扫描注解@ComponentScan扫描

        需要注意的是@ComponentScan虽然没有明确写出,但其包含在@SpringBootApplication中,其默认的扫描范围是SpringBoot启动类所在的包及其子包

解决方法

        1. 手动添加@ComponentScan注解,指定要扫描的包   (==仅做了解,不推荐==)

推荐做法(如下图):

- 将我们定义的controller,service,dao这些包呢,都放在引导类所在包com.itheima的子包下,这样我们定义的bean就会被自动的扫描到

3)DI详解

在入门程序案例中,我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。

@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)

> 入门程序举例:在EmpController运行的时候,就要到IOC容器当中去查找EmpService这个类型的对象,而我们的IOC容器中刚好有一个EmpService这个类型的对象,所以就找到了这个类型的对象完成注入操作。

但如果存在多个相同类型bean对象,那么程序就会出错

Spring提供了以下几种解决方案:

- @Primary

- @Qualifier

- @Resource

使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。

使用@Qualifier注解: 指定当前要注入的bean对象.在@Qualifier的value属性中,指定注入的bean的名称.

        @Qualifier注解不能单独使用,必须配合@Autowired使用

                注意:@Qualifier中写的bean名称可以指定(在@Component(value = "名称")及其衍生注解),如果未指定则是类名首字母小写模式

使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。  

面试题 : @Autowird 与 @Resource的区别

- @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
- @Autowired 默认是按照类型注入,而@Resource是按照名称注入

6.注解汇总

@RestController

@Component衍生注解,内置了@Component、@ResponseBody(一般标注控制层)

启动类启动时,就会扫描到这个注解,创建对象,放入IOC容器中(原因是有@Component注解)

@RequestMapping

指定访问路径的,通常用于类上和方法上,分别代表一级和二级路径

@RequestParam

1.为参数起别名(别名映射)

2.接受List(及集合)参数时用到的注解

3.默认属性required = true.若不改变,则表明该注解的参数必须有值传入,不传报错400

4.如果不想改变required = true,也可以用defaultValue传入一个默认值,防止报错

@RequestBody

接受请求体的内容,get请求没有请求体,通常接受post等非get请求的请求体,通常接受json格式数据

@PathVariable

路径参数映射到方法的形参上(路径参数所需要的注解)

@ResponseBody

将return的数据响应给浏览器,如果是对象,帮我们自动转成json格式在做响应(一般不需要我们手动写,在控制层写的@RestController注解就内置了)

@Primary

如果同类型下有多个bean对象,可以使用该注解声明谁的优先级最高

@Autowired

依赖注入,默认按照类型去容器中匹配完成注入.如果同类型bean对象有多个,则报错

@Qualifier

为了解决多个同类型bean对象的报错,根据名字陪陪一个容器中的对象.需要配合@Autowired使用

@Resource

直接根据名字去容器匹配对象,属于jdk提供的,jdk1.8之后不在提供。(慎用)

@Component

声明当前类会被框架创建bean对象,IOC注解(控制反转)

其衍生注解@Controller        @Service        @Repository

三.MySQL

1.MySQL概述

1.1简述数据库

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

数据库管理系统:DataBase Management System,简称DBMS),一般我们简称为数据库

        DBMS是操作和管理数据库的大型软件。将来我们只需要操作这个软件,就可以通过这个软件来操纵和管理数据库了。

        给DBMS软件发送一条指令,告诉这个软件我们要执行的是什么样的操作,要对哪个数据进行操作。而这个指令就是SQL语句

SQL:(Structured Query Language) 结构化查询语言,它是操作关系型数据库的编程语言,定义了一套操作关系型数据库的统一标准。我们学习数据库开发,最为重要的就是学习SQL语句 。

以下是主流的数据库(数据库管理系统):

  • Oracle:大型的收费数据库,Oracle公司产品,价格昂贵。(通常是不差钱的公司会选择使用这个数据库)

  • MySQL:开源免费的中小型数据库,后来Sun公司收购了MySQL,而Oracle又收购了Sun公司。目前Oracle推出两个版本的Mysql:社区版(开源免费)、商业版(收费)。

  • SQL Server:Microsoft 公司推出的收费的中型数据库,C#、.net等语言常用。

  • PostgreSQL:开源免费的中小型数据库。

  • DB2:IBM公司的大型收费数据库产品。

  • SQLLite:嵌入式的微型数据库。Android内置的数据库采用的就是该数据库。

  • MariaDB:开源免费的中小型数据库。是MySQL数据库的另外一个分支、另外一个衍生产品,与MySQL数据库有很好的兼容性。

以上的数据库都是关系型数据库,我们只要学会SQL语句就可以通过SQL语句来操作Mysql,也可以通过SQL语句来操作Oracle或SQL Server.

1.2安装

        点开下面的链接:https://dev.mysql.com/downloads/mysql/

        将下载好的压缩包解压在想解压的位置(注意这个解压位置就是我们数据库的位置)

配置环境变量

        和java环境变量配置类似(目的只是在黑窗口cmd中可以方便操作)

验证是否成功

        右键开始菜单(就是屏幕左下角),选择命令提示符(管理员),打开黑框,敲入mysql,回车。

        如果提示`Can't connect to MySQL server on 'localhost'`则证明添加成功;

        如果提示`mysql不是内部或外部命令,也不是可运行的程序或批处理文件`则表示添加添加失败,请重新检查步骤并重试。(一般是环境变量设置失败或错误)

初始化MySQL

        以管理员身份,运行命令行窗口:

        在刚才的命令行中,输入如下的指令: 

mysqld --initialize-insecure

        稍微等待一会,如果出现没有出现报错信息,则证明data目录初始化没有问题,此时再查看MySQL目录下已经有data目录生成。

        如出现一下错误,是由于权限不足导致的,以管理员方式运行 cmd

        

注册MySQL服务

       命令行(注意必须以管理员身份启动)中,输入如下的指令,回车执行: 

mysqld -install

启动MySQL

        在黑框里敲入`net start mysql`,回车

net start mysql          // 启动mysql服务
    
net stop mysql          // 停止mysql服务

 

修改默认账户密码

        在黑框里敲入`mysqladmin -u root password 1234`,这里的`1234`就是指默认管理员(即root账户)的密码,可以自行修改成你喜欢的

 mysqladmin -u root password 1234

登陆MySQL

        右键开始菜单,选择`命令提示符`,打开黑框。
在黑框中输入,`mysql -uroot -p1234`,回车,出现下图且左下角为`mysql>`,则登录成功。

 mysql -uroot -p1234

 

        退出mysql:
                exit
                quit

        登陆参数:
                mysql -u用户名 -p密码 -h要连接的mysql服务器的ip地址(默认127.0.0.1) -P端口号(默认3306)

 卸载MySQL

net stop mysql

mysqld -remove mysql

        最后删除MySQL目录及相关的环境变量。

        至此,MySQL卸载完成!

1.3数据模型

1)关系数据库

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

而所谓二维表,指的是由行和列组成的表,如下图:

 

        关系则是表与表之间有联系,例如第一张表中的部门编号与第二张表的编号对应起来,两张表之间就有了联系

        二维表的优点:

                使用表存储数据,格式统一,便于维护

                使用SQL语言操作,标准统一,使用方便,可用于复杂查询

我们之前提到的MySQL、Oracle、DB2、SQLServer这些都是属于关系型数据库,里面都是基于二维表存储数据的。

结论:基于二维表存储数据的数据库就成为关系型数据库,不是基于二维表存储数据的数据库,就是非关系型数据库(比如大家后面要学习的Redis,就属于非关系型数据库)。

 2)数据模型

         MySQL是关系型数据库,是基于二维表进行数据存储的,具体的结构图下:

        1.通过MySQL客户端连接数据库管理系统DBMS,然后通过DBMS操作数据库
         2.使用MySQL客户端,向数据库管理系统发送一条SQL语句,由数据库管理系统根据SQL语句指令去操作数据库中的表结构及数据
        3.一个数据库服务器中可以创建多个数据库 

1.4 SQL简介

通用语法规则:

        SQL语句可以单行或多行书写(以分号;作为结尾)

        SQL语句可以使用空格/缩进来增强语句的可读性。

        MySQL数据库的SQL语句不区分大小写。

        注释:  

  • 单行注释:-- 注释内容 或 # 注释内容(MySQL特有)

  • 多行注释: /* 注释内容 */

 分类:

        SQL语句根据其功能被分为四大类:DDL、DML、DQL、DCL

        DDL:定义数据库对象(在图中就是创建数据库,创建表,已经创建字段,字段可以理解是表的一列)

1.5 小结

 

2.数据库操作

2.1 DDL

2.1.1数据库操作

         查询、创建、使用、删除。

 其中:

show databases; 是查看所有已经创建的数据库

use 数据库名; 切换你要操作的数据

select database(); 固定写法,查看当前正在操作的数据库是哪一个

[if not exists] 表示约束,比如这一个就是是否存在

 这些语句不需要去记住,因为有很多图形化工具辅助我们去操作,例如idea里就集成了DataGrip操作

2.1.2表操作

        表创建,与常见约束

        查询:(左边为在控制台的语句,右边是使用图形化操作工具后可以直观感受,不需要在去输入语句)

        修改:

        删除:(注意,表中的数据也会全部删除)

        数据类型:包括三个(数值类型,字符串类型,日期时间类型)

                对于符号在给数值类型确定范围时,需要指定.默认是signed(有字符,及可以取负值)

double(4,1):整个数字长度为4,保留1为小数,其最大取值为: 999.9

示例:

create table tb_emp (
  id int unsigned primary key auto_increment comment 'ID',
  username varchar(20) not null unique comment '用户名',
  password varchar(32) default '123456' comment '密码',
  name varchar(10) not null comment '姓名',
  gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
  image varchar(300) comment '图像',
  job tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管',
  entrydate date comment '入职时间',
  create_time datetime not null comment '创建时间',
  update_time datetime not null comment '修改时间'
) comment '员工表';

 2.2 DML

        Data Manipulation Language(数据操作   语言),

用来对数据库中表的数据记录进行增、删、改操作。

  • 添加数据(INSERT)

  • 修改数据(UPDATE)

  • 删除数据(DELETE)

2.2.1 insert:增加

        SQL有一个函数now()可以获得当前时间

        

        注意:

                1.插入数据时,指定的字段顺序需要和值的顺序是一一对应的

                2.字符串和日期类型数据应该包含在引号中(单引号)

                3.插入的数据大小,应该在字段的规定范围内

        示例:

-- 向tb_emp表的username、name、gender字段插入数据
-- 因为设计表时create_time, update_time两个字段不能为NULL,所以也做为要插入的字段
insert into tb_emp(username, name, gender, create_time, update_time)
values ('wuji', '张无忌', 1, now(), now());


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

-- 批量向tb_emp表的username、name、gender字段插入数据
insert into tb_emp(username, name, gender, create_time, update_time)
values ('weifuwang', '韦一笑', 1, now(), now()),
       ('fengzi', '张三疯', 1, now(), now());

2.2.2 update:修改数据

        

        注意:

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

                在修改数据时,一般需要同时修改公共字段update_time,将其修改为当前操作时间.

-- 将tb_emp表中id为1的员工,姓名name字段更新为'张三'
update tb_emp set name='张三',update_time=now() where id=1;

-- 将tb_emp表的所有员工入职日期更新为'2010-01-01'
update tb_emp set entrydate='2010-01-01',update_time=now();

2.2.3 delete:删除

        注意:

                1.delete语句的条件可以有,也可以没有,如果没有条件,则会删除整张表的所有数据.

                2.delete语句不能删除某一个字段的值(如果要操作,可以使用update(DDL语句,update,将该字段的值为null)) 

2.3 DQL

2.3.1 介绍

        Data Query Language:(数据查询语言),用来查询数据库表中的记录。

        语句分为以下基类

 2.3.2 基本查询

        

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

 *号是通配符 代表查询所有字段,在实际开发中尽量少用(不直观、影响效率)

-- 查询返回所有字段
select * from tb_emp;

 

-- 查询所有员工的 name,entrydate,并起别名(姓名、入职日期)
-- 方式1:
select name AS 姓名, entrydate AS 入职日期 from tb_emp;
-- 方式2: 别名中有特殊字符时,使用''或""包含
select name AS '姓 名', entrydate AS '入职日期' from tb_emp;
-- 方式3:
select name AS "姓名", entrydate AS "入职日期" from tb_emp;

 

        注意:其别名的时候,可以不加引号,同时as是可以省略的 

-- 查询已有的员工关联了哪几种职位(不要重复)
select distinct job from tb_emp;

-- 查询所有姓名和职位,如果职位为空就显示'未分配职位'
-- ifnull() 函数,判断是否为空,如果为空就显示参数里的内容
select name, ifnull(job,'未分配职位') from tb_emp;

 

 2.3.3 条件查询

        语法与常见运算符

         简单逻辑(=)

-- 查询 姓名 为 杨逍 的员工
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where name = '杨逍'; -- 字符串使用''或""包含

 

        简单逻辑(<=)

-- 查询 id小于等于5 的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where id <=5;

        is null(is not null):见名之意,判断是否为null

-- 查询 没有分配职位 的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where job is null ;

-- 查询 有职位 的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where job is not null ;

         

-- 查询 密码不等于 '123456' 的员工信息
-- 方式1:
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where password <> '123456';
-- 方式2:
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where password != '123456';

-- 查询 入职日期 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间的员工信息
-- 方式1:
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where entrydate>='2000-01-01' and entrydate<='2010-01-01';
-- 方式2: between...and
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where entrydate between '2000-01-01' and '2010-01-01';

-- 查询 入职时间 在 '2000-01-01' (包含) 到 '2010-01-01'(包含) 之间 且 性别为女 的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where entrydate between '2000-01-01' and '2010-01-01'
      and gender = 2;

-- 查询 职位是 2 (讲师), 3 (学工主管), 4 (教研主管) 的员工信息
-- 方式1:使用or连接多个条件
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where job=2 or job=3 or job=4;
-- 方式2:in关键字
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where job in (2,3,4);

        like(模糊查询,常与通配符配合''_'   '%' )

-- 查询 姓名 为两个字的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where name like '__';  # 通配符 "_" 代表任意1个字符


-- 查询 姓 '张' 的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where name like '张%';

2.3.4 聚合函数

        使用聚合函数查询就是纵向查询,它是对一列的值进行计算,然后返回一个结果值。(将一列数据作为一个整体,进行纵向计算)

        语法

select  聚合函数(字段列表) from  表名 ;

注意 : 聚合函数会忽略空值,对NULL值不作为统计

         常见聚合函数

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

count :按照列去统计有多少行数据。

        - 在根据指定的列统计的时候,如果这一列中有null的行,该行不会被统计在其中。

sum :计算指定列的数值和,如果不是数值类型,那么计算结果为0

max :计算指定列的最大值

min :计算指定列的最小值

avg :计算指定列的平均值

         

        案例(count函数,计数)

-- 统计该企业员工数量
# count(字段)
select count(id) from tb_emp;-- 结果:29
select count(job) from tb_emp;-- 结果:28 (聚合函数对NULL值不做计算)

# count(常量)
select count(0) from tb_emp;
select count('A') from tb_emp;

# count(*)  推荐此写法(MySQL底层进行了优化)
select count(*) from tb_emp;

         案例(min函数,最小值)

-- 统计该企业最早入职的员工的入职时间
select min(entrydate) from tb_emp;

         案例(max函数,最大值)

-- 统计该企业最迟入职的员工
select max(entrydate) from tb_emp;

        案例(avg函数,求平均值)

-- 统计该企业员工 ID 的平均值
select avg(id) from tb_emp;

        案例(sum函数,求和)

-- 统计该企业员工的 ID 之和
select sum(id) from tb_emp;
 2.3.5 分组查询    

        分组: 按照某一列或者某几列,把相同的数据进行合并输出。

                分组其实就是按列进行分类(指定列下相同的数据归为一类),然后可以对分类完的数据进行合并计算。
                分组查询通常会使用聚合函数进行计算。

        where与having 的区别:

                1.执行时机不同:where是分组之前进行过滤,不满足where条件,不参与分组;having是对分组过后的结果进行过滤

                2.判断掉件不同: where不能对聚合函数进行判断,而having可以

        注意:

                1.分组以后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义(且多数情况下会报错).

                2.执行顺序: where > 聚合函数 > having

        案例汇总

-- 根据性别分组 , 统计男性和女性员工的数量
select gender, count(*)
from tb_emp
group by gender; -- 按照gender字段进行分组(gender字段下相同的数据归为一组)


-- 查询入职时间在 '2015-01-01' (包含) 以前的员工 , 并对结果根据职位分组 , 获取员工数量大于等于2的职位
select job, count(*)
from tb_emp
where entrydate <= '2015-01-01'   -- 分组前条件
group by job                      -- 按照job字段分组
having count(*) >= 2;             -- 分组后条件

 2.3.6 排序查询

        对查询的数据进行排序

                ASC :升序(默认值)

                DESC:降序

         如果是多个字段排序,那么只有在一个字段排序相同时,才会执行第二个字段的排序

        

       案例汇总

-- 根据入职时间, 对员工进行升序排序
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
order by entrydate ASC; -- 按照entrydate字段下的数据进行升序排序

select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
order by  entrydate; -- 默认就是ASC(升序)


-- 根据入职时间,对员工进行降序排序
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
order by entrydate DESC; -- 按照entrydate字段下的数据进行降序排序


-- 根据入职时间对公司的员工进行升序排序,入职时间相同,再按照更新时间进行降序排序
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
order by entrydate ASC , update_time DESC;

 

2.3.7 分页查询
 

         注意事项:

                1.起始索引从0开始,起始索引 = (查询页码 - 1) * 每页显示记录数.

                2.分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中LIMIT(也就是说,不同数据库的分页查询的语法不同,MySQL中是limit,其他数据库可能是其他关键字)

                3.如果查询的是第一页数据,起始索引可以省略,直接简写为limit 10

          案例汇总:

-- 查询 第1页 员工数据, 每页展示5条记录
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
limit 5; -- 如果查询的是第1页数据,起始索引可以省略,直接简写为:limit 条数


-- 查询 第3页 员工数据, 每页展示5条记录
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
limit 10 , 5; -- 从索引10开始,向后取5条记录(计算公式 起始索引10 = (页码数3 - 1) * 每页显示数量5 )
 2.3.8 案例

        案例1: 按照图中需求查找要求进行查询(其要根据图中要求进行分页)

        分析:

                1.图中查询条件有员工姓名,员工性别,入职时间区间段

                        姓名:张 (张开头,模糊查询)

                        性别: 男

                        入职时间: 2000-01-01 ~ 2015-12-31(between... and ... )

                2.图中下方还有分页状态,且图中是根据最后操作时间降序排序

-- 根据输入条件查询第1页数据(每页展示10条记录)
-- 输入条件:
   -- 姓名:张 (模糊查询)
   -- 性别:男
   -- 入职时间:2000-01-01 ~ 2015-12-31
-- 分页: 0 , 10
-- 排序: 修改时间  DESC
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where name like '张%' and gender = 1 and entrydate between '2000-01-01' and '2015-12-31'
order by update_time desc
limit 0 , 10;-- 根据输入条件查询第1页数据(每页展示10条记录)
-- 输入条件:
   -- 姓名:张 (模糊查询)
   -- 性别:男
   -- 入职时间:2000-01-01 ~ 2015-12-31
-- 分页: 0 , 10
-- 排序: 修改时间  DESC
select id, username, password, name, gender, image, job, entrydate, create_time, update_time
from tb_emp
where name like '张%' and gender = 1 and entrydate between '2000-01-01' and '2015-12-31'
order by update_time desc
limit 0 , 10;

        案例2: 

  

   分析:以上信息统计在开发中也叫图形报表(将统计好的数据以可视化的形式展示出来)

        员工性别统计:以饼状图的形式展示出企业男性员人数和女性员工人数
                只要查询出男性员工和女性员工各自有多少人就可以了
        员工职位统计:以柱状图的形式展示各职位的在岗人数
                只要查询出各个职位有多少人就可以了

        员工性别统计:此处用到if函数

                if(表达式, tvalue, fvalue) :当表达式为true时,取值tvalue;当表达式为false时,取值fvalue

-- if(条件表达式, true取值 , false取值)
select if(gender=1,'男性员工','女性员工') AS 性别, count(*) AS 人数
from tb_emp
group by gender;

         

        员工职位统计:此处用到case

                case   表达式    when   值1   then  结果1   [when 值2  then  结果2 ...]     [else result]     end

-- case 表达式 when 值1 then 结果1  when 值2  then  结果2 ...  else  result  end
select (case job
             when 1 then '班主任'
             when 2 then '讲师'
             when 3 then '学工主管'
             when 4 then '教研主管'
             else '未分配职位'
        end) AS 职位 ,
       count(*) AS 人数
from tb_emp
group by job;

-- case也有另一种用法,该用法可以加判断,比如大于等
select (case job
             when job = 1 then '班主任'
             when job = 2 then '讲师'
             when job = 3 then '学工主管'
             when job = 4 then '教研主管'
             else '未分配职位'
        end) AS 职位 ,
       count(*) AS 人数
from tb_emp
group by job;

-- 此处也可以用if但代码会很复杂(不推荐)
select if(job = 1,'班主任',if(job = 2, '讲师',if(job = 3,'学工主管',if(job = 4,'教研主管','未分配职位')))) as 职位,count(*) as 人数  from tb_emp group by job;

3. 多表设计

3.1 一对多

        表与表之间的联系

        一对多关系的实现:在数据库中多的一方,添加字段,来关联一的一方的主键(因为一的一方添加多的字段会出现一行出现多个值,这是错误的)

以下提供一部分测试数据

# 建议:创建新的数据库(多表设计存放在新数据库下)
create database db03;
use db03;

-- 部门表
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
(
    id          int unsigned primary key auto_increment comment 'ID',
    username    varchar(20)      not null unique comment '用户名',
    password    varchar(32) default '123456' comment '密码',
    name        varchar(10)      not null comment '姓名',
    gender      tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
    image       varchar(300) comment '图像',
    job         tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管',
    entrydate   date comment '入职时间',
    
    dept_id     int unsigned comment '部门ID', -- 员工的归属部门
    
    create_time datetime         not null comment '创建时间',
    update_time datetime         not null comment '修改时间'
) comment '员工表';



-- 部门表测试数据
insert into tb_dept (id, name, create_time, update_time) values
(1,'学工部',now(),now()),
(2,'教研部',now(),now()),
(3,'咨询部',now(),now()),
(4,'就业部',now(),now()),
(5,'人事部',now(),now());

-- 员工表测试数据
INSERT INTO tb_emp
(id, username, password, name, gender, image, job, entrydate,dept_id, create_time, update_time) VALUES
(1,'jinyong','123456','金庸',1,'1.jpg',4,'2000-01-01',2,now(),now()),
(2,'zhangwuji','123456','张无忌',1,'2.jpg',2,'2015-01-01',2,now(),now()),
(3,'yangxiao','123456','杨逍',1,'3.jpg',2,'2008-05-01',2,now(),now()),
(4,'weiyixiao','123456','韦一笑',1,'4.jpg',2,'2007-01-01',2,now(),now()),
(5,'changyuchun','123456','常遇春',1,'5.jpg',2,'2012-12-05',2,now(),now()),
(6,'xiaozhao','123456','小昭',2,'6.jpg',3,'2013-09-05',1,now(),now()),
(7,'jixiaofu','123456','纪晓芙',2,'7.jpg',1,'2005-08-01',1,now(),now()),
(8,'zhouzhiruo','123456','周芷若',2,'8.jpg',1,'2014-11-09',1,now(),now()),
(9,'dingminjun','123456','丁敏君',2,'9.jpg',1,'2011-03-11',1,now(),now()),
(10,'zhaomin','123456','赵敏',2,'10.jpg',1,'2013-09-05',1,now(),now()),
(11,'luzhangke','123456','鹿杖客',1,'11.jpg',1,'2007-02-01',1,now(),now()),
(12,'hebiweng','123456','鹤笔翁',1,'12.jpg',1,'2008-08-18',1,now(),now()),
(13,'fangdongbai','123456','方东白',1,'13.jpg',2,'2012-11-01',2,now(),now()),
(14,'zhangsanfeng','123456','张三丰',1,'14.jpg',2,'2002-08-01',2,now(),now()),
(15,'yulianzhou','123456','俞莲舟',1,'15.jpg',2,'2011-05-01',2,now(),now()),
(16,'songyuanqiao','123456','宋远桥',1,'16.jpg',2,'2010-01-01',2,now(),now()),
(17,'chenyouliang','123456','陈友谅',1,'17.jpg',NULL,'2015-03-21',NULL,now(),now());

 外键

        外键约束:让两张表的数据建立连接,保证数据的一致性和完整性。  

        对应的关键字:foreign key

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


-- 建完表后,添加外键
alter table  表名  add constraint  外键名称  foreign key(外键字段名) references 主表(主表列名);

 方式1:通过SQL语句操作

-- 修改表: 添加外键约束
alter table tb_emp  
add  constraint  fk_dept_id  foreign key (dept_id)  references  tb_dept(id);

 方式2:图形化界面操作(以idea为例)

物理外键与逻辑外键

  • 物理外键

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

    • 缺点:

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

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

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

  • 逻辑外键

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

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

在现在的企业开发中,很少会使用物理外键(mysql外键),都是使用逻辑外键(java代码)。 甚至在一些数据库开发规范中,会明确指出禁止使用物理外键 foreign key 

3.2 一对一

        一对一就是一张表与另一个表一一对应,通常是用来做单表的拆分,也就是将一张大表拆分成两张小表通常是用来做单表的拆分,也就是将一张大表拆分成两张小表,将大表中的一些基础字段放在一张表当中,将其他的字段放在另外一张表当中,以此来提高数据的操作效率。(原因:在实际数据查询中,很多数据并不是用户所需要的,只要将部分不需要的数据查分出去,减少计算机IO压力)

        一对一的应用场景: 用户表(基本信息+身份信息)

         其实一对一我们可以看成一种特殊的一对多,我们也可以通过外键来体现一对一之间的关系,我们只需要在任意一方来添加一个外键就可以了。

3.3 多对多

多对多的关系在开发中属于也比较常见的。比如:学生和老师的关系,一个学生可以有多个授课老师,一个授课老师也可以有多个学生。在比如:学生和课程的关系,一个学生可以选修多门课程,一个课程也可以供多个学生选修。

案例:学生与课程的关系

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

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

4. 多表查询

4.1 概述

         提供参考数据

-- 部门表
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 '部门表';
-- 部门表测试
insert into tb_dept (id, name, create_time, update_time)
values (1, '学工部', now(), now()),
       (2, '教研部', now(), now()),
       (3, '咨询部', now(), now()),
       (4, '就业部', now(), now()),
       (5, '人事部', now(), now());

-- 员工表
create table tb_emp
(
    id          int unsigned primary key auto_increment comment 'ID',
    username    varchar(20)      not null unique comment '用户名',
    password    varchar(32) default '123456' comment '密码',
    name        varchar(10)      not null comment '姓名',
    gender      tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
    image       varchar(300) comment '图像',
    job         tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
    entrydate   date comment '入职时间',
    dept_id     int unsigned comment '部门ID',
    create_time datetime         not null comment '创建时间',
    update_time datetime         not null comment '修改时间'
) comment '员工表';
-- 员工表测试数据
INSERT INTO tb_emp(id, username, password, name, gender, image, job, entrydate,dept_id, create_time, update_time) 
VALUES 
(1,'jinyong','123456','金庸',1,'1.jpg',4,'2000-01-01',2,now(),now()),
(2,'zhangwuji','123456','张无忌',1,'2.jpg',2,'2015-01-01',2,now(),now()),
(3,'yangxiao','123456','杨逍',1,'3.jpg',2,'2008-05-01',2,now(),now()),
(4,'weiyixiao','123456','韦一笑',1,'4.jpg',2,'2007-01-01',2,now(),now()),
(5,'changyuchun','123456','常遇春',1,'5.jpg',2,'2012-12-05',2,now(),now()),
(6,'xiaozhao','123456','小昭',2,'6.jpg',3,'2013-09-05',1,now(),now()),
(7,'jixiaofu','123456','纪晓芙',2,'7.jpg',1,'2005-08-01',1,now(),now()),
(8,'zhouzhiruo','123456','周芷若',2,'8.jpg',1,'2014-11-09',1,now(),now()),
(9,'dingminjun','123456','丁敏君',2,'9.jpg',1,'2011-03-11',1,now(),now()),
(10,'zhaomin','123456','赵敏',2,'10.jpg',1,'2013-09-05',1,now(),now()),
(11,'luzhangke','123456','鹿杖客',1,'11.jpg',5,'2007-02-01',3,now(),now()),
(12,'hebiweng','123456','鹤笔翁',1,'12.jpg',5,'2008-08-18',3,now(),now()),
(13,'fangdongbai','123456','方东白',1,'13.jpg',5,'2012-11-01',3,now(),now()),
(14,'zhangsanfeng','123456','张三丰',1,'14.jpg',2,'2002-08-01',2,now(),now()),
(15,'yulianzhou','123456','俞莲舟',1,'15.jpg',2,'2011-05-01',2,now(),now()),
(16,'songyuanqiao','123456','宋远桥',1,'16.jpg',2,'2007-01-01',2,now(),now()),
(17,'chenyouliang','123456','陈友谅',1,'17.jpg',NULL,'2015-03-21',NULL,now(),now());

         查询时从多张表中获取所需数据 ,如果直接查询只会出现笛卡尔积

select * from  tb_emp , tb_dept;

此时,我们看到查询结果中包含了大量的结果集,总共85条记录,而这其实就是员工表所有的记录(17行)与部门表所有记录(5行)的所有组合情况,这种现象称之为笛卡尔积。 

        在SQL语句中取出无效笛卡尔积只要加上查询条件即可

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

多表查询可以分为:

  1. 内连接:两张表有交集的数据合并到一起

    • 隐式:select * from 表A , 表B where 条件

    • 显示:select * from 表A join 表B on 表关联条件 where 其他条件

  2. 外连接:主表的全部及从表的部分

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

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

  3. 子查询:sql嵌套

4.2 内连接

        隐式内连接:

select  字段列表   from   表1 , 表2   where  条件 ... ;

        显示内连接:

select  字段列表   from   表1  [ inner ]  join 表2  on  连接条件 where 过滤条件 ;

        案例:

-- 隐式内连接实现
select tb_emp.name , tb_dept.name -- 分别查询两张表中的数据
from tb_emp , tb_dept -- 关联两张表
where tb_emp.dept_id = tb_dept.id; -- 消除笛卡尔积

-- 显式内连接实现
select tb_emp.name , tb_dept.name
from tb_emp inner join tb_dept
on tb_emp.dept_id = tb_dept.id;

4.3 外连接

        左外连接语法结构:

select  字段列表   from   表1  left  [ outer ]  join 表2  on  连接条件 ... ;

        右外连接语法结构:

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

        案例:

-- 左外连接:以left join关键字左边的表为主表,查询主表中所有数据,以及和主表匹配的右边表中的数据
select emp.name , dept.name
from tb_emp AS emp left join tb_dept AS dept 
     on emp.dept_id = dept.id;

-- 右外连接
select dept.name , emp.name
from tb_emp AS emp right join  tb_dept AS dept
     on emp.dept_id = dept.id;

        值得注意的是,左右连接只是写法,本质还是需要和连接表所对应,例如都是左外连接,但在将两张表互换位置后,也能实现右连接的效果.

4.4 子查询 

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

SELECT  *  FROM   t1   WHERE  column1 =  ( SELECT  column1  FROM  t2 ... );

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

        子查询可以书写的位置:

                1. where之后
                2. from之后
                3. select之后

4.4.1 标量子查询

        子查询返回的结果是单个值(数字、字符串、日期等),最简单的形式,这种子查询称为标量子查询。

        案例:

-- 查询在 "方东白" 入职之后的员工信息
-- 1.查询"方东白"的入职日期
select entrydate from tb_emp where name = '方东白';     #查询结果:2012-11-01
-- 2.查询指定入职日期之后入职的员工信息
select * from tb_emp where entrydate > '2012-11-01';

-- 合并以上两条SQL语句
select * from tb_emp where entrydate > (select entrydate from tb_emp where name = '方东白');

 4.4.2 列子查询

        子查询放回的结果是一列的(也称为某个字段),其特点是一列但可以多行

                常见操作符:

                        in : 在指定的集合范围之内,多选一

                        not in : 不在指定的集合范围之内

        案例:

-- 查询"教研部"和"咨询部"的所有员工信息
-- 1.查询"销售部"和"市场部"的部门ID
select id from tb_dept where name = '教研部' or name = '咨询部';    #查询结果:3,2
-- 2.根据部门ID, 查询员工信息
select * from tb_emp where dept_id in (3,2);

-- 合并以上两条SQL语句
select * from tb_emp where dept_id in (select id from tb_dept where name = '教研部' or name = '咨询部');

 

 4.4.3 行子查询

        子查询返回的结果是一行(可以是多列),这种子查询称为行子查询。

        案例:

-- 查询与"韦一笑"的入职日期及职位都相同的员工信息 
-- 查询"韦一笑"的入职日期 及 职位
select entrydate , job from tb_emp where name = '韦一笑';  #查询结果: 2007-01-01 , 2
-- 查询与"韦一笑"的入职日期及职位相同的员工信息
select * from tb_emp where (entrydate,job) = ('2007-01-01',2);

-- 合并以上两条SQL语句
select * from tb_emp where (entrydate,job) = (select entrydate , job from tb_emp where name = '韦一笑');

4.4.4 表子查询

        子查询返回的结果是多行多列(结果就是一种表),常作为临时表,这种子查询称为表子查询

        案例:

-- 查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门信息
select * from emp where entrydate > '2006-01-01';

select e.*, d.* from (select * from emp where entrydate > '2006-01-01') e left join dept d on e.dept_id = d.id ;

 

5. 事务

5.1 概念

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

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

5.2 操作

        1.自动提交事务:即执行一条sql语句提交一次事务。(默认MySQL的事务是自动提交)

        2.手动提交事务:先开启,再提交 

                事务操作有关的SQL语句:

        事务提交有两种情况:

                1.开启事务 -> 执行SQL语句 -> 成功 -> 提交事务.

                2.开启事务 -> 执行SQL语句 -> 失败 -> 回滚事务.

        以下给一个简单的案例

-- 开启事务
start transaction ;

-- 删除学工部
delete from tb_dept where id = 1;

-- 删除学工部的员工
delete from tb_emp where dept_id = 1;

        如果执行成功,则提交事务

-- 提交事务 (成功时执行)
commit ;

         如果如果执行失败,则回滚事务

-- 回滚事务 (出错时执行)
rollback ;

5.3 四大特性

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

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

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

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

 四大特征可以简称为(ACID)

        原子性(Atomicity) :原子性是指事务包装的一组sql是一个不可分割的工作单元,事务中的操作要么全部成功,要么全部失败。

        一致性(Consistency):一个事务完成之后数据都必须处于一致性状态。

​                如果事务成功的完成,那么数据库的所有变化将生效。

​                如果事务执行出现错误,那么数据库的所有变化将会被回滚(撤销),返回到原始状态。

        隔离性(Isolation):多个用户并发的访问数据库时,一个用户的事务不能被其他用户的事务干扰,多个并发的事务之间要相互隔离。

​                一个事务的成功或者失败对于其他的事务是没有影响

        持久性(Durability):一个事务一旦被提交或回滚,它对数据库的改变将是永久性的,哪怕数据库发生异常,重启之后数据亦然存在。

6. 索引

6.1 介绍

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

                简单来讲,就是使用索引可以提高查询的效率。(在大批量数据的查找时,能显著提高效率)

        以下用一个大批量数据做一个示例

        测试没有使用索引的查询:

         添加索引:

-- 添加索引
create index idx_sku_sn on tb_sku (sn);  #在添加索引时,也需要消耗时间

-- 查询数据(使用了索引)
select * from tb_sku where sn = '100000003145008';

 

        优点:

                1. 提高数据查询的效率,降低数据库的IO成本。
                2. 通过索引列对数据进行排序,降低数据排序的成本,降低CPU消耗。

        缺点:

                1. 索引会占用存储空间。
                2. 索引大大提高了查询效率,同时却也降低了insert、update、delete的效率。

6.2 结构

        MySQL数据库支持的索引结构有很多,如:Hash索引、B+Tree索引、Full-Text索引等。

        我们平常所说的索引,如果没有特别指明,都是指默认的 B+Tree 结构组织的索引。

举例:查询方式

        假如要查找45的数据

        1.看根节点,45大于38小于67,所以走中间p2指针

        2.找到下一个节点后,45是大于38小于47所以走左边p1指针

        3.最后根据双向链表格式在找具体数据位置

B+Tree结构:

        1.每一个节点,可以存储多个key(有n个key,就有n个指针)
        2.节点分为:叶子节点、非叶子节点
                叶子节点,就是最后一层子节点,所有的数据都存储在叶子节点上
                非叶子节点,不是树结构最下面的节点,用于索引数据,存储的的是:key+指针
        3.为了提高范围查询效率,叶子节点形成了一个双向链表,便于数据的排序及区间范围查询

拓展:

        1.非叶子节点都是由key+指针域组成的,一个key占8字节,一个指针占6字节,而一个节点总共容量是16KB,那么可以计算出一个节点可以存储的元素个数:16*1024字节 / (8+6)=1170个元素。

                查看mysql索引节点大小:show global status like 'innodb_page_size';    -- 节点大小:16384

        2.当根节点中可以存储1170个元素,那么根据每个元素的地址值又会找到下面的子节点,每个子节点也会存储1170个元素,那么第二层即第二次IO的时候就会找到数据大概是:1170*1170=136W。也就是说B+Tree数据结构中只需要经历两次磁盘IO就可以找到136W条数据。

        3.对于第二层每个元素有指针,那么会找到第三层,第三层由key+数据组成,假设key+数据总大小是1KB,而每个节点一共能存储16KB,所以一个第三层一个节点大概可以存储16个元素(即16条记录)。那么结合第二层每个元素通过指针域找到第三层的节点,第二层一共是135W个元素,那么第三层总元素大小就是:135W*16结果就是2000W+的元素个数。

        结合上述分析B+Tree有如下优点:

                千万条数据,B+Tree可以控制在小于等于3的高度(IO次数)
                所有的数据都存储在叶子节点上,并且底层已经实现了按照索引进行排序,还可以支持范围查询,叶子节点是一个双向链表,支持从小到大或者从大到小查找

6.3 语法

        创建索引

create  [ unique ]  index 索引名 on  表名 (字段名,... ) ;

 

        查看索引 

show  index  from  表名;

        删除索引

drop  index  索引名  on  表名;

注意事项:

        1.主键字段,在建表时,会自动创建主键索引

        2.添加唯一约束时,数据库实际上会添加唯一索引

四. MyBatis

 1. 快速入门

1.1 创建springboot项目

创建springboot工程,并导入 mybatis的起步依赖、mysql的驱动包。

 

 

 项目工程创建完成后,自动在pom.xml文件中,导入Mybatis依赖和MySQL驱动依赖(查看是否存在这些依赖)

<!-- 仅供参考:只粘贴了pom.xml中部分内容 -->
<dependencies>
        <!-- mybatis起步依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!-- mysql驱动包依赖 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- spring单元测试 (集成了junit) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>

1.2 数据准备

创建用户表user,并创建对应的实体类User。

  • 用户表:

-- 用户表
create table user(
    id int unsigned primary key auto_increment comment 'ID',
    name varchar(100) comment '姓名',
    age tinyint unsigned comment '年龄',
    gender tinyint unsigned comment '性别, 1:男, 2:女',
    phone varchar(11) comment '手机号'
) comment '用户表';

-- 测试数据
insert into user(id, name, age, gender, phone) VALUES (null,'白眉鹰王',55,'1','18800000000');
insert into user(id, name, age, gender, phone) VALUES (null,'金毛狮王',45,'1','18800000001');
insert into user(id, name, age, gender, phone) VALUES (null,'青翼蝠王',38,'1','18800000002');
insert into user(id, name, age, gender, phone) VALUES (null,'紫衫龙王',42,'2','18800000003');
insert into user(id, name, age, gender, phone) VALUES (null,'光明左使',37,'1','18800000004');
insert into user(id, name, age, gender, phone) VALUES (null,'光明右使',48,'1','18800000005');

  • 实体类

    • 实体类的属性名与表中的字段名一一对应。

public class User {
    private Integer id;   //id(主键)
    private String name;  //姓名
    private Short age;    //年龄
    private Short gender; //性别
    private String phone; //手机号
    
    //省略GET, SET方法
}

修改application.properties:(在main -> source中)

#驱动类名称
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=1234

 1.3 编写SQL语句

在创建出来的springboot工程中,在引导类所在包下,在创建一个包 mapper。在mapper包下创建一个接口 UserMapper ,这是一个持久层接口(Mybatis的持久层接口规范一般都叫 XxxMapper)。

import com.itheima.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;

@Mapper
public interface UserMapper {
    
    //查询所有用户数据
    @Select("select id, name, age, gender, phone from user")
    public List<User> list();
    
}

@Mapper注解:表示是mybatis中的Mapper接口

  • 程序运行时:框架会自动生成接口的实现类对象(代理对象),并给交Spring的IOC容器管理

@Select注解:代表的就是select查询,用于书写select查询语句

 1.4 单元测试

测试类代码如下:

@SpringBootTest
public class MybatisQuickstartApplicationTests {
	
    @Autowired
    private UserMapper userMapper;
	
    @Test
    public void testList(){
        List<User> userList = userMapper.list();
        for (User user : userList) {
            System.out.println(user);
        }
    }

}

 

2. JDBC介绍(了解)

2.1 介绍

ava语言操作数据库呢,只能通过一种方式:使用sun公司提供的 JDBC 规范。

Mybatis框架,就是对原始的JDBC程序的封装。

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

本质:

  • sun公司官方定义的一套操作所有关系型数据库的规范,即接口。

  • 各个数据库厂商去实现这套接口,提供数据库驱动jar包。

  • 我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类。

 2.2 代码(暂时了解)

import com.itheima.pojo.User;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class JdbcTest {
    @Test
    public void testJdbc() throws Exception {
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");

        //2. 获取数据库连接
        String url="jdbc:mysql://127.0.0.1:3306/mybatis";
        String username = "root";
        String password = "1234";
        Connection connection = DriverManager.getConnection(url, username, password);

        //3. 执行SQL
        Statement statement = connection.createStatement(); //操作SQL的对象
        String sql="select id,name,age,gender,phone from user";
        ResultSet rs = statement.executeQuery(sql);//SQL查询结果会封装在ResultSet对象中

        List<User> userList = new ArrayList<>();//集合对象(用于存储User对象)
        //4. 处理SQL执行结果
        while (rs.next()){
            //取出一行记录中id、name、age、gender、phone下的数据
            int id = rs.getInt("id");
            String name = rs.getString("name");
            short age = rs.getShort("age");
            short gender = rs.getShort("gender");
            String phone = rs.getString("phone");
            //把一行记录中的数据,封装到User对象中
            User user = new User(id,name,age,gender,phone);
            userList.add(user);//User对象添加到集合
        }
        //5. 释放资源
        statement.close();
        connection.close();
        rs.close();

        //遍历集合
        for (User user : userList) {
            System.out.println(user);
        }
    }
}

DriverManager(类):数据库驱动管理类。

  • 作用:

    1. 注册驱动

    2. 创建java代码和数据库之间的连接,即获取Connection对象

Connection(接口):建立数据库连接的对象

  • 作用:用于建立java程序和数据库之间的连接

Statement(接口): 数据库操作对象(执行SQL语句的对象)。

  • 作用:用于向数据库发送sql语句

ResultSet(接口):结果集对象(一张虚拟表)

  • 作用:sql查询语句的执行结果会封装在ResultSet中

通过上述代码,我们看到直接基于JDBC程序来操作数据库,代码实现非常繁琐,所以在项目开发中,我们很少使用。 在项目开发中,通常会使用Mybatis这类的高级技术来操作数据库,从而简化数据库操作、提高开发效率。

2.3 技术对比

分析了JDBC的缺点之后,我们再来看一下在mybatis中,是如何解决这些问题的:

  1. 数据库连接四要素(驱动、链接、用户名、密码),都配置在springboot默认的配置文件 application.properties中

  2. 查询结果的解析及封装,由mybatis自动完成映射封装,我们无需关注

  3. 在mybatis中使用了数据库连接池技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。

3. 数据库连接池(理解)

3.1 介绍

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

                程序在启动时,会在数据库连接池(容器)中,创建一定数量的Connection对象

        2.它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个

                客户端在执行SQL时,先从连接池中获取一个Connection对象,然后在执行SQL语句,SQL语句执行完之后,释放Connection时就会把Connection对象归还给连接池(Connection对象可以复用)

        3.释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏

                客户端获取到Connection对象了,但是Connection对象并没有去访问数据库(处于空闲),数据库连接池发现Connection对象的空闲时间 > 连接池中预设的最大空闲时间,此时数据库连接池就会自动释放掉这个连接对象

优势:

        1.资源重用

        2.提升系统响应速度

        3.避免数据库连接遗漏

3.2 产品

         连接池实现

        官方(sun)提供了数据库连接池标准(javax.sql.DataSource接口)

                功能:获取连接 

                第三方组织必须按照DataSource接口实现

public Connection getConnection() throws SQLException;

              

        常见的数据库连接池:

Hikari(追光者) [默认的连接池] 

 

Druid(德鲁伊) 

  • Druid连接池是阿里巴巴开源的数据库连接池项目

  • 功能强大,性能优秀,是Java语言最好的数据库连接池之一

导入Druid的两种方式

        1.导入配置文件

<dependency>
    <!-- Druid连接池依赖 -->
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>

        2.配置文件k中加上druid

spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.druid.username=root
spring.datasource.druid.password=1234

4. lombok

4.1 介绍

允许我们使用注解,快速生成类的get,set等方法

重点为@Data @NoArgsConstructor @AllArgsConstructor三个注解

注解 作用
@Getter/@Setter 为所有的属性提供get/set方法
@ToString 会给类自动生成易阅读的 toString 方法
@EqualsAndHashCode 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法
@Data 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode)
@NoArgsConstructor 为实体类生成无参的构造器方法
@AllArgsConstructor 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。
@SLF4j 日志注解,标注在类上,代表这个类可以直接使用log对象记录日志
@Builder 会生产一个满参构造,和builder()方法,但如果自己加上无参构造,满参构造就会消息,所以使用这个注解要3个一起使用

4.2 使用

        1.导入依赖

<!-- 在springboot的父工程中,已经集成了lombok并指定了版本号,故当前引入依赖时不需要指定version -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

        2. 在实体类上添加注解

import lombok.Data;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;
}

5. Mybatis基础操作

5.1 准备

1.准备数据库表

-- 部门管理
create table 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 '部门表';
-- 部门表测试数据
insert into dept (id, name, create_time, update_time)
values (1, '学工部', now(), now()),
       (2, '教研部', now(), now()),
       (3, '咨询部', now(), now()),
       (4, '就业部', now(), now()),
       (5, '人事部', now(), now());


-- 员工管理
create table emp
(
    id          int unsigned primary key auto_increment comment 'ID',
    username    varchar(20)      not null unique comment '用户名',
    password    varchar(32) default '123456' comment '密码',
    name        varchar(10)      not null comment '姓名',
    gender      tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
    image       varchar(300) comment '图像',
    job         tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
    entrydate   date comment '入职时间',
    dept_id     int unsigned comment '部门ID',
    create_time datetime         not null comment '创建时间',
    update_time datetime         not null comment '修改时间'
) comment '员工表';
-- 员工表测试数据
INSERT INTO emp (id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time)
VALUES 
(1, 'jinyong', '123456', '金庸', 1, '1.jpg', 4, '2000-01-01', 2, now(), now()),
(2, 'zhangwuji', '123456', '张无忌', 1, '2.jpg', 2, '2015-01-01', 2, now(), now()),
(3, 'yangxiao', '123456', '杨逍', 1, '3.jpg', 2, '2008-05-01', 2, now(), now()),
(4, 'weiyixiao', '123456', '韦一笑', 1, '4.jpg', 2, '2007-01-01', 2, now(), now()),
(5, 'changyuchun', '123456', '常遇春', 1, '5.jpg', 2, '2012-12-05', 2, now(), now()),
(6, 'xiaozhao', '123456', '小昭', 2, '6.jpg', 3, '2013-09-05', 1, now(), now()),
(7, 'jixiaofu', '123456', '纪晓芙', 2, '7.jpg', 1, '2005-08-01', 1, now(), now()),
(8, 'zhouzhiruo', '123456', '周芷若', 2, '8.jpg', 1, '2014-11-09', 1, now(), now()),
(9, 'dingminjun', '123456', '丁敏君', 2, '9.jpg', 1, '2011-03-11', 1, now(), now()),
(10, 'zhaomin', '123456', '赵敏', 2, '10.jpg', 1, '2013-09-05', 1, now(), now()),
(11, 'luzhangke', '123456', '鹿杖客', 1, '11.jpg', 5, '2007-02-01', 3, now(), now()),
(12, 'hebiweng', '123456', '鹤笔翁', 1, '12.jpg', 5, '2008-08-18', 3, now(), now()),
(13, 'fangdongbai', '123456', '方东白', 1, '13.jpg', 5, '2012-11-01', 3, now(), now()),
(14, 'zhangsanfeng', '123456', '张三丰', 1, '14.jpg', 2, '2002-08-01', 2, now(), now()),
(15, 'yulianzhou', '123456', '俞莲舟', 1, '15.jpg', 2, '2011-05-01', 2, now(), now()),
(16, 'songyuanqiao', '123456', '宋远桥', 1, '16.jpg', 2, '2010-01-01', 2, now(), now()),
(17, 'chenyouliang', '123456', '陈友谅', 1, '17.jpg', NULL, '2015-03-21', NULL, now(), now());

 2.创建对应的实体类Emp(实体类属性采用驼峰命名)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
    private Integer id;
    private String username;
    private String password;
    private String name;
    private Short gender;
    private String image;
    private Short job;
    private LocalDate entrydate;     //LocalDate类型对应数据表中的date类型
    private Integer deptId;
    private LocalDateTime createTime;//LocalDateTime类型对应数据表中的datetime类型
    private LocalDateTime updateTime;
}

3.准备Mapper接口:EmpMapper

/*@Mapper注解:表示当前接口为mybatis中的Mapper接口
  程序运行时会自动创建接口的实现类对象(代理对象),并交给Spring的IOC容器管理
*/
@Mapper
public interface EmpMapper {

}

 完成以上步骤后的项目工程目录

5.2 删除

5.2.1 步骤

根据主键id删除数据

        1.接口方法

@Mapper
public interface EmpMapper {

    @Delete("delete from emp where id = #{id}")//使用#{key}方式获取方法中的参数值
    public void delete(Integer id);
    
}

        @Delete注解:用于编写delete操作的SQL语句

        #{}为mybatis为我们提供的占位符,中间传入参数,如图中参数id,放入大括号中

        2.测试

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired //从Spring的IOC容器中,获取类型是EmpMapper的对象并注入
    private EmpMapper empMapper;

    @Test
    public void testDel(){
        //调用删除方法
        empMapper.delete(16);
    }

}

运行后查看表格可发现数据删除成功

5.2.2 日志输入

在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。具体操作如下:

1. 打开application.properties文件

2. 开启mybatis的日志,并指定输出到控制台

#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

开启日志之后,我们再次运行单元测试,可以看到在控制台中看到查询日志

5.2.3  预编译SQL

预编译SQL有两个优势:

        1. 性能更高
        2. 更安全(防止SQL注入)

#{} 防止sql注入攻击(会进行预编译,在代码中会先用?表示,在将数据传入执行)

${}不能防止sql注入攻击(是字符的拼接)

SQL注入:是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。

        这里做一个假设,我们登录本质是查询数据库,看用户名和密码是否一致,查询返回值是否大于1,代码如下

#两个引号中的分别传入用户输入的用户名和密码
select count(*) from emp where username = '' and password = '';

        如果用户名和密码正确,count(*)应该是1,用户名和密码错误,count(*)结果为0,可以一次判断是否能正常登录.

但如果用户输入密码为以下时

' or '1' = '

此时我们的SQL语句发生了改变

select count(*) from emp where username = '' and password = '' or '1' = '';

无论用户输入什么用户名,只要密码是这种形式,count(*)就永远有值,因为where后面语句永远为true.用户就可以直接登录

5.2.4 参数占位符

在Mybatis中提供的参数占位符有两种:${...} 、#{...}

  • #{...}

    • 执行SQL时,会将#{…}替换为?,生成预编译SQL,会自动设置参数值

    • 使用时机:参数传递,都使用#{…}

  • ${...}

    • 拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题

    • 使用时机:如果对表名、列表进行动态设置时使用

注意事项:在项目开发中,建议使用#{...},生成预编译SQL,防止SQL注入安全。

 5.3 新增

5.3.1 基本新增

        在添加的数据很多时,我们可以在方法的形参改为分装的对象,不过对象的属性和占位符以及数据库表格中的字段名要一致(dept_id和deptId这种可以使用sql和java各自的规范,mybatis可以识别)

        接口方法与注解:

@Mapper
public interface EmpMapper {

    @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);

}

        测试类:

import com.itheima.mapper.EmpMapper;
import com.itheima.pojo.Emp;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDate;
import java.time.LocalDateTime;

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testInsert(){
        //创建员工对象
        Emp emp = new Emp();
        emp.setUsername("tom");
        emp.setName("汤姆");
        emp.setImage("1.jpg");
        emp.setGender((short)1);
        emp.setJob((short)1);
        emp.setEntrydate(LocalDate.of(2000,1,1));
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        emp.setDeptId(1);
        //调用添加方法
        empMapper.insert(emp);
    }
}

        日志输出

 5.3.2 主键返回

概念:在数据添加成功后,需要获取插入数据库数据的主键。

  • 默认情况下,执行插入操作时,是不会主键值返回的。如果我们想要拿到主键值,需要在Mapper接口中的方法上添加一个Options注解,并在注解中指定属性useGeneratedKeys=true和keyProperty="实体类属性名"

代码实现

//接口
@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);

}



//测试
@Mapper
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testInsert(){
        //创建员工对象
        Emp emp = new Emp();
        emp.setUsername("jack");
        emp.setName("杰克");
        emp.setImage("1.jpg");
        emp.setGender((short)1);
        emp.setJob((short)1);
        emp.setEntrydate(LocalDate.of(2000,1,1));
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        emp.setDeptId(1);
        //调用添加方法
        empMapper.insert(emp);

        System.out.println(emp.getDeptId());
    }
}

此时mybatis此时就会自动将添加后的数据的id,分装到emp对象中

5.4 更新

        接口方法:

@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 SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testUpdate(){
        //要修改的员工信息
        Emp emp = new Emp();
        emp.setId(23);
        emp.setUsername("songdaxia");
        emp.setPassword(null);
        emp.setName("老宋");
        emp.setImage("2.jpg");
        emp.setGender((short)1);
        emp.setJob((short)2);
        emp.setEntrydate(LocalDate.of(2012,1,1));
        emp.setCreateTime(null);
        emp.setUpdateTime(LocalDateTime.now());
        emp.setDeptId(2);
        //调用方法,修改员工数据
        empMapper.update(emp);
    }
}

 5.5 查询

5.5.1 根据ID查询

        根据id查询,接口方法

@Mapper
public interface EmpMapper {
    @Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
    public Emp getById(Integer id);
}

         测试类:

@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

    @Test
    public void testGetById(){
        Emp emp = empMapper.getById(1);
        System.out.println(emp);
    }
}

        结果是有可能部分字段没有数据值的(原因在以下解释)

5.5.2 数据封装

        deptId,createTime,updateTime这几个字段没有值的原因如下: 

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

        解决方案:

                1. 起别名
                2. 结果映射
                3. 开启驼峰命名:前提mysql和java类都一定要按照各自规范来定义属性  

起别名:在SQL语句中,对不一样的列名起别名,别名和实体类属性名一样

@Select("select id, username, password, name, gender, image, job, entrydate, " +
        "dept_id AS deptId, create_time AS createTime, update_time AS updateTime " +
        "from emp " +
        "where id=#{id}")
public Emp getById(Integer id);

 手动结果映射:通过 @Results及@Result 进行手动结果映射(Results里面放多个Result注解,每个Result对应一个字段映射,这种虽然麻烦,但自定义极高)

//写法
@Results({@Result(column = "dept_id", property = "deptId"),
          @Result(column = "create_time", property = "createTime"),
          @Result(column = "update_time", property = "updateTime")})
@Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where id=#{id}")
public Emp getById(Integer id);


//Results源代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Results {
String id() default "";

Result[] value() default {};  //Result类型的数组
}

//@Result源代码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Repeatable(Results.class)
public @interface Result {
boolean id() default false;//表示当前列是否为主键(true:是主键)

String column() default "";//指定表中字段名

String property() default "";//指定类中属性名

Class<?> javaType() default void.class;

JdbcType jdbcType() default JdbcType.UNDEFINED;

Class<? extends TypeHandler> typeHandler() default UnknownTypeHandler.class;

One one() default @One;

Many many() default @Many;
}

开启驼峰命名(推荐):如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射(要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。)

# 在application.properties中添加:
mybatis.configuration.map-underscore-to-camel-case=true
5.5.3 条件查询

        方式一(不推荐,可能存在SQL注入的风险)

@Mapper
public interface EmpMapper {
    @Select("select * from emp " +
            "where name like '%${name}%' " +
            "and gender = #{gender} " +
            "and entrydate between #{begin} and #{end} " +
            "order by update_time desc")
    public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);
}

         以上方式注意事项:

                1. 方法中的形参名和SQL语句中的参数占位符名保持一致

                2. 模糊查询使用${...}进行字符串拼接,这种方式呢,由于是字符串拼接,并不是预编译的形式,所以效率不高、且存在sql注入风险

        方式二(使用MySQL提供的字符串拼接函数:concat('%' , '关键字' , '%'))(如果没有结果请看5.5.4)

@Mapper
public interface EmpMapper {

    @Select("select * from emp " +
            "where name like concat('%',#{name},'%') " +
            "and gender = #{gender} " +
            "and entrydate between #{begin} and #{end} " +
            "order by update_time desc")
    public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

}

        执行结果依然是#{}这种占位符的形式,性能更高,且安全(避免SQL注入)

5.5.4 参数名说明

        我们需要保证接口中方法的形参名和SQL语句中的参数占位符名相同。

        当方法中的形参名和SQL语句中的占位符参数名不相同时,就会出现以下问题:

参数名在不同的SpringBoot版本中,处理方案还不同:

        在springBoot的2.x版本(保证参数名一致)

        springBoot的父工程对compiler编译插件进行了默认的参数parameters配置,使得在编译时,会在生成的字节码文件中保留原方法形参的名称,所以#{…}里面可以直接通过形参名获取对应的值

        在springBoot的1.x版本/单独使用mybatis(使用@Param注解来指定SQL语句中的参数名)

        在编译时,生成的字节码文件当中,不会保留Mapper接口中方法的形参名称,而是使用var1、var2、...这样的形参名字,此时要获取参数值时,就要通过@Param注解来指定SQL语句中的参数名

 6. Mybatis的XML配置文件

Mybatis的开发有两种方式:

        1. 注解
        2. XML(稍微比注解多一些)

6.1 XML配置文件规范

        原因:使用Mybatis的注解方式,主要是来完成一些简单的增删改功能.如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件

        在Mybatis中使用XML映射文件方式开发,需要符合一定的规范:

                1.XML映射文件的名称和Mapper接口名称一致,并且将XML映射文件和Mapper接口放在相同的包下(同包同名,值得一提的是,在SpringBoot项目中java包名和resources会在编译后合成一个包,所以xml文件只要在resources包中和映射文件包名相同即可)

                2.XML映射文件的namespace属性为Mapper接口全限定名一致

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

<select>标签:就是用于编写select查询语句的。

  • resultType属性,指的是查询返回的单条记录所封装的类型。

6.2 XML配置文件实现

        第1步:创建XML映射文件

        第2步:编写XML映射文件

               xml映射文件中的dtd约束,直接从mybatis官网复制即可

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="">
 
</mapper>

         配置:XML映射文件的namespace属性为Mapper接口全限定名

        配置:XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致(如果为集合,就要写泛型的全限定名)

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

    <!--查询操作-->
    <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>

         运行结果

值得一提的是,如果在xml中想要插入数据后的返回值,同样可以配置keyProperty="id" useGeneratedKeys="true"

7. Mybatis动态SQL

动态SQL: 使写的SQL语句更加灵活,根据用户传入的参数个数不同,动态的改变代码中的SQL语句

7.1 动态SQL-if

        <if>:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。

<if test="条件表达式">
   要拼接的sql语句
</if>
7.1.1 条件查询

示例:把SQL语句改造为动态SQL方式

        原有的SQL语句

<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>

         动态SQL语句(注意,在实际情况对于字符串类型的数据还行进行空字符串的判断,因为不同的浏览器返回的值不同)

<select id="list" resultType="com.itheima.pojo.Emp">
        select * 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>
    
        order by update_time desc
</select>

        测试方法:

@Test
public void testList(){
    //性别数据为null、开始时间和结束时间也为null
    List<Emp> list = empMapper.list("张", null, null, null);
    for(Emp emp : list){
        System.out.println(emp);
    }
}

以上问题的解决方案:使用`<where>`标签代替SQL语句中的where关键字

        <where>`只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR

<select id="list" resultType="com.itheima.pojo.Emp">
        select * from emp
        <where>
             <!-- if做为where标签的子元素 -->
             <if test="name != null and name!=''">
                 and 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>
 7.1.2 更新员工

案例:完善更新员工功能,修改为动态更新员工数据信息

        动态更新员工信息,如果更新时传递有值,则更新;如果更新时没有传递值,则不更新
        解决方案:动态SQL

XML文件代码:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<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>
        where id=#{id}
    </update>

</mapper>

测试方法:

@Test
public void testUpdate2(){
        //要修改的员工信息
        Emp emp = new Emp();
        emp.setId(20);
        emp.setUsername("Tom111");
        emp.setName("汤姆111");

        emp.setUpdateTime(LocalDateTime.now());

        //调用方法,修改员工数据
        empMapper.update(emp);
}

只使用这种发方法会出现各种问题

以上问题的解决方案:使用`<set>`标签代替SQL语句中的set关键字

        <set>`:动态的在SQL语句中插入set关键字,并会删掉额外的逗号。(用于update语句中)

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

    <!--更新操作-->
    <update id="update">
        update emp
        <!-- 使用set标签,代替update语句中的set关键字 -->
        <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>

7.2 动态SQL-foreach

案例:员工删除功能(既支持删除单条记录,又支持批量删除)

写死的SQL语句: 

delete from emp where id in (1,2,3);

 Mapper接口:

@Mapper
public interface EmpMapper {
    //批量删除
    public void deleteByIds(List<Integer> ids);
}

XML映射文件:

        使用`<foreach>`遍历deleteByIds方法中传递的参数ids集合

#forEach介绍
<foreach collection="集合名称" item="集合遍历出来的元素/项" separator="每一次遍历使用的分隔符" 
         open="遍历开始前拼接的片段" close="遍历结束后拼接的片段">
</foreach>



<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mapper.EmpMapper">
    <!--删除操作
	for(int id:ids)
-->
    <delete id="deleteByIds">
        delete from emp where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </delete>
</mapper> 

 

 7.3 动态SQL-sql&include

我们可以对重复的代码片段进行抽取,将其通过`<sql>`标签封装到一个SQL片段,然后再通过<include>`标签进行引用。

        <sql>`:定义可重用的SQL片段

        <include>`:通过属性refid,指定包含的SQL片段

 SQL片段: 抽取重复的代码

<sql id="commonSelect">
 	select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp
</sql>

然后通过`<include>` 标签在原来抽取的地方进行引用。操作如下:

<select id="list" resultType="com.itheima.pojo.Emp">
    <include refid="commonSelect"/>
    <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>

五. 其他内容

1. 文件上传

1.1 本地存储

代码实现:

  1. 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录)

  2. 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下

MultipartFile 常见方法:

  • String getOriginalFilename(); //获取原始文件名

  • void transferTo(File dest); //将接收的文件转存到磁盘文件中

  • long getSize(); //获取文件的大小,单位:字节

  • byte[] getBytes(); //获取文件内容的字节数组

  • InputStream getInputStream(); //获取接收到的文件内容的输入流

@Slf4j
@RestController
public class UploadController {

    @PostMapping("/upload")
    public Result upload(String username, Integer age, MultipartFile image) throws IOException {
        log.info("文件上传:{},{},{}",username,age,image);

        //获取原始文件名
        String originalFilename = image.getOriginalFilename();

        //构建新的文件名
        String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名
        String newFileName = UUID.randomUUID().toString()+extname;//随机名+文件扩展名

        //将文件存储在服务器的磁盘目录
        image.transferTo(new File("E:/images/"+newFileName));

        return Result.success();
    }

}

1.2 阿里云OSS

注册阿里云OSS(这里不在赘述)

参照官方提供的SDK,改造一下,即可实现文件上传功能:

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;

import java.io.FileInputStream;
import java.io.InputStream;

public class AliOssTest {
    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "oss-cn-shanghai.aliyuncs.com";
        
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "LTAI5t9MZK8iq5T2Av5GLDxX";
        String accessKeySecret = "C0IrHzKZGKqU8S7YQcevcotD3Zd5Tc";
        
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "web-framework01";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "1.jpg";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        String filePath= "C:\\Users\\Administrator\\Pictures\\1.jpg";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            InputStream inputStream = new FileInputStream(filePath);
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
            // 设置该属性可以返回response。如果不设置,则返回的response为空。
            putObjectRequest.setProcess("true");
            // 创建PutObject请求。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
            // 如果上传成功,则返回200。
            System.out.println(result.getResponse().getStatusCode());
        } 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();
            }
        }
    }
}

在以上代码中,需要替换的内容为:

  • accessKeyId:阿里云账号AccessKey

  • accessKeySecret:阿里云账号AccessKey对应的秘钥

  • bucketName:Bucket名称

  • objectName:对象名称,在Bucket中存储的对象的名称

  • filePath:文件路径

修改后的代码

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

@Component
public class AliOSSUtils {
    private String endpoint = "https://oss-cn-shanghai.aliyuncs.com";
    private String accessKeyId = "LTAI5t9MZK8iq5T2Av5GLDxX";
    private String accessKeySecret = "C0IrHzKZGKqU8S7YQcevcotD3Zd5Tc";
    private String bucketName = "web-framework01";

    /**
     * 实现上传图片到OSS
     */
    public String upload(MultipartFile multipartFile) throws IOException {
        // 获取上传的文件的输入流
        InputStream inputStream = multipartFile.getInputStream();

        // 避免文件覆盖
        String originalFilename = multipartFile.getOriginalFilename();
        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

        //上传文件到 OSS
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        ossClient.putObject(bucketName, fileName, inputStream);

        //文件访问路径
        String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;

        // 关闭ossClient
        ossClient.shutdown();
        return url;// 把上传到oss的路径返回
    }
}

1.3 配置文件

为了使代码更加灵活,我们可以将参数配置在配置文件中。如下:

#自定义的阿里云OSS配置信息
aliyun.oss.endpoint=https://oss-cn-hangzhou.aliyuncs.com
aliyun.oss.accessKeyId=LTAI4GCH1vX6DKqJWxd6nEuW
aliyun.oss.accessKeySecret=yBshYweHOpqDuhCArrVHwIiBKpyqSL
aliyun.oss.bucketName=web-tlias

1)

        因为application.properties是springboot项目默认的配置文件,所以springboot程序在启动时会默认读取application.properties配置文件,而我们可以使用一个现成的注解:@Value,获取配置文件中的数据。

        @Value 注解通常用于外部配置的属性注入,具体用法为: @Value("${配置文件中的key}")

@Component
public class AliOSSUtils {

    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    
    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;
    
    @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret;
    
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
 	
 	//省略其他代码...
 }   

2) @ConfigurationProperties

        

Spring提供的简化方式套路:

  1. 需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致

    比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法

  2. 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象

  3. 在实体类上添加@ConfigurationProperties注解,并通过perfect属性来指定配置参数项的前缀

代码就可以改成

2. 登录

1.4 JWT令牌(token)

JWT全称:JSON Web Token (官网:JSON Web Tokens - jwt.io

JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)

  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

    签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。

 

JWT是如何将原始的JSON格式数据,转变为字符串的呢?

其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码

Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号

需要注意的是Base64是编码方式,而不是加密方式。

1.4.1 生产和校验

要想使用JWT令牌,需要先引入JWT的依赖

<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

生成JWT代码实现:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtils {
    //秘钥,可以分装到配置文件中
    private static String KEY = "xhgroup";
    private static Long TIME = 3600000L;

    public static String getJwt(Map<String,Object> claims){
        String token = Jwts.builder()
                .setClaims(claims) //自定义内容(载荷)  第二部分,为一个map集合
                .signWith(SignatureAlgorithm.HS256, KEY.getBytes()) //签名算法  第三部分
                .setExpiration(new Date(System.currentTimeMillis() + TIME)) //设置有效时间
                .compact();

        return token;
    }

    public static Map parse(String token){
        //自定义一个方法,用于解析浏览器传过来的token
        Claims claims = Jwts.parser()
                .setSigningKey(KEY.getBytes())
                .parseClaimsJws(token)
                .getBody();

        return claims;
    }

}
1.4.2 过滤器
import com.alibaba.fastjson.JSONObject;
import com.itheima.pojo.Result;
import com.itheima.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter("/*") //代表要过滤的路径
@Slf4j
public class LoginFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //将原本方法的对象强转(目的为使用其中的方法)
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        //获得请求的路径
        String uri = req.getRequestURI();
        //判断是否为登陆路径
        if ("/login".equals(uri)){
            log.info("登陆请求:{}",uri);
            //放行
            chain.doFilter(req,resp);
            return;
        }
        //或得请求头中的token值
        String token = req.getHeader("token");
        //非空判断
        if (token == null){
            log.info("token为空:{}",token);
            resp.getWriter().write(fastError());
            return;
        }

        try {
            //解析token
            JwtUtils.parse(token);
        } catch (Exception e) {
            log.info("token错误:{}",token);
            resp.getWriter().write(fastError());
            return;
        }
        chain.doFilter(req,resp);
    }

    private String fastError(){
        Result r = Result.error("NOT_LOGIN");
        return JSONObject.toJSONString(r);
    }

}

多个过滤器的执行顺序是由过滤器的名字的自然排序所决定的

1.4.3 拦截器

实现代码

import com.itheima.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor inter;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器
        registry.addInterceptor(inter).addPathPatterns("/**").excludePathPatterns("/login");
    }
}
import com.alibaba.fastjson.JSONObject;
import com.itheima.pojo.Result;
import com.itheima.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("拦截器执行,拦截信息:{}", handler);
        //解析token
        String token = request.getHeader("token");
        if (token == null) {
            response.getWriter().write(fastError());
            return false;
        }

        try {
            JwtUtils.parse(token);
        } catch (Exception e) {
            response.getWriter().write(fastError());
            return false;
        }

        return true;
    }

    private String fastError() {
        Result r = Result.error("NOT_LOGIN");
        return JSONObject.toJSONString(r);
    }


}

 拦截器的顺序有注册先后决定

1.4.4 异常处理

全局异常处理器

  • 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。

  • 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。

  • @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        //处理异常
        @ExceptionHandler(Exception.class) //指定能够处理的异常类型
        public Result ex1(Exception e){
            e.printStackTrace();//打印堆栈中的异常信息
            //捕获到异常之后,响应一个标准的Result
            return Result.error("对不起,操作失败,请联系管理员");
        }
       

  • @RestControllerAdvice //表示当前类为全局异常处理器(@ControllerAdvice +@ResponseBody处理异常的方法返回值会转换为json后再响应给前端)

  • @ExceptionHandler //指定可以捕获哪种类型的异常进行处理

3. 事务管理

事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败。

事务的操作主要有三步:

  1. 开启事务(一组操作开始前,开启事务):start transaction / begin ;

  2. 提交事务(这组操作全部成功后,提交事务):commit ;

  3. 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;

3.1 Spring事务管理

@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。

@Transactional注解:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。

@Transactional注解书写位置:

  • 方法

    • 当前方法交给spring进行事务管理

    • 当前类中所有的方法都交由spring进行事务管理

  • 接口

    • 接口下所有的实现类当中所有的方法都交给spring 进行事务管理

@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Autowired
    private EmpMapper empMapper;

    
    @Override
    @Transactional  //当前方法添加了事务管理
    public void delete(Integer id){
        //根据部门id删除部门信息
        deptMapper.deleteById(id);
        
        //模拟:异常发生
        int i = 1/0;

        //删除部门下的所有员工信息
        empMapper.deleteByDeptId(id);   
    }
}

3.2 事务进阶

@Transactional注解当中的两个常见的属性:

  1. 异常回滚的属性:rollbackFor

  2. 事务传播行为:propagation

3.2.1 rollbackFor

默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。

假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。

    @Override
    @Transactional(rollbackFor=Exception.class)
    public void delete(Integer id){
        //根据部门id删除部门信息
        deptMapper.deleteById(id);
        
        //模拟:异常发生
        int num = id/0;

        //删除部门下的所有员工信息
        empMapper.deleteByDeptId(id);   
    }

3.2.2 propagation

 这个属性是用来配置事务的传播行为的。

  • 就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

属性值 含义
REQUIRED 【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
SUPPORTS 支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY 必须有事务,否则抛异常
NEVER 必须没事务,否则抛异常
    @Transactional(propagation = Propagation.REQUIRES_NEW)//事务传播行为:不论是否有事务,都新建事务

 4. AOP

AOP是一种思想

  • AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。

  • AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)

4.1 快速入门

需求:统计各个业务层方法执行耗时。

实现步骤:

  1. 导入依赖:在pom.xml中导入AOP的依赖

  2. 编写AOP程序:针对于特定方法根据业务需要进行编程

为演示方便,可以自建新项目或导入提供的springboot-aop-quickstart项目工程

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 新建一个类

@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {

    @Around("execution(* com.itheima.service.*.*(..))") 
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //记录方法执行开始时间
        long begin = System.currentTimeMillis();

        //执行原始方法
        Object result = pjp.proceed();//DeptService.findAll()

        //记录方法执行结束时间
        long end = System.currentTimeMillis();

        //计算方法执行耗时
        log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);

        return result;
    }
}

4.2 AOP核心概念

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

连接点指的是可以被aop控制的方法。例如:入门程序当中所有的业务方法都是可以被aop控制的方法。

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

在入门程序中是需要统计各个业务方法的执行耗时的,此时我们就需要在这些业务方法运行开始之前,先记录这个方法运行的开始时间,在每一个业务方法运行结束的时候,再来记录这个方法运行的结束时间。

但是在AOP面向切面编程当中,我们只需要将这部分重复的代码逻辑抽取出来单独定义。抽取出来的这一部分重复的逻辑,也就是共性的功能。

 

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

在通知当中,我们所定义的共性功能到底要应用在哪些方法上?此时就涉及到了切入点pointcut概念。切入点指的是匹配连接点的条件。通知仅会在切入点方法运行时才会被应用。

在aop的开发当中,我们通常会通过一个切入点表达式来描述切入点(后面会有详解)。

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

当通知和切入点结合在一起,就形成了一个切面。通过切面就能够描述当前aop程序需要针对于哪个原始方法,在什么时候执行什么样的操作。

切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)  

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

目标对象指的就是通知所应用的对象,我们就称之为目标对象。

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

4.3 AOP进阶

4.3.1 通知类型

在入门程序当中,我们已经使用了一种功能最为强大的通知类型:Around环绕通知。

@Around("execution(* com.itheima.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
    //省略的逻辑体;
}

Spring中AOP的通知类型:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行

  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行

  • @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

  • @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行

  • @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行

Spring提供了@PointCut注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。

@Slf4j
@Component
@Aspect
public class MyAspect1 {

    //切入点方法(公共的切入点表达式)
    @Pointcut("execution(* com.itheima.service.*.*(..))")
    private void pt(){

    }

    //前置通知(引用切入点)
    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info("before ...");

    }
}
4.3.2 通知顺序

通过以上程序运行可以看出在不同切面类中,默认按照切面类的类名字母排序:

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

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

如果我们想控制通知的执行顺序有两种方式:

  1. 修改切面类的类名(这种方式非常繁琐、而且不便管理)

  2. 使用Spring提供的@Order注解

使用@Order注解,控制通知的执行顺序:

@Slf4j
@Component
@Aspect
@Order(2)  //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
}
4.3.3 切入点表达式
5.3.3.1 execution

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

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

其中带?的表示可以省略的部分

  • 访问修饰符:可省略(比如: public、protected)

  • 包名.类名: 可通配

  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")

 

可以使用通配符描述切入点

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

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

切入点表达式的语法规则:

  1. 方法的访问修饰符可以省略

  2. 返回值可以使用*号代替(任意返回值类型)

  3. 包名可以使用*号代替,代表任意包(一层包使用一个*

  4. 使用..配置包名,标识此包以及此包下的所有子包

  5. 类名可以使用*号代替,标识任意类

  6. 方法名可以使用*号代替,表示任意方法

  7. 可以使用 * 配置参数,一个任意类型的参数

  8. 可以使用.. 配置参数,任意个任意类型的参数

注意事项:

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

切入点表达式的书写建议:

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头

4.3.3.2 @annotation

实现步骤:

  1. 编写自定义注解

  2. 在业务类要做为连接点的方法上添加自定义注解

1)自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}

2)业务类

@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Override
    @MyLog //自定义注解(表示:当前方法属于目标方法)
    public List<Dept> list() {
        List<Dept> deptList = deptMapper.list();
        //模拟异常
        //int num = 10/0;
        return deptList;
    }

    @Override
    @MyLog  //自定义注解(表示:当前方法属于目标方法)
    public void delete(Integer id) {
        //1. 删除部门
        deptMapper.delete(id);
    }


    @Override
    public void save(Dept dept) {
        dept.setCreateTime(LocalDateTime.now());
        dept.setUpdateTime(LocalDateTime.now());
        deptMapper.save(dept);
    }

    @Override
    public Dept getById(Integer id) {
        return deptMapper.getById(id);
    }

    @Override
    public void update(Dept dept) {
        dept.setUpdateTime(LocalDateTime.now());
        deptMapper.update(dept);
    }
}

3)切面类

@Slf4j
@Component
@Aspect
public class MyAspect6 {
    //针对list方法、delete方法进行前置通知和后置通知

    //前置通知
    @Before("@annotation(com.itheima.anno.MyLog)")
    public void before(){
        log.info("MyAspect6 -> before ...");
    }

    //后置通知
    @After("@annotation(com.itheima.anno.MyLog)")
    public void after(){
        log.info("MyAspect6 -> after ...");
    }
}
  • execution切入点表达式

    • 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式

    • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐

  • annotation 切入点表达式

    • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/XHgroup/article/details/134904061

智能推荐

python色卡识别_用Python帮小姐姐选口红,人人都是李佳琦-程序员宅基地

文章浏览阅读502次。原标题:用Python帮小姐姐选口红,人人都是李佳琦 对于李佳琦,想必知道他的女生要远远多于男生,李佳琦最早由于直播向广大的网友们推荐口红,逐渐走红网络,被大家称作“口红一哥”。不可否认的是,李佳琦的直播能力确实很强,他能够抓住绝大多数人的心理,让大家喜欢看他的直播,看他直播推荐的口红适不适合自己,色号适合什么样子的妆容。为了提升效率,让自己的家人或者女友能够快速的挑选出合适自己妆容的口红色号,今..._获取口红品牌 及色号,色值api

linux awk命令NR详解,linux awk命令详解-程序员宅基地

文章浏览阅读3.6k次。简介awk命令的名称是取自三位创始人Alfred Aho 、Peter Weinberger 和 Brian Kernighan姓名的首字母,awk有自己的程序设计语言,设计简短的程序,读入文件,数据排序,处理数据,生成报表等功能。awk 通常用于文本处理和报表生成,最基本功能是在文件或者字符串中基于指定规则浏览和抽取信息,awk抽取信息后,才能进行其他文本操作。awk 通常以文件的一行为处理单位..._linux awk nr

android 网络连接失败!failed to connect to /192.168.1.186(port 8080)_failed to connect to 192.168.88.218:80-程序员宅基地

文章浏览阅读1.3w次,点赞5次,收藏2次。在网上找了一个小时,一直没有头绪,因为上个星期还是好好的,最后看到一个大神的解答,只需要将防火墙关闭就好了.原本向测试功能的,却卡在了登录上.以此记录.另外好像还有种错误是电脑与手机连接的WiFi不同,也可以看看...._failed to connect to 192.168.88.218:80

matlab 多径衰落,利用MATLAB仿真多径衰落信道.doc-程序员宅基地

文章浏览阅读1.9k次。利用MATLAB仿真多种多径衰落信道摘要:移动信道的多径传播引起的瑞利衰落,时延扩展以及伴随接收过程的多普勒频移使接受信号受到严重的衰落,阴影效应会是接受的的信号过弱而造成通信的中断:在信道中存在噪声和干扰,也会是接收信号失真而造成误码,所以通过仿真找到衰落的原因并采取一些信号处理技术来改善信号接收质量显得很重要,这里利用MATLAB对多径衰落信道的波形做一比较。一,多径衰落信道的特点关于多径衰落..._matlab多径衰落工具箱

python对json的操作及实例解析_import json灰色-程序员宅基地

文章浏览阅读1w次,点赞2次,收藏17次。Json简介:Json,全名 JavaScript Object Notation,是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。(来自百度百科)python关于json文_import json灰色

mysql实现MHA高可用详细步骤_mysql mha超详细教程-程序员宅基地

文章浏览阅读1.1k次,点赞6次,收藏3次。一、工作原理MHA工作原理总结为以下几条:(1) 从宕机崩溃的 master 保存二进制日志事件(binlog events);(2) 识别含有最新更新的 slave ;(3) 应用差异的中继日志(relay log) 到其他 slave ;(4) 应用从 master 保存的二进制日志事件(binlog events);(5) 通过Manager控制器提升一个 slave 为新 m..._mysql mha超详细教程

随便推点

Linux环境下主从搭建心得(高手勿喷)_linux的java主从策略是什么-程序员宅基地

文章浏览阅读194次。一 java环境安装:1 安装JDK 参考链接地址:https://blog.csdn.net/qq_42815754/article/details/82968464注:有网情况下直接 yum 一键安装:yum -y list java(1)首先执行以下命令查看可安装的jdk版本(2)选择自己需要的jdk版本进行安装,比如这里安装1.8,执行以下命令:yum install -y java-1.8.0-openjdk-devel.x86_64(3)安装完之后,查看安装的jdk 版本,输入以下指令_linux的java主从策略是什么

ACM第四题_acm竞赛题 i 'm from mars-程序员宅基地

文章浏览阅读104次。定义int 类型,由while实现A,B的连续输入,输出A+B的值按Ctrl Z结束循环。#include&amp;lt;iostream&amp;gt;using namespace std;int main(){ int A,B; while(cin&amp;gt;&amp;gt;A&amp;gt;&amp;gt;B) { cout&amp;lt;&amp;lt;A+B&amp;lt;&_acm竞赛题 i 'm from mars

TextView.SetLinkMovementMethod后拦截所有点击事件的原因以及解决方法-程序员宅基地

文章浏览阅读5.2k次。在需要给TextView的某句话添加点击事件的时候,我们一般会使用ClickableSpan来进行富文本编辑。与此同时我们还需要配合 textView.setMovementMethod(LinkMovementMethod.getInstance());方法才能使点击处理生效。但与此同时还会有一个问题:如果我们给父布局添加一个点击事件,需要在点击非链接的时候触发(例如RectclerV..._linkmovementmethod

JAVA实现压缩解压文件_java 解压zip-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏31次。JAVA实现压缩解压文件_java 解压zip

JDK8 新特性-Map对key和value分别排序实现_java comparingbykey-程序员宅基地

文章浏览阅读1.3w次,点赞7次,收藏21次。在Java 8 中使用Stream 例子对一个 Map 进行按照keys或者values排序.1. 快速入门 在java 8中按照此步骤对map进行排序.将 Map 转换为 Stream 对其进行排序 Collect and return a new LinkedHashMap (保持顺序)Map result = map.entrySet().stream() .sort..._java comparingbykey

GDKOI2021普及Day1总结-程序员宅基地

文章浏览阅读497次。第一次参加GDKOI,考完感觉还可以,结果发现还是不行,有一些地方细节打错,有些失分严重,总结出以下几点:1.大模拟一定要注意,细节打挂就是没分,像T1就是一道大模拟题,马上切了,后面就没想着检查以下,导致有些地方挂掉了,用民间数据一测,才85分。2.十年OI一场空,不开longlonglong longlonglong见祖宗。今天的T2本来想用暴力水点分的,结果没想到longlong→intlong long\to intlonglong→int,40→040\to040→0。3.代码实现能力太差,_gdkoi

推荐文章

热门文章

相关标签