DAY01
1. 分层解耦
1.1 三层架构
1.1.1 介绍
在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。
单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。
这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。
我们之前开发的程序呢,并不满足单一职责原则。下面我们来分析下之前的程序:
那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:
-
数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。
-
逻辑处理:负责业务逻辑处理的代码。
-
请求处理、响应数据:负责,接收页面的请求,给页面响应数据。
按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层:
-
Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
-
Service:业务逻辑层。处理具体的业务逻辑。
-
Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。
基于三层架构的程序执行流程:
-
前端发起的请求,由Controller层接收(Controller响应数据给前端)
-
Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
-
Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
-
Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)
思考:按照三层架构的思想,如何要对业务逻辑(Service层)进行变更,会影响到Controller层和Dao层吗?
答案:不会影响。 (程序的扩展性、维护性变得更好了)
1.1.2 代码拆分
我们使用三层架构思想,来改造下之前的程序:
-
控制层包名:xxxx.controller
-
业务逻辑层包名:xxxx.service
-
数据访问层包名:xxxx.dao
控制层:接收前端发送的请求,对请求进行处理,并响应数据
@RestController public class EmpController { //业务层对象 private EmpService empService = new EmpServiceA(); @RequestMapping("/listEmp") public Result list(){ //1. 调用service层, 获取数据 List<Emp> empList = empService.listEmp(); //3. 响应数据 return Result.success(empList); } }
业务逻辑层:处理具体的业务逻辑
-
业务接口
//业务逻辑接口(制定业务标准) public interface EmpService { //获取员工列表 public List<Emp> listEmp(); }
-
业务实现类
//业务逻辑实现类(按照业务标准实现) public class EmpServiceA implements EmpService { //dao层对象 private EmpDao empDao = new EmpDaoA(); @Override public List<Emp> listEmp() { //1. 调用dao, 获取数据 List<Emp> empList = empDao.listEmp(); //2. 对数据进行转换处理 - gender, job empList.stream().forEach(emp -> { //处理 gender 1: 男, 2: 女 String gender = emp.getGender(); if("1".equals(gender)){ emp.setGender("男"); }else if("2".equals(gender)){ emp.setGender("女"); } //处理job - 1: 讲师, 2: 班主任 , 3: 就业指导 String job = emp.getJob(); if("1".equals(job)){ emp.setJob("讲师"); }else if("2".equals(job)){ emp.setJob("班主任"); }else if("3".equals(job)){ emp.setJob("就业指导"); } }); return empList; } }
数据访问层:负责数据的访问操作,包含数据的增、删、改、查
-
数据访问接口
//数据访问层接口(制定标准) public interface EmpDao { //获取员工列表数据 public List<Emp> listEmp(); }
-
数据访问实现类
//数据访问实现类 public class EmpDaoA implements EmpDao { @Override public List<Emp> listEmp() { //1. 加载并解析emp.xml String file = this.getClass().getClassLoader().getResource("emp.xml").getFile(); System.out.println(file); List<Emp> empList = XmlParserUtils.parse(file, Emp.class); return empList; } }
三层架构的好处:
-
复用性强
-
便于维护
-
利用扩展
1.2 分层解耦
刚才我们学习过程序分层思想了,接下来呢,我们来学习下程序的解耦思想。
解耦:解除耦合。
1.2.1 耦合问题
首先需要了解软件开发涉及到的两个概念:内聚和耦合。
-
内聚:软件中各个功能模块内部的功能联系。
-
耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
软件设计原则:高内聚低耦合。
高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 "高内聚"。
低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。
程序中高内聚的体现:
-
EmpServiceA类中只编写了和员工相关的逻辑处理代码
程序中耦合代码的体现:
-
把业务类变为EmpServiceB时,需要修改controller层中的代码
高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。
1.2.2 解耦思路
之前我们在编写代码时,需要什么对象,就直接new一个就可以了。 这种做法呢,层与层之间代码就耦合了,当service层的实现变了之后, 我们还需要修改controller层的代码。
那应该怎么解耦呢?
-
首先不能在EmpController中使用new对象。代码如下:
-
此时,就存在另一个问题了,不能new,就意味着没有业务层对象(程序运行就报错),怎么办呢?
-
我们的解决思路是:
-
提供一个容器,容器中存储一些对象(例:EmpService对象)
-
controller程序从容器中获取EmpService类型的对象
-
-
我们想要实现上述解耦操作,就涉及到Spring中的两个核心概念:
-
控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器
-
依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
程序运行时需要某个资源,此时容器就为其提供这个资源。
例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象
IOC容器中创建、管理的对象,称之为:bean对象
1.3 IOC&DI
上面我们引出了Spring中IOC和DI的基本概念,下面我们就来具体学习下IOC和DI的代码实现。
1.3.1 IOC&DI入门
任务:完成Controller层、Service层、Dao层的代码解耦
-
思路:
-
删除Controller层、Service层中new对象的代码
-
Service层及Dao层的实现类,交给IOC容器管理
-
为Controller及Service注入运行时依赖的对象
-
Controller程序中注入依赖的Service层对象
-
Service程序中注入依赖的Dao层对象
-
-
第1步:删除Controller层、Service层中new对象的代码
第2步:Service层及Dao层的实现类,交给IOC容器管理
-
使用Spring提供的注解:@Component ,就可以实现类交给IOC容器管理
第3步:为Controller及Service注入运行时依赖的对象
-
使用Spring提供的注解:@Autowired ,就可以实现程序运行时IOC容器自动注入需要的依赖对象
完整的三层代码:
-
Controller层:
@RestController public class EmpController { @Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量 private EmpService empService ; @RequestMapping("/listEmp") public Result list(){ //1. 调用service, 获取数据 List<Emp> empList = empService.listEmp(); //3. 响应数据 return Result.success(empList); } }
-
Service层:
@Component //将当前对象交给IOC容器管理,成为IOC容器的bean public class EmpServiceA implements EmpService { @Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量 private EmpDao empDao ; @Override public List<Emp> listEmp() { //1. 调用dao, 获取数据 List<Emp> empList = empDao.listEmp(); //2. 对数据进行转换处理 - gender, job empList.stream().forEach(emp -> { //处理 gender 1: 男, 2: 女 String gender = emp.getGender(); if("1".equals(gender)){ emp.setGender("男"); }else if("2".equals(gender)){ emp.setGender("女"); } //处理job - 1: 讲师, 2: 班主任 , 3: 就业指导 String job = emp.getJob(); if("1".equals(job)){ emp.setJob("讲师"); }else if("2".equals(job)){ emp.setJob("班主任"); }else if("3".equals(job)){ emp.setJob("就业指导"); } }); return empList; } }
Dao层:
@Component //将当前对象交给IOC容器管理,成为IOC容器的bean public class EmpDaoA implements EmpDao { @Override public List<Emp> listEmp() { //1. 加载并解析emp.xml String file = this.getClass().getClassLoader().getResource("emp.xml").getFile(); System.out.println(file); List<Emp> empList = XmlParserUtils.parse(file, Emp.class); return empList; } }
运行测试:
-
启动SpringBoot引导类,打开浏览器,输入:http://localhost:8080/emp.html
1.3.2 IOC详解
通过IOC和DI的入门程序呢,我们已经基本了解了IOC和DI的基础操作。接下来呢,我们学习下IOC控制反转和DI依赖注入的细节。
1.3.2.1 bean的声明
前面我们提到IOC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。
在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component
而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:
-
@Controller (标注在控制层类上)
-
@Service (标注在业务层类上)
-
@Repository (标注在数据访问层类上)
修改入门案例代码:
-
Controller层:
@RestController //@RestController = @Controller + @ResponseBody public class EmpController { @Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量 private EmpService empService ; @RequestMapping("/listEmp") public Result list(){ //1. 调用service, 获取数据 List<Emp> empList = empService.listEmp(); //3. 响应数据 return Result.success(empList); } }
-
Service层:
@Service public class EmpServiceA implements EmpService { @Autowired //运行时,从IOC容器中获取该类型对象,赋值给该变量 private EmpDao empDao ; @Override public List<Emp> listEmp() { //1. 调用dao, 获取数据 List<Emp> empList = empDao.listEmp(); //2. 对数据进行转换处理 - gender, job empList.stream().forEach(emp -> { //处理 gender 1: 男, 2: 女 String gender = emp.getGender(); if("1".equals(gender)){ emp.setGender("男"); }else if("2".equals(gender)){ emp.setGender("女"); } //处理job - 1: 讲师, 2: 班主任 , 3: 就业指导 String job = emp.getJob(); if("1".equals(job)){ emp.setJob("讲师"); }else if("2".equals(job)){ emp.setJob("班主任"); }else if("3".equals(job)){ emp.setJob("就业指导"); } }); return empList; } }
Dao层:
@Repository public class EmpDaoA implements EmpDao { @Override public List<Emp> listEmp() { //1. 加载并解析emp.xml String file = this.getClass().getClassLoader().getResource("emp.xml").getFile(); System.out.println(file); List<Emp> empList = XmlParserUtils.parse(file, Emp.class); return empList; } }
要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解 | 说明 | 位置 |
---|---|---|
@Controller | @Component的衍生注解 | 标注在控制器类上 |
@Service | @Component的衍生注解 | 标注在业务类上 |
@Repository | @Component的衍生注解 | 标注在数据访问类上(由于与mybatis整合,用的少) |
@Component | 声明bean的基础注解 | 不属于以上三类时,用此注解 |
查看源码:
在IOC容器中,每一个Bean都有一个属于自己的名字,可以通过注解的value属性指定bean的名字。如果没有指定,默认为类名首字母小写。
注意事项:
声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。
3.3.2.2 组件扫描
问题:使用前面学习的四个注解声明的bean,一定会生效吗?
答案:不一定。(原因:bean想要生效,还需要被组件扫描)
下面我们通过修改项目工程的目录结构,来测试bean对象是否生效:
运行程序后,报错:
为什么没有找到bean对象呢?
-
使用四大注解声明的bean,要想生效,还需要被组件扫描注解@ComponentScan扫描
@ComponentScan注解虽然没有显式配置,但是实际上已经包含在了引导类声明注解 @SpringBootApplication 中,默认扫描的范围是SpringBoot启动类所在包及其子包。
-
解决方案:手动添加@ComponentScan注解,指定要扫描的包 (仅做了解,不推荐)
推荐做法(如下图):
-
将我们定义的controller,service,dao这些包呢,都放在引导类所在包com.itheima的子包下,这样我们定义的bean就会被自动的扫描到
1.3.3 DI详解
上一小节我们讲解了控制反转IOC的细节,接下来呢,我们学习依赖注解DI的细节。
依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。
在入门程序案例中,我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。
@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)
入门程序举例:在EmpController运行的时候,就要到IOC容器当中去查找EmpService这个类型的对象,而我们的IOC容器中刚好有一个EmpService这个类型的对象,所以就找到了这个类型的对象完成注入操作。
那如果在IOC容器中,存在多个相同类型的bean对象,会出现什么情况呢?
-
程序运行会报错
如何解决上述问题呢?Spring提供了以下几种解决方案:
-
@Primary
-
@Qualifier
-
@Resource
使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。
使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。
-
@Qualifier注解不能单独使用,必须配合@Autowired使用
使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。
面试题 : @Autowird 与 @Resource的区别
@Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
@Autowired 默认是按照类型注入,而@Resource是按照名称注入
2. MySQL概述
官网:https://dev.mysql.com/
2.1 安装
2.1.1 版本
MySQL官方提供了两个版本:
-
商业版本(MySQL Enterprise Edition)
-
该版本是收费的,我们可以使用30天。 官方会提供对应的技术支持。
-
-
社区版本(MySQL Community Server)
-
该版本是免费的,但是MySQL不会提供任何的技术支持。
-
本课程,采用的是MySQL的社区版本(8.0.31)
1.1.2 安装
官网下载地址:https://downloads.mysql.com/archives/community/
这个安装包呢,大家可以不用下载,因为资料中已提供现成的安装包。
安装MySQL具体的步骤,参考资料中提供的 《MySQL安装文档》 ,进行软件的安装即可。
1.1.3 连接
MySQL服务器启动完毕后,然后再使用如下指令,来连接MySQL服务器:
mysql -u用户名 -p密码 [-h数据库服务器的IP地址 -P端口号]
-h 参数不加,默认连接的是本地 127.0.0.1 的MySQL服务器
-P 参数不加,默认连接的端口号是 3306
上述指令,可以有两种形式:
-
密码直接在-p参数之后直接指定 (这种方式不安全,密码直接以明文形式出现在命令行)
-
密码在-p回车之后,在命令行中输入密码,然后回车
1.1.4 企业使用方式(了解)
上述的MySQL服务器我们是安装在本地的,这个仅仅是在我们学习阶段,在真实的企业开发中,MySQL数据库服务器是不会在我们本地安装的,是在公司的服务器上安装的,而服务器还需要放置在专门的IDC机房中的,IDC机房呢,就需要保证恒温、恒湿、恒压,而且还要保证网络、电源的可靠性(备用电源及网络)。
那我们要想使用服务器上的这台MySQL服务器,就需要在我们的电脑上去远程连接这台MySQL。 而服务器上安装的MySQL数据库呢,并不是你一个人在访问,我们项目组的其他开发人员也是需要访问这台MySQL的。
我们在学习阶段,如果想提前体验一下企业中数据库的使用方式,可以借助于VMware虚拟机来实现。我们可以在我们的电脑上安装一个VMware,然后在Vmware虚拟机上在安装一个服务器操作系统Linux,然后再在Linux服务器上安装各种企业级软件。
接下来,就来演示一下,通过MySQL的客户端命令行,如何来连接服务器上部署的MySQL :
mysql -u用户名 -p密码 [-h数据库服务器的IP地址 -P端口号]
2.2 数据模型
介绍完了Mysql数据库的安装配置之后,接下来我们再来聊一聊Mysql当中的数据模型。学完了这一小节之后,我们就能够知道在Mysql数据库当中到底是如何来存储和管理数据的。
在介绍 Mysql的数据模型之前,需要先了解一个概念:关系型数据库。
关系型数据库(RDBMS)
概念:建立在关系模型基础上,由多张相互连接的二维表组成的数据库。
而所谓二维表,指的是由行和列组成的表,如下图:
二维表的优点:
-
使用表存储数据,格式统一,便于维护
-
使用SQL语言操作,标准统一,使用方便,可用于复杂查询
我们之前提到的MySQL、Oracle、DB2、SQLServer这些都是属于关系型数据库,里面都是基于二维表存储数据的。
结论:基于二维表存储数据的数据库就成为关系型数据库,不是基于二维表存储数据的数据库,就是非关系型数据库(比如大家后面要学习的Redis,就属于非关系型数据库)。
2). 数据模型
介绍完了关系型数据库之后,接下来我们再来看一看在Mysql数据库当中到底是如何来存储数据的,也就是Mysql 的数据模型。
MySQL是关系型数据库,是基于二维表进行数据存储的,具体的结构图下:
-
通过MySQL客户端连接数据库管理系统DBMS,然后通过DBMS操作数据库
-
使用MySQL客户端,向数据库管理系统发送一条SQL语句,由数据库管理系统根据SQL语句指令去操作数据库中的表结构及数据
-
一个数据库服务器中可以创建多个数据库,一个数据库中也可以包含多张表,而一张表中又可以包含多行记录。
在Mysql数据库服务器当中存储数据,你需要:
先去创建数据库(可以创建多个数据库,之间是相互独立的)
在数据库下再去创建数据表(一个数据库下可以创建多张表)
再将数据存放在数据表中(一张表可以存储多行数据)
2.3 SQL简介
SQL:结构化查询语言。一门操作关系型数据库的编程语言,定义操作所有关系型数据库的统一标准。
在学习具体的SQL语句之前,先来了解一下SQL语言的语法。
2.3.1 SQL通用语法
1、SQL语句可以单行或多行书写,以分号结尾。
2、SQL语句可以使用空格/缩进来增强语句的可读性。
3、MySQL数据库的SQL语句不区分大小写。
4、注释:
-
单行注释:-- 注释内容 或 # 注释内容(MySQL特有)
-
多行注释: /* 注释内容 */
以上就是SQL语句的通用语法,这些通用语法大家目前先有一个直观的认识,我们后面在讲解每一类SQL语句的时候,还会再来强调通用语法。
2.3.2 分类
SQL语句根据其功能被分为四大类:DDL、DML、DQL、DCL
分类 | 全称 | 说明 |
---|---|---|
DDL | Data Definition Language | 数据定义语言,用来定义数据库对象(数据库,表,字段) |
DML | Data Manipulation Language | 数据操作语言,用来对数据库表中的数据进行增删改 |
DQL | Data Query Language | 数据查询语言,用来查询数据库中表的记录 |
DCL | Data Control Language | 数据控制语言,用来创建数据库用户、控制数据库的访问权限 |
3. 数据库设计-DDL
下面我们就正式的进入到SQL语句的学习,在学习之前先给大家介绍一下我们要开发一个项目,整个开发流程是什么样的,以及在流程当中哪些环节会涉及到数据库。
3.1 项目开发流程
需求文档:
-
在我们开发一个项目或者项目当中的某个模块之前,会先会拿到产品经理给我们提供的页面原型及需求文档。
设计:
-
拿到产品原型和需求文档之后,我们首先要做的不是编码,而是要先进行项目的设计,其中就包括概要设计、详细设计、接口设计、数据库设计等等。
-
数据库设计根据产品原型以及需求文档,要分析各个模块涉及到的表结构以及表结构之间的关系,以及表结构的详细信息。最终我们需要将数据库以及数据库当中的表结构设计创建出来。
开发/测试:
-
参照页面原型和需求进行编码,实现业务功能。在这个过程当中,我们就需要来操作设计出来的数据库表结构,来完成业务的增删改查操作等。
部署上线:
-
在项目的功能开发测试完成之后,项目就可以上线运行了,后期如果项目遇到性能瓶颈,还需要对项目进行优化。优化很重要的一个部分就是数据库的优化,包括数据库当中索引的建立、SQL 的优化、分库分表等操作。
在上述的流程当中,针对于数据库来说,主要包括三个阶段:
-
数据库设计阶段
-
参照页面原型以及需求文档设计数据库表结构
-
-
数据库操作阶段
-
根据业务功能的实现,编写SQL语句对数据表中的数据进行增删改查操作
-
-
数据库优化阶段
-
通过数据库的优化来提高数据库的访问性能。优化手段:索引、SQL优化、分库分表等
-
接下来我们就先来学习第一部分数据库的设计,而数据库的设计就是来定义数据库,定义表结构以及表中的字段。
3.2 数据库操作
我们在进行数据库设计,需要使用到刚才所介绍SQL分类中的DDL语句。
DDL英文全称是Data Definition Language(数据定义语言),用来定义数据库对象(数据库、表)。
DDL中数据库的常见操作:查询、创建、使用、删除。
3.2.1 查询数据库
查询所有数据库:
show databases;
命令行中执行效果如下:
查询当前数据库:
select database();
命令行中执行效果如果:
我们要操作某一个数据库,必须要切换到对应的数据库中。
通过指令:select database() ,就可以查询到当前所处的数据库
3.2.2 创建数据库
语法:
create database [ if not exists ] 数据库名;
案例: 创建一个itcast数据库。
create database itcast;
命令行执行效果如下:
注意:在同一个数据库服务器中,不能创建两个名称相同的数据库,否则将会报错。
-
可以使用if not exists来避免这个问题
-- 数据库不存在,则创建该数据库;如果存在则不创建 create database if not extists itcast;
命令行执行效果如下:
3.2.3 使用数据库
语法:
use 数据库名 ;
我们要操作某一个数据库下的表时,就需要通过该指令,切换到对应的数据库下,否则不能操作。
案例:切换到itcast数据
use itcast;
命令执行效果如下:
3.2.4 删除数据库
语法:
drop database [ if exists ] 数据库名 ;
如果删除一个不存在的数据库,将会报错。
可以加上参数 if exists ,如果数据库存在,再执行删除,否则不执行删除。
案例:删除itcast数据库
drop database if exists itcast; -- itcast数据库存在时删除
命令执行效果如下:
说明:上述语法中的database,也可以替换成 schema
如:create schema db01;
如:show schemas;
3.3 图形化工具
3.3.1 介绍
前面我们讲解了DDL中关于数据库操作的SQL语句,在我们编写这些SQL时,都是在命令行当中完成的。大家在练习的时候应该也感受到了,在命令行当中来敲这些SQL语句很不方便,主要的原因有以下 3 点:
-
没有任何代码提示。(全靠记忆,容易敲错字母造成执行报错)
-
操作繁琐,影响开发效率。(所有的功能操作都是通过SQL语句来完成的)
-
编写过的SQL代码无法保存。
在项目开发当中,通常为了提高开发效率,都会借助于现成的图形化管理工具来操作数据库。
目前MySQL主流的图形化界面工具有以下几种:
DataGrip是JetBrains旗下的一款数据库管理工具,是管理和开发MySQL、Oracle、PostgreSQL的理想解决方案。
官网: DataGrip:由 JetBrains 开发的数据库和 SQL 跨平台 IDE
3.3.2 安装
安装: 参考资料中提供的《DataGrip安装手册》
说明:DataGrip这款工具可以不用安装,因为Jetbrains公司已经将DataGrip这款工具的功能已经集成到了 IDEA当中,所以我们就可以使用IDEA来作为一款图形化界面工具来操作Mysql数据库。
3.3.3 使用
3.2.3.1 连接数据库
1、打开IDEA自带的Database
2、配置MySQL
3、输入相关信息
4、下载MySQL连接驱动
5、测试数据库连接
6、保存配置
默认情况下,连接上了MySQL数据库之后, 数据库并没有全部展示出来。 需要选择要展示哪些数据库。具体操作如下:
3.2.3.2 操作数据库
创建数据库:
有了图形化界面工具后,就可以方便的使用图形化工具:创建数据库,创建表、修改表等DDL操作。
其实工具底层也是通过DDL语句操作的数据库,只不过这些SQL语句是图形化界面工具帮我们自动完成的。
查看所有数据库:
3.3 表操作
学习完了DDL语句当中关于数据库的操作之后,接下来我们继续学习DDL语句当中关于表结构的操作。
关于表结构的操作也是包含四个部分:创建表、查询表、修改表、删除表。
3.3.1 创建
3.3.1.1 语法
create table 表名( 字段1 字段1类型 [约束] [comment 字段1注释 ], 字段2 字段2类型 [约束] [comment 字段2注释 ], ...... 字段n 字段n类型 [约束] [comment 字段n注释 ] ) [ comment 表注释 ] ;
注意: [ ] 中的内容为可选参数; 最后一个字段后面没有逗号
案例:创建tb_user表
-
对应的结构如下:
-
建表语句:
create table tb_user ( id int comment 'ID,唯一标识', # id是一行数据的唯一标识(不能重复) username varchar(20) comment '用户名', name varchar(10) comment '姓名', age int comment '年龄', gender char(1) comment '性别' ) comment '用户表';
数据表创建完成,接下来我们还需要测试一下是否可以往这张表结构当中来存储数据。
双击打开tb_user表结构,大家会发现里面没有数据:
添加数据:
此时我们再插入一条数据:
我们之前提到过:id字段是一行数据的唯一标识,不能有重复值。但是现在数据表中有两个相同的id值,这是为什么呢?
-
其实我们现在创建表结构的时候, id这个字段我们只加了一个备注信息说明它是一个唯一标识,但是在数据库层面呢,并没有去限制字段存储的数据。所以id这个字段没有起到唯一标识的作用。
想要限制字段所存储的数据,就需要用到数据库中的约束。
3.3.1.2 约束
概念:所谓约束就是作用在表中字段上的规则,用于限制存储在表中的数据。
作用:就是来保证数据库当中数据的正确性、有效性和完整性。(后面的学习会验证这些)
在MySQL数据库当中,提供了以下5种约束:
约束 | 描述 | 关键字 |
---|---|---|
非空约束 | 限制该字段值不能为null | not null |
唯一约束 | 保证字段的所有数据都是唯一、不重复的 | unique |
主键约束 | 主键是一行数据的唯一标识,要求非空且唯一 | primary key |
默认约束 | 保存数据时,如果未指定该字段值,则采用默认值 | default |
外键约束 | 让两张表的数据建立连接,保证数据的一致性和完整性 | foreign key |
注意:约束是作用于表中字段上的,可以在创建表/修改表的时候添加约束。
案例:创建tb_user表
-
对应的结构如下:
在上述的表结构中:
id 是一行数据的唯一标识
username 用户名字段是非空且唯一的
name 姓名字段是不允许存储空值的
gender 性别字段是有默认值,默认为男
-
建表语句:
create table tb_user ( id int primary key comment 'ID,唯一标识', username varchar(20) not null unique comment '用户名', name varchar(10) not null comment '姓名', age int comment '年龄', gender char(1) default '男' comment '性别' ) comment '用户表';
数据表创建完成,接下来测试一下表中字段上的约束是否生效
大家有没有发现一个问题:id字段下存储的值,如果由我们自己来维护会比较麻烦(必须保证值的唯一性)。MySQL数据库为了解决这个问题,给我们提供了一个关键字:auto_increment(自动增长)
主键自增:auto_increment
每次插入新的行记录时,数据库自动生成id字段(主键)下的值
具有auto_increment的数据列是一个正数序列开始增长(从1开始自增)
create table tb_user ( id int primary key auto_increment comment 'ID,唯一标识', #主键自动增长 username varchar(20) not null unique comment '用户名', name varchar(10) not null comment '姓名', age int comment '年龄', gender char(1) default '男' comment '性别' ) comment '用户表';
测试:主键自增
2.3.1.3 数据类型
在上面建表语句中,我们在指定字段的数据类型时,用到了int 、varchar、char,那么在MySQL中除了以上的数据类型,还有哪些常见的数据类型呢? 接下来,我们就来详细介绍一下MySQL的数据类型。
MySQL中的数据类型有很多,主要分为三类:数值类型、字符串类型、日期时间类型。
数值类型
类型 | 大小 | 有符号(SIGNED)范围 | 无符号(UNSIGNED)范围 | 描述 |
---|---|---|---|---|
TINYINT | 1byte | (-128,127) | (0,255) | 小整数值 |
SMALLINT | 2bytes | (-32768,32767) | (0,65535) | 大整数值 |
MEDIUMINT | 3bytes | (-8388608,8388607) | (0,16777215) | 大整数值 |
INT/INTEGER | 4bytes | (-2147483648,2147483647) | (0,4294967295) | 大整数值 |
BIGINT | 8bytes | (-263,263-1) | (0,2^64-1) | 极大整数值 |
FLOAT | 4bytes | (-3.402823466 E+38,3.402823466351 E+38) | 0 和 (1.175494351 E-38,3.402823466 E+38) | 单精度浮点数值 |
DOUBLE | 8bytes | (-1.7976931348623157 E+308,1.7976931348623157 E+308) | 0 和 (2.2250738585072014 E-308,1.7976931348623157 E+308) | 双精度浮点数值 |
DECIMAL | 依赖于M(精度)和D(标度)的值 | 依赖于M(精度)和D(标度)的值 | 小数值(精确定点数) |
示例: 年龄字段 ---不会出现负数, 而且人的年龄不会太大 age tinyint unsigned 分数 ---总分100分, 最多出现一位小数 score double(4,1)
字符串类型
类型 | 大小 | 描述 |
---|---|---|
CHAR | 0-255 bytes | 定长字符串(需要指定长度) |
VARCHAR | 0-65535 bytes | 变长字符串(需要指定长度) |
TINYBLOB | 0-255 bytes | 不超过255个字符的二进制数据 |
TINYTEXT | 0-255 bytes | 短文本字符串 |
BLOB | 0-65 535 bytes | 二进制形式的长文本数据 |
TEXT | 0-65 535 bytes | 长文本数据 |
MEDIUMBLOB | 0-16 777 215 bytes | 二进制形式的中等长度文本数据 |
MEDIUMTEXT | 0-16 777 215 bytes | 中等长度文本数据 |
LONGBLOB | 0-4 294 967 295 bytes | 二进制形式的极大文本数据 |
LONGTEXT | 0-4 294 967 295 bytes | 极大文本数据 |
char 与 varchar 都可以描述字符串,char是定长字符串,指定长度多长,就占用多少个字符,和字段值的长度无关 。而varchar是变长字符串,指定的长度为最大占用长度 。相对来说,char的性能会更高些。
示例: 用户名 username ---长度不定, 最长不会超过50 username varchar(50) 手机号 phone ---固定长度为11 phone char(11)
日期时间类型
类型 | 大小 | 范围 | 格式 | 描述 |
---|---|---|---|---|
DATE | 3 | 1000-01-01 至 9999-12-31 | YYYY-MM-DD | 日期值 |
TIME | 3 | -838:59:59 至 838:59:59 | HH:MM:SS | 时间值或持续时间 |
YEAR | 1 | 1901 至 2155 | YYYY | 年份值 |
DATETIME | 8 | 1000-01-01 00:00:00 至 9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值 |
TIMESTAMP | 4 | 1970-01-01 00:00:01 至 2038-01-19 03:14:07 | YYYY-MM-DD HH:MM:SS | 混合日期和时间值,时间戳 |
示例: 生日字段 birthday ---生日只需要年月日 birthday date 创建时间 createtime --- 需要精确到时分秒 createtime datetime
2.3.1.4 案例
需求:根据产品原型/需求创建表((设计合理的数据类型、长度、约束)
参考资料中提供的《黑马-tlias智能学习辅助系统》页面原型,设计员工管理模块的表结构
暂不考虑所属部门字段
产品原型及需求如下:
步骤:
-
阅读产品原型及需求文档,看看里面涉及到哪些字段。
-
查看需求文档说明,确认各个字段的类型以及字段存储数据的长度限制。
-
在页面原型中描述的基础字段的基础上,再增加额外的基础字段。
使用SQL创建表:
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 教研主管', entrydate date comment '入职时间', create_time datetime not null comment '创建时间', update_time datetime not null comment '修改时间' ) comment '员工表';
除了使用SQL语句创建表外,我们还可以借助于图形化界面来创建表结构,这种创建方式会更加直观、更加方便。
操作步骤如下:
-
在指定操作的数据库上,右键New ==> Table
-
编辑表结构的相关信息
2.3.1.5 设计表流程
通过上面的案例,我们明白了,设计一张表,基本的流程如下:
-
阅读页面原型及需求文档
-
基于页面原则和需求文档,确定原型字段(类型、长度限制、约束)
-
再增加表设计所需要的业务基础字段(id主键、插入时间、修改时间)
说明:
create_time:记录的是当前这条数据插入的时间。
update_time:记录当前这条数据最后更新的时间。
3.3.2 查询
关于表结构的查询操作,工作中一般都是直接基于图形化界面操作。
查询当前数据库所有表
show tables;
查看指定表结构
desc 表名 ;#可以查看指定表的字段、字段的类型、是否可以为NULL、是否存在默认值等信息
查询指定表的建表语句
show create table 表名 ;
3.3.3 修改
关于表结构的修改操作,工作中一般都是直接基于图形化界面操作。
添加字段
alter table 表名 add 字段名 类型(长度) [comment 注释] [约束];
案例: 为tb_emp表添加字段qq,字段类型为 varchar(11)
alter table tb_emp add qq varchar(11) comment 'QQ号码';
图形化操作:添加字段
修改数据类型
alter table 表名 modify 字段名 新数据类型(长度);
alter table 表名 change 旧字段名 新字段名 类型(长度) [comment 注释] [约束];
案例:修改qq字段的字段类型,将其长度由11修改为13
alter table tb_emp modify qq varchar(13) comment 'QQ号码';
案例:修改qq字段名为 qq_num,字段类型varchar(13)
alter table tb_emp change qq qq_num varchar(13) comment 'QQ号码';
图形化操作:修改数据类型和字段名
删除字段
alter table 表名 drop 字段名;
案例:删除tb_emp表中的qq_num字段
alter table tb_emp drop qq_num;
图形化操作:删除字段
修改表名
rename table 表名 to 新表名;
案例:将当前的tb_emp表的表名修改为emp
rename table tb_emp to emp;
图形化操作:修改表名
3.3.4 删除
关于表结构的删除操作,工作中一般都是直接基于图形化界面操作。
删除表语法:
drop table [ if exists ] 表名;
if exists :只有表名存在时才会删除该表,表名不存在,则不执行删除操作(如果不加该参数项,删除一张不存在的表,执行将会报错)。
案例:如果tb_emp表存在,则删除tb_emp表
drop table if exists tb_emp; -- 在删除表时,表中的全部数据也会被删除。
图形化操作:删除表
DAY02
1. 数据库操作-DML
DML英文全称是Data Manipulation Language(数据操作语言),用来对数据库中表的数据记录进行增、删、改操作。
-
添加数据(INSERT)
-
修改数据(UPDATE)
-
删除数据(DELETE)
1.1 增加(insert)
insert语法:
-
向指定字段添加数据
insert into 表名 (字段名1, 字段名2) values (值1, 值2);
-
全部字段添加数据
insert into 表名 values (值1, 值2, ...);
-
批量添加数据(指定字段)
insert into 表名 (字段名1, 字段名2) values (值1, 值2), (值1, 值2);
-
批量添加数据(全部字段)
insert into 表名 values (值1, 值2, ...), (值1, 值2, ...);
案例1:向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());
案例2:向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());
案例3:批量向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());
图形化操作:双击tb_emp表查看数据
Insert操作的注意事项:
-
插入数据时,指定的字段顺序需要与值的顺序是一一对应的。
-
字符串和日期型数据应该包含在引号中。
-
插入的数据大小,应该在字段的规定范围内。
1.2 修改(update)
update语法:
update 表名 set 字段名1 = 值1 , 字段名2 = 值2 , .... [where 条件] ;
案例1:将tb_emp表中id为1的员工,姓名name字段更新为'张三'
update tb_emp set name='张三',update_time=now() where id=1;
案例2:将tb_emp表的所有员工入职日期更新为'2010-01-01'
update tb_emp set entrydate='2010-01-01',update_time=now();
注意事项:
修改语句的条件可以有,也可以没有,如果没有条件,则会修改整张表的所有数据。
在修改数据时,一般需要同时修改公共字段update_time,将其修改为当前操作时间。
1.3 删除(delete)
delete语法:
delete from 表名 [where 条件] ;
案例1:删除tb_emp表中id为1的员工
delete from tb_emp where id = 1;
案例2:删除tb_emp表中所有员工
delete from tb_emp;
注意事项:
• DELETE 语句的条件可以有,也可以没有,如果没有条件,则会删除整张表的所有数据。
• DELETE 语句不能删除某一个字段的值(可以使用UPDATE,将该字段值置为NULL即可)。
• 当进行删除全部数据操作时,会提示询问是否确认删除所有数据,直接点击Execute即可。
2. 数据库操作-DQL
2.1 介绍
DQL英文全称是Data Query Language(数据查询语言),用来查询数据库表中的记录。
查询关键字:SELECT
查询操作是所有SQL语句当中最为常见,也是最为重要的操作。在一个正常的业务系统中,查询操作的使用频次是要远高于增删改操作的。当我们打开某个网站或APP所看到的展示信息,都是通过从数据库中查询得到的,而在这个查询过程中,还会涉及到条件、排序、分页等操作。
2.2 语法
DQL查询语句,语法结构如下:
SELECT 字段列表 FROM 表名列表 WHERE 条件列表 GROUP BY 分组字段列表 HAVING 分组后条件列表 ORDER BY 排序字段列表 LIMIT 分页参数
我们今天会将上面的完整语法拆分为以下几个部分学习:
-
基本查询(不带任何条件)
-
条件查询(where)
-
分组查询(group by)
-
排序查询(order by)
-
分页查询(limit)
准备一些测试数据用于查询操作:
create database db02; -- 创建数据库 use db02; -- 切换数据库 -- 员工管理(带约束) 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 '员工表'; -- 准备测试数据 INSERT INTO tb_emp (id, username, password, name, gender, image, job, entrydate, create_time, update_time) VALUES (1, 'jinyong', '123456', '金庸', 1, '1.jpg', 4, '2000-01-01', '2022-10-27 16:35:33', '2022-10-27 16:35:35'), (2, 'zhangwuji', '123456', '张无忌', 1, '2.jpg', 2, '2015-01-01', '2022-10-27 16:35:33', '2022-10-27 16:35:37'), (3, 'yangxiao', '123456', '杨逍', 1, '3.jpg', 2, '2008-05-01', '2022-10-27 16:35:33', '2022-10-27 16:35:39'), (4, 'weiyixiao', '123456', '韦一笑', 1, '4.jpg', 2, '2007-01-01', '2022-10-27 16:35:33', '2022-10-27 16:35:41'), (5, 'changyuchun', '123456', '常遇春', 1, '5.jpg', 2, '2012-12-05', '2022-10-27 16:35:33', '2022-10-27 16:35:43'), (6, 'xiaozhao', '123456', '小昭', 2, '6.jpg', 3, '2013-09-05', '2022-10-27 16:35:33', '2022-10-27 16:35:45'), (7, 'jixiaofu', '123456', '纪晓芙', 2, '7.jpg', 1, '2005-08-01', '2022-10-27 16:35:33', '2022-10-27 16:35:47'), (8, 'zhouzhiruo', '123456', '周芷若', 2, '8.jpg', 1, '2014-11-09', '2022-10-27 16:35:33', '2022-10-27 16:35:49'), (9, 'dingminjun', '123456', '丁敏君', 2, '9.jpg', 1, '2011-03-11', '2022-10-27 16:35:33', '2022-10-27 16:35:51'), (10, 'zhaomin', '123456', '赵敏', 2, '10.jpg', 1, '2013-09-05', '2022-10-27 16:35:33', '2022-10-27 16:35:53'), (11, 'luzhangke', '123456', '鹿杖客', 1, '11.jpg', 2, '2007-02-01', '2022-10-27 16:35:33', '2022-10-27 16:35:55'), (12, 'hebiweng', '123456', '鹤笔翁', 1, '12.jpg', 2, '2008-08-18', '2022-10-27 16:35:33', '2022-10-27 16:35:57'), (13, 'fangdongbai', '123456', '方东白', 1, '13.jpg', 1, '2012-11-01', '2022-10-27 16:35:33', '2022-10-27 16:35:59'), (14, 'zhangsanfeng', '123456', '张三丰', 1, '14.jpg', 2, '2002-08-01', '2022-10-27 16:35:33', '2022-10-27 16:36:01'), (15, 'yulianzhou', '123456', '俞莲舟', 1, '15.jpg', 2, '2011-05-01', '2022-10-27 16:35:33', '2022-10-27 16:36:03'), (16, 'songyuanqiao', '123456', '宋远桥', 1, '16.jpg', 2, '2010-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:05'), (17, 'chenyouliang', '12345678', '陈友谅', 1, '17.jpg', null, '2015-03-21', '2022-10-27 16:35:33', '2022-10-27 16:36:07'), (18, 'zhang1', '123456', '张一', 1, '2.jpg', 2, '2015-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:09'), (19, 'zhang2', '123456', '张二', 1, '2.jpg', 2, '2012-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:11'), (20, 'zhang3', '123456', '张三', 1, '2.jpg', 2, '2018-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:13'), (21, 'zhang4', '123456', '张四', 1, '2.jpg', 2, '2015-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:15'), (22, 'zhang5', '123456', '张五', 1, '2.jpg', 2, '2016-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:17'), (23, 'zhang6', '123456', '张六', 1, '2.jpg', 2, '2012-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:19'), (24, 'zhang7', '123456', '张七', 1, '2.jpg', 2, '2006-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:21'), (25, 'zhang8', '123456', '张八', 1, '2.jpg', 2, '2002-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:23'), (26, 'zhang9', '123456', '张九', 1, '2.jpg', 2, '2011-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:25'), (27, 'zhang10', '123456', '张十', 1, '2.jpg', 2, '2004-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:27'), (28, 'zhang11', '123456', '张十一', 1, '2.jpg', 2, '2007-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:29'), (29, 'zhang12', '123456', '张十二', 1, '2.jpg', 2, '2020-01-01', '2022-10-27 16:35:33', '2022-10-27 16:36:31');
2.3 基本查询
在基本查询的DQL语句中,不带任何的查询条件,语法如下:
-
查询多个字段
select 字段1, 字段2, 字段3 from 表名;
-
查询所有字段(通配符)
select * from 表名;
-
设置别名
select 字段1 [ as 别名1 ] , 字段2 [ as 别名2 ] from 表名;
-
去除重复记录
select distinct 字段列表 from 表名;
案例1:查询指定字段 name,entrydate并返回
select name,entrydate from tb_emp;
案例2:查询返回所有字段
select * from tb_emp;
*
号代表查询所有字段,在实际开发中尽量少用(不直观、影响效率)
案例3:查询所有员工的 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;
案例4:查询已有的员工关联了哪几种职位(不要重复)
select distinct job from tb_emp;
2.4 条件查询
语法:
select 字段列表 from 表名 where 条件列表 ; -- 条件列表:意味着可以有多个条件
学习条件查询就是学习条件的构建方式,而在SQL语句当中构造条件的运算符分为两类:
-
比较运算符
-
逻辑运算符
常用的比较运算符如下:
比较运算符 | 功能 |
---|---|
> | 大于 |
>= | 大于等于 |
< | 小于 |
<= | 小于等于 |
= | 等于 |
<> 或 != | 不等于 |
between ... and ... | 在某个范围之内(含最小、最大值) |
in(...) | 在in之后的列表中的值,多选一 |
like 占位符 | 模糊匹配(_匹配单个字符, %匹配任意个字符) |
is null | 是null |
常用的逻辑运算符如下:
逻辑运算符 | 功能 |
---|---|
and 或 && | 并且 (多个条件同时成立) |
or 或 || | 或者 (多个条件任意一个成立) |
not 或 ! | 非 , 不是 |
案例1:查询 姓名 为 杨逍 的员工
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp where name = '杨逍'; -- 字符串使用''或""包含
案例2:查询 id小于等于5 的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp where id <=5;
案例3:查询 没有分配职位 的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp where job is null ;
注意:查询为NULL的数据时,不能使用
= null
案例4:查询 有职位 的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp where job is not null ;
案例5:查询 密码不等于 '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';
案例6:查询 入职日期 在 '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';
案例7:查询 入职时间 在 '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;
案例8:查询 职位是 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);
案例9:查询 姓名 为两个字的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp where name like '__'; # 通配符 "_" 代表任意1个字符
案例10:查询 姓 '张' 的员工信息
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp where name like '张%'; # 通配符 "%" 代表任意个字符(0个 ~ 多个)
2.5 聚合函数
之前我们做的查询都是横向查询,就是根据条件一行一行的进行判断,而使用聚合函数查询就是纵向查询,它是对一列的值进行计算,然后返回一个结果值。(将一列数据作为一个整体,进行纵向计算)
语法:
select 聚合函数(字段列表) from 表名 ;
注意 : 聚合函数会忽略空值,对NULL值不作为统计。
常用聚合函数:
函数 | 功能 |
---|---|
count | 统计数量 |
max | 最大值 |
min | 最小值 |
avg | 平均值 |
sum | 求和 |
count :按照列去统计有多少行数据。
在根据指定的列统计的时候,如果这一列中有null的行,该行不会被统计在其中。
sum :计算指定列的数值和,如果不是数值类型,那么计算结果为0
max :计算指定列的最大值
min :计算指定列的最小值
avg :计算指定列的平均值
案例1:统计该企业员工数量
# 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;
案例2:统计该企业最早入职的员工
select min(entrydate) from tb_emp;
案例3:统计该企业最迟入职的员工
select max(entrydate) from tb_emp;
案例4:统计该企业员工 ID 的平均值
select avg(id) from tb_emp;
案例5:统计该企业员工的 ID 之和
select sum(id) from tb_emp;
2.6 分组查询
分组: 按照某一列或者某几列,把相同的数据进行合并输出。
分组其实就是按列进行分类(指定列下相同的数据归为一类),然后可以对分类完的数据进行合并计算。
分组查询通常会使用聚合函数进行计算。
语法:
select 字段列表 from 表名 [where 条件] group by 分组字段名 [having 分组后过滤条件];
案例1:根据性别分组 , 统计男性和女性员工的数量
select gender, count(*) from tb_emp group by gender; -- 按照gender字段进行分组(gender字段下相同的数据归为一组)
案例2:查询入职时间在 '2015-01-01' (包含) 以前的员工 , 并对结果根据职位分组 , 获取员工数量大于等于2的职位
select job, count(*) from tb_emp where entrydate <= '2015-01-01' -- 分组前条件 group by job -- 按照job字段分组 having count(*) >= 2; -- 分组后条件
注意事项:
• 分组之后,查询的字段一般为聚合函数和分组字段,查询其他字段无任何意义
• 执行顺序:where > 聚合函数 > having
where与having区别(面试题)
-
执行时机不同:where是分组之前进行过滤,不满足where条件,不参与分组;而having是分组之后对结果进行过滤。
-
判断条件不同:where不能对聚合函数进行判断,而having可以。
2.7 排序查询
排序在日常开发中是非常常见的一个操作,有升序排序,也有降序排序。
语法:
select 字段列表 from 表名 [where 条件列表] [group by 分组字段 ] order by 字段1 排序方式1 , 字段2 排序方式2 … ;
-
排序方式:
-
ASC :升序(默认值)
-
DESC:降序
-
案例1:根据入职时间, 对员工进行升序排序
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(升序)
注意事项:如果是升序, 可以不指定排序方式ASC
案例2:根据入职时间,对员工进行降序排序
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp order by entrydate DESC; -- 按照entrydate字段下的数据进行降序排序
案例3:根据入职时间对公司的员工进行升序排序,入职时间相同,再按照更新时间进行降序排序
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp order by entrydate ASC , update_time DESC;
注意事项:如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序
2.8 分页查询
分页操作在业务系统开发时,也是非常常见的一个功能,日常我们在网站中看到的各种各样的分页条,后台也都需要借助于数据库的分页操作。
分页查询语法:
select 字段列表 from 表名 limit 起始索引, 查询记录数 ;
案例1:从起始索引0开始查询员工数据, 每页展示5条记录
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp limit 0 , 5; -- 从索引0开始,向后取5条记录
案例2:查询 第1页 员工数据, 每页展示5条记录
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp limit 5; -- 如果查询的是第1页数据,起始索引可以省略,直接简写为:limit 条数
案例3:查询 第2页 员工数据, 每页展示5条记录
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp limit 5 , 5; -- 从索引5开始,向后取5条记录
案例4:查询 第3页 员工数据, 每页展示5条记录
select id, username, password, name, gender, image, job, entrydate, create_time, update_time from tb_emp limit 10 , 5; -- 从索引10开始,向后取5条记录
注意事项:
起始索引从0开始。 计算公式 : 起始索引 = (查询页码 - 1)* 每页显示记录数
分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是LIMIT
如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 条数
2.9 案例
DQL的基本语法我们学习结束了,接下来我们就运用所掌握的DQL语句的语法来完成两个案例。
2.9.1 案例一
案例:根据需求完成员工管理的条件分页查询
分析:根据输入的条件,查询第1页数据
在员工管理的列表上方有一些查询条件:员工姓名、员工性别,员工入职时间(开始时间~结束时间)
姓名:张
性别:男
入职时间:2000-01-01 ~ 2015-12-31
除了查询条件外,在列表的下面还有一个分页条,这就涉及到了分页查询
查询第1页数据(每页显示10条数据)
基于查询的结果,按照修改时间进行降序排序
结论:条件查询 + 分页查询 + 排序查询
SQL语句代码:
-- 根据输入条件查询第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.9.2 案例二
案例:根据需求完成员工信息的统计
分析:以上信息统计在开发中也叫图形报表(将统计好的数据以可视化的形式展示出来)
员工性别统计:以饼状图的形式展示出企业男性员人数和女性员工人数
只要查询出男性员工和女性员工各自有多少人就可以了
员工职位统计:以柱状图的形式展示各职位的在岗人数
只要查询出各个职位有多少人就可以了
员工性别统计:
-- if(条件表达式, true取值 , false取值) select if(gender=1,'男性员工','女性员工') AS 性别, count(*) AS 人数 from tb_emp group by gender;
if(表达式, tvalue, fvalue) :当表达式为true时,取值tvalue;当表达式为false时,取值fvalue
员工职位统计:
-- 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 表达式 when 值1 then 结果1 [when 值2 then 结果2 ...] [else result] end
DAY03
1. 多表设计
关于单表的操作(单表的设计、单表的增删改查)我们就已经学习完了。接下来我们就要来学习多表的操作,首先来学习多表的设计。
项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种:
-
一对多(多对一)
-
多对多
-
一对一
1.1 一对多
1.1.1 表设计
需求:根据页面原型及需求文档 ,完成部门及员工的表结构设计
-
员工管理页面原型:(前面已完成tb_emp表结构设计)
-
部门管理页面原型:
经过上述分析,现已明确的部门表结构:
业务字段 : 部门名称
基础字段 : id(主键)、创建时间、修改时间
部门表 - SQL语句:
# 建议:创建新的数据库(多表设计存放在新数据库下) 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());
员工表 - 部门表之间的关系:
一对多关系实现:在数据库表中多的一方,添加字段,来关联属于一这方的主键。
2.1.2 外键约束
问题
-
表结构创建完毕后,我们看到两张表的数据分别为:
现在员工表中有五个员工都归属于1号部门(学工部),当删除了1号部门后,数据变为:
1号部门被删除了,但是依然还有5个员工是属于1号部门的。 此时:就出现数据的不完整、不一致了。
问题分析
目前上述的两张表(员工表、部门表),在数据库层面,并未建立关联,所以是无法保证数据的一致性和完整性的
问题解决
想解决上述的问题呢,我们就可以通过数据库中的 外键约束 来解决。
外键约束:让两张表的数据建立连接,保证数据的一致性和完整性。
对应的关键字:foreign key
外键约束的语法:
-- 创建表时指定 create table 表名( 字段名 数据类型, ... [constraint] [外键名称] foreign key (外键字段名) references 主表 (主表列名) ); -- 建完表后,添加外键 alter table 表名 add constraint 外键名称 foreign key(外键字段名) references 主表(主表列名);
那接下来,我们就为员工表的dept_id 建立外键约束,来关联部门表的主键。
方式1:通过SQL语句操作
-- 修改表: 添加外键约束 alter table tb_emp add constraint fk_dept_id foreign key (dept_id) references tb_dept(id);
方式2:图形化界面操作
当我们添加外键约束时,我们得保证当前数据库表中的数据是完整的。 所以,我们需要将之前删除掉的数据再添加回来。
当我们添加了外键之后,再删除ID为1的部门,就会发现,此时数据库报错了,不允许删除。
外键约束(foreign key):保证了数据的完整性和一致性。
物理外键和逻辑外键
-
物理外键
-
概念:使用foreign key定义外键关联另外一张表。
-
缺点:
-
影响增、删、改的效率(需要检查外键关系)。
-
仅用于单节点数据库,不适用与分布式、集群场景。
-
容易引发数据库的死锁问题,消耗性能。
-
-
-
逻辑外键
-
概念:在业务层逻辑中,解决外键关联。
-
通过逻辑外键,就可以很方便的解决上述问题。
-
在现在的企业开发中,很少会使用物理外键,都是使用逻辑外键。 甚至在一些数据库开发规范中,会明确指出禁止使用物理外键 foreign key
1.2 一对一
一对一关系表在实际开发中应用起来比较简单,通常是用来做单表的拆分,也就是将一张大表拆分成两张小表,将大表中的一些基础字段放在一张表当中,将其他的字段放在另外一张表当中,以此来提高数据的操作效率。
一对一的应用场景: 用户表(基本信息+身份信息)
基本信息:用户的ID、姓名、性别、手机号、学历
身份信息:民族、生日、身份证号、身份证签发机关,身份证的有效期(开始时间、结束时间)
如果在业务系统当中,对用户的基本信息查询频率特别的高,但是对于用户的身份信息查询频率很低,此时出于提高查询效率的考虑,我就可以将这张大表拆分成两张小表,第一张表存放的是用户的基本信息,而第二张表存放的就是用户的身份信息。他们两者之间一对一的关系,一个用户只能对应一个身份证,而一个身份证也只能关联一个用户。
那么在数据库层面怎么去体现上述两者之间是一对一的关系呢?
其实一对一我们可以看成一种特殊的一对多。一对多我们是怎么设计表关系的?是不是在多的一方添加外键。同样我们也可以通过外键来体现一对一之间的关系,我们只需要在任意一方来添加一个外键就可以了。
一对一 :在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)
SQL脚本:
-- 用户基本信息表 create table tb_user( id int unsigned primary key auto_increment comment 'ID', name varchar(10) not null comment '姓名', gender tinyint unsigned not null comment '性别, 1 男 2 女', phone char(11) comment '手机号', degree varchar(10) comment '学历' ) comment '用户基本信息表'; -- 测试数据 insert into tb_user values (1,'白眉鹰王',1,'18812340001','初中'), (2,'青翼蝠王',1,'18812340002','大专'), (3,'金毛狮王',1,'18812340003','初中'), (4,'紫衫龙王',2,'18812340004','硕士'); -- 用户身份信息表 create table tb_user_card( id int unsigned primary key auto_increment comment 'ID', nationality varchar(10) not null comment '民族', birthday date not null comment '生日', idcard char(18) not null comment '身份证号', issued varchar(20) not null comment '签发机关', expire_begin date not null comment '有效期限-开始', expire_end date comment '有效期限-结束', user_id int unsigned not null unique comment '用户ID', constraint fk_user_id foreign key (user_id) references tb_user(id) ) comment '用户身份信息表'; -- 测试数据 insert into tb_user_card values (1,'汉','1960-11-06','100000100000100001','朝阳区公安局','2000-06-10',null,1), (2,'汉','1971-11-06','100000100000100002','静安区公安局','2005-06-10','2025-06-10',2), (3,'汉','1963-11-06','100000100000100003','昌平区公安局','2006-06-10',null,3), (4,'回','1980-11-06','100000100000100004','海淀区公安局','2008-06-10','2028-06-10',4);
1.3 多对多
多对多的关系在开发中属于也比较常见的。比如:学生和老师的关系,一个学生可以有多个授课老师,一个授课老师也可以有多个学生。在比如:学生和课程的关系,一个学生可以选修多门课程,一个课程也可以供多个学生选修。
案例:学生与课程的关系
-
关系:一个学生可以选修多门课程,一门课程也可以供多个学生选择
-
实现关系:建立第三张中间表,中间表至少包含两个外键,分别关联两方主键
SQL脚本:
-- 学生表 create table tb_student( id int auto_increment primary key comment '主键ID', name varchar(10) comment '姓名', no varchar(10) comment '学号' ) comment '学生表'; -- 学生表测试数据 insert into tb_student(name, no) values ('黛绮丝', '2000100101'),('谢逊', '2000100102'),('殷天正', '2000100103'),('韦一笑', '2000100104'); -- 课程表 create table tb_course( id int auto_increment primary key comment '主键ID', name varchar(10) comment '课程名称' ) comment '课程表'; -- 课程表测试数据 insert into tb_course (name) values ('Java'), ('PHP'), ('MySQL') , ('Hadoop'); -- 学生课程表(中间表) create table tb_student_course( id int auto_increment comment '主键' primary key, student_id int not null comment '学生ID', course_id int not null comment '课程ID', constraint fk_courseid foreign key (course_id) references tb_course (id), constraint fk_studentid foreign key (student_id) references tb_student (id) )comment '学生课程中间表'; -- 学生课程表测试数据 insert into tb_student_course(student_id, course_id) values (1,1),(1,2),(1,3),(2,2),(2,3),(3,4);
1.4 案例
下面通过一个综合案例加深对于多表关系的理解,并掌握多表设计的流程。
需求
步骤
-
阅读页面原型及需求文档,分析各个模块涉及到的表结构,及表结构之间的关系。
-
根据页面原型及需求文档,分析各个表结构中具体的字段及约束。
分析
-
页面原型-分类管理
分类的信息:分类名称、分类类型[菜品/套餐]、分类排序、分类状态[禁用/启用]、分类的操作时间(修改时间)。
-
页面原型-菜品管理
菜品的信息:菜品名称、菜品图片、菜品分类、菜品售价、菜品售卖状态、菜品的操作时间(修改时间)。
思考:分类与菜品之间是什么关系?
思考逻辑:一个分类下可以有多个菜品吗?反过来再想一想,一个菜品会对应多个分类吗?
答案:一对多关系。一个分类下会有多个菜品,而一个菜品只能归属一个分类。
设计表原则:在多的一方,添加字段,关联属于一这方的主键。
-
页面原型-套餐管理
套餐的信息:套餐名称、套餐图片、套餐分类、套餐价格、套餐售卖状态、套餐的操作时间。
思考:套餐与菜品之间是什么关系?
思考逻辑:一个套餐下可以有多个菜品吗?反过来再想一想,一个菜品可以出现在多个套餐中吗?
答案:多对多关系。一个套餐下会有多个菜品,而一个菜品也可以出现在多个套餐中。
设计表原则:创建第三张中间表,建立两个字段分别关联菜品表的主键和套餐表的主键。
分析页面原型及需求文档后,我们获得:
-
分类表
-
业务字段:分类名称、分类类型、分类排序、分类状态
-
基础字段:id(主键)、分类的创建时间、分类的修改时间
-
-
菜品表
-
业务字段:菜品名称、菜品图片、菜品分类、菜品售价、菜品售卖状态
-
基础字段:id(主键)、分类的创建时间、分类的修改时间
-
-
套餐表
-
业务字段:套餐名称、套餐图片、套餐分类、套餐价格、套餐售卖状态
-
基础字段:id(主键)、分类的创建时间、分类的修改时间
-
表结构之间的关系:
-
分类表 - 菜品表 : 一对多
-
在菜品表中添加字段(菜品分类),关联分类表
-
-
菜品表 - 套餐表 : 多对多
-
创建第三张中间表(套餐菜品关联表),在中间表上添加两个字段(菜品id、套餐id),分别关联菜品表和分类表
-
表结构
分类表:category
-
业务字段:分类名称、分类类型、分类排序、分类状态
-
基础字段:id(主键)、创建时间、修改时间
-- 分类表 create table category ( id int unsigned primary key auto_increment comment '主键ID', name varchar(20) not null unique comment '分类名称', type tinyint unsigned not null comment '类型 1 菜品分类 2 套餐分类', sort tinyint unsigned not null comment '顺序', status tinyint unsigned not null default 0 comment '状态 0 禁用,1 启用', create_time datetime not null comment '创建时间', update_time datetime not null comment '更新时间' ) comment '菜品及套餐分类';
菜品表:dish
-
业务字段:菜品名称、菜品图片、菜品分类、菜品售价、菜品售卖状态
-
基础字段:id(主键)、分类的创建时间、分类的修改时间
-- 菜品表 create table dish ( id int unsigned primary key auto_increment comment '主键ID', name varchar(20) not null unique comment '菜品名称', category_id int unsigned not null comment '菜品分类ID', -- 逻辑外键 price decimal(8, 2) not null comment '菜品价格', image varchar(300) not null comment '菜品图片', description varchar(200) comment '描述信息', status tinyint unsigned not null default 0 comment '状态, 0 停售 1 起售', create_time datetime not null comment '创建时间', update_time datetime not null comment '更新时间' ) comment '菜品';
套餐表:setmeal
-
业务字段:套餐名称、套餐图片、套餐分类、套餐价格、套餐售卖状态
-
基础字段:id(主键)、分类的创建时间、分类的修改时间
-- 套餐表 create table setmeal ( id int unsigned primary key auto_increment comment '主键ID', name varchar(20) not null unique comment '套餐名称', category_id int unsigned not null comment '分类id', -- 逻辑外键 price decimal(8, 2) not null comment '套餐价格', image varchar(300) not null comment '图片', description varchar(200) comment '描述信息', status tinyint unsigned not null default 0 comment '状态 0:停用 1:启用', create_time datetime not null comment '创建时间', update_time datetime not null comment '更新时间' ) comment '套餐';
套餐菜品关联表:setmeal_dish
-- 套餐菜品关联表 create table setmeal_dish ( id int unsigned primary key auto_increment comment '主键ID', setmeal_id int unsigned not null comment '套餐id ', -- 逻辑外键 dish_id int unsigned not null comment '菜品id', -- 逻辑外键 copies tinyint unsigned not null comment '份数' ) comment '套餐菜品关联表';
2. 多表查询
2.1 概述
2.1.1 数据准备
SQL脚本:
#建议:创建新的数据库 create database db04; use db04; -- 部门表 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());
1.1.2 介绍
多表查询:查询时从多张表中获取所需数据
单表查询的SQL语句:select 字段列表 from 表名;
那么要执行多表查询,只需要使用逗号分隔多张表即可,如: select 字段列表 from 表1, 表2;
查询用户表和部门表中的数据:
select * from tb_emp , tb_dept;
此时,我们看到查询结果中包含了大量的结果集,总共85条记录,而这其实就是员工表所有的记录(17行)与部门表所有记录(5行)的所有组合情况,这种现象称之为笛卡尔积。
笛卡尔积:笛卡尔乘积是指在数学中,两个集合(A集合和B集合)的所有组合情况。
在多表查询时,需要消除无效的笛卡尔积,只保留表关联部分的数据
在SQL语句中,如何去除无效的笛卡尔积呢?只需要给多表查询加上连接查询的条件即可。
select * from tb_emp , tb_dept where tb_emp.dept_id = tb_dept.id ;
由于id为17的员工,没有dept_id字段值,所以在多表查询时,根据连接查询的条件并没有查询到。
1.1.3 分类
多表查询可以分为:
-
连接查询
-
内连接:相当于查询A、B交集部分数据
-
-
外连接
-
左外连接:查询左表所有数据(包括两张表交集部分数据)
-
右外连接:查询右表所有数据(包括两张表交集部分数据)
-
-
子查询
2.2 内连接
内连接查询:查询两表或多表中交集部分数据。
内连接从语法上可以分为:
-
隐式内连接
-
显式内连接
隐式内连接语法:
select 字段列表 from 表1 , 表2 where 条件 ... ;
显式内连接语法:
select 字段列表 from 表1 [ inner ] join 表2 on 连接条件 ... ;
案例:查询员工的姓名及所属的部门名称
-
隐式内连接实现
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;
多表查询时给表起别名:
-
tableA as 别名1 , tableB as 别名2 ;
-
tableA 别名1 , tableB 别名2 ;
使用了别名的多表查询:
select emp.name , dept.name from tb_emp emp inner join tb_dept dept on emp.dept_id = dept.id;
注意事项:
一旦为表起了别名,就不能再使用表名来指定对应的字段了,此时只能够使用别名来指定字段。
2.3 外连接
外连接分为两种:左外连接 和 右外连接。
左外连接语法结构:
select 字段列表 from 表1 left [ outer ] join 表2 on 连接条件 ... ;
左外连接相当于查询表1(左表)的所有数据,当然也包含表1和表2交集部分的数据。
右外连接语法结构:
select 字段列表 from 表1 right [ outer ] join 表2 on 连接条件 ... ;
右外连接相当于查询表2(右表)的所有数据,当然也包含表1和表2交集部分的数据。
案例:查询员工表中所有员工的姓名, 和对应的部门名称
-- 左外连接:以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;
注意事项:
左外连接和右外连接是可以相互替换的,只需要调整连接查询时SQL语句中表的先后顺序就可以了。而我们在日常开发使用时,更偏向于左外连接。
2.4 子查询
2.4.1 介绍
SQL语句中嵌套select语句,称为嵌套查询,又称子查询。
SELECT * FROM t1 WHERE column1 = ( SELECT column1 FROM t2 ... );
子查询外部的语句可以是insert / update / delete / select 的任何一个,最常见的是 select。
根据子查询结果的不同分为:
-
标量子查询(子查询结果为单个值[一行一列])
-
列子查询(子查询结果为一列,但可以是多行)
-
行子查询(子查询结果为一行,但可以是多列)
-
表子查询(子查询结果为多行多列[相当于子查询结果是一张表])
子查询可以书写的位置:
-
where之后
-
from之后
-
select之后
2.4.2 标量子查询
子查询返回的结果是单个值(数字、字符串、日期等),最简单的形式,这种子查询称为标量子查询。
常用的操作符: = <> > >= < <=
案例1:查询"教研部"的所有员工信息
可以将需求分解为两步:
查询 "教研部" 部门ID
根据 "教研部" 部门ID,查询员工信息
-- 1.查询"教研部"部门ID select id from tb_dept where name = '教研部'; #查询结果:2 -- 2.根据"教研部"部门ID, 查询员工信息 select * from tb_emp where dept_id = 2; -- 合并出上两条SQL语句 select * from tb_emp where dept_id = (select id from tb_dept where name = '教研部');
案例2:查询在 "方东白" 入职之后的员工信息
可以将需求分解为两步:
查询 方东白 的入职日期
查询 指定入职日期之后入职的员工信息
-- 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 = '方东白');
2.4.3 列子查询
子查询返回的结果是一列(可以是多行),这种子查询称为列子查询。
常用的操作符:
操作符 | 描述 |
---|---|
IN | 在指定的集合范围之内,多选一 |
NOT IN | 不在指定的集合范围之内 |
案例:查询"教研部"和"咨询部"的所有员工信息
分解为以下两步:
查询 "销售部" 和 "市场部" 的部门ID
根据部门ID, 查询员工信息
-- 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 = '咨询部');
2.4.4 行子查询
子查询返回的结果是一行(可以是多列),这种子查询称为行子查询。
常用的操作符:= 、<> 、IN 、NOT IN
案例:查询与"韦一笑"的入职日期及职位都相同的员工信息
可以拆解为两步进行:
查询 "韦一笑" 的入职日期 及 职位
查询与"韦一笑"的入职日期及职位相同的员工信息
-- 查询"韦一笑"的入职日期 及 职位 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 = '韦一笑');
2.4.5 表子查询
子查询返回的结果是多行多列,常作为临时表,这种子查询称为表子查询。
案例:查询入职日期是 "2006-01-01" 之后的员工信息 , 及其部门信息
分解为两步执行:
查询入职日期是 "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 ;
2.5 案例
基于之前设计的多表案例的表结构,我们来完成今天的多表查询案例需求。
准备环境
将资料中准备好的多表查询的数据准备的SQL脚本导入数据库中。
-
分类表:category
-
菜品表:dish
-
套餐表:setmeal
-
套餐菜品关系表:setmeal_dish
需求实现
-
查询价格低于 10元 的菜品的名称 、价格 及其 菜品的分类名称
/*查询技巧: 明确1:查询需要用到哪些字段 菜品名称、菜品价格 、 菜品分类名 明确2:查询的字段分别归属于哪张表 菜品表:[菜品名称、菜品价格] 分类表:[分类名] 明确3:如查多表,建立表与表之间的关联 菜品表.caategory_id = 分类表.id 其他:(其他条件、其他要求) 价格 < 10 */ select d.name , d.price , c.name from dish AS d , category AS c where d.category_id = c.id and d.price < 10;
-
查询所有价格在 10元(含)到50元(含)之间 且 状态为"起售"的菜品名称、价格及其分类名称 (即使菜品没有分类 , 也要将菜品查询出来)
select d.name , d.price, c.name from dish AS d left join category AS c on d.category_id = c.id where d.price between 10 and 50 and d.status = 1;
-
查询每个分类下最贵的菜品, 展示出分类的名称、最贵的菜品的价格
select c.name , max(d.price) from dish AS d , category AS c where d.category_id = c.id group by c.name;
-
查询各个分类下 菜品状态为 "起售" , 并且 该分类下菜品总数量大于等于3 的 分类名称
/*查询技巧: 明确1:查询需要用到哪些字段 分类名称、菜品总数量 明确2:查询用到的字段分别归属于哪张表 分类表:[分类名] 菜品表:[菜品状态] 明确3:如查多表,建立表与表之间的关联 菜品表.caategory_id = 分类表.id 其他:(其他条件、其他要求) 条件:菜品状态 = 1 (1表示起售) 分组:分类名 分组后条件: 总数量 >= 3 */ select c.name , count(*) from dish AS d , category AS c where d.category_id = c.id and d.status = 1 -- 起售状态 group by c.name -- 按照分类名分组 having count(*)>=3; -- 各组后筛选菜品总数据>=3
-
查询出 "商务套餐A" 中包含了哪些菜品 (展示出套餐名称、价格, 包含的菜品名称、价格、份数)
select s.name, s.price, d.name, d.price, sd.copies from setmeal AS s , setmeal_dish AS sd , dish AS d where s.id = sd.setmeal_id and sd.dish_id = d.id and s.name='商务套餐A';
-
查询出低于菜品平均价格的菜品信息 (展示出菜品名称、菜品价格)
-- 1.计算菜品平均价格 select avg(price) from dish; -- 查询结果:37.736842 -- 2.查询出低于菜品平均价格的菜品信息 select * from dish where price < 37.736842; -- 合并以上两条SQL语句 select * from dish where price < (select avg(price) from dish);
DAY04
1. 事务
场景:学工部整个部门解散了,该部门及部门下的员工都需要删除了。
-
操作:
-- 删除学工部 delete from dept where id = 1; -- 删除成功 -- 删除学工部的员工 delete from emp where dept_id = 1; -- 删除失败(操作过程中出现错误:造成删除没有成功)
-
问题:如果删除部门成功了,而删除该部门的员工时失败了,此时就造成了数据的不一致。
要解决上述的问题,就需要通过数据库中的事务来解决。
1.1 介绍
在实际的业务开发中,有些业务操作要多次访问数据库。一个业务要发送多条SQL语句给数据库执行。需要将多次访问数据库的操作视为一个整体来执行,要么所有的SQL语句全部执行成功。如果其中有一条SQL语句失败,就进行事务的回滚,所有的SQL语句全部执行失败。
简而言之:事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
事务作用:保证在一个事务中多次操作数据库表中数据时,要么全都成功,要么全都失败。
1.2 操作
MYSQL中有两种方式进行事务的操作:
-
自动提交事务:即执行一条sql语句提交一次事务。(默认MySQL的事务是自动提交)
-
手动提交事务:先开启,再提交
事务操作有关的SQL语句:
SQL语句 | 描述 |
---|---|
start transaction; / begin ; | 开启手动控制事务 |
commit; | 提交事务 |
rollback; | 回滚事务 |
手动提交事务使用步骤:
第1种情况:开启事务 => 执行SQL语句 => 成功 => 提交事务
第2种情况:开启事务 => 执行SQL语句 => 失败 => 回滚事务
使用事务控制删除部门和删除该部门下的员工的操作:
-- 开启事务 start transaction ; -- 删除学工部 delete from tb_dept where id = 1; -- 删除学工部的员工 delete from tb_emp where dept_id = 1;
-
上述的这组SQL语句,如果如果执行成功,则提交事务
-- 提交事务 (成功时执行) commit ;
-
上述的这组SQL语句,如果如果执行失败,则回滚事务
-- 回滚事务 (出错时执行) rollback ;
1.3 四大特性
面试题:事务有哪些特性?
-
原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败。
-
一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
-
隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
-
持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
事务的四大特性简称为:ACID
-
原子性(Atomicity) :原子性是指事务包装的一组sql是一个不可分割的工作单元,事务中的操作要么全部成功,要么全部失败。
-
一致性(Consistency):一个事务完成之后数据都必须处于一致性状态。
如果事务成功的完成,那么数据库的所有变化将生效。
如果事务执行出现错误,那么数据库的所有变化将会被回滚(撤销),返回到原始状态。
-
隔离性(Isolation):多个用户并发的访问数据库时,一个用户的事务不能被其他用户的事务干扰,多个并发的事务之间要相互隔离。
一个事务的成功或者失败对于其他的事务是没有影响。
-
持久性(Durability):一个事务一旦被提交或回滚,它对数据库的改变将是永久性的,哪怕数据库发生异常,重启之后数据亦然存在。
2. 索引
2.1 介绍
索引(index):是帮助数据库高效获取数据的数据结构 。
-
简单来讲,就是使用索引可以提高查询的效率。
测试没有使用索引的查询:
添加索引后查询:
-- 添加索引 create index idx_sku_sn on tb_sku (sn); #在添加索引时,也需要消耗时间 -- 查询数据(使用了索引) select * from tb_sku where sn = '100000003145008';
优点:
-
提高数据查询的效率,降低数据库的IO成本。
-
通过索引列对数据进行排序,降低数据排序的成本,降低CPU消耗。
缺点:
-
索引会占用存储空间。
-
索引大大提高了查询效率,同时却也降低了insert、update、delete的效率。
2.2 结构
MySQL数据库支持的索引结构有很多,如:Hash索引、B+Tree索引、Full-Text索引等。
我们平常所说的索引,如果没有特别指明,都是指默认的 B+Tree 结构组织的索引。
在没有了解B+Tree结构前,我们先回顾下之前所学习的树结构:
二叉查找树:左边的子节点比父节点小,右边的子节点比父节点大
当我们向二叉查找树保存数据时,是按照从大到小(或从小到大)的顺序保存的,此时就会形成一个单向链表,搜索性能会打折扣。
可以选择平衡二叉树或者是红黑树来解决上述问题。(红黑树也是一棵平衡的二叉树)
但是在Mysql数据库中并没有使用二叉搜索数或二叉平衡数或红黑树来作为索引的结构。
思考:采用二叉搜索树或者是红黑树来作为索引的结构有什么问题?
答案
说明:如果数据结构是红黑树,那么查询1000万条数据,根据计算树的高度大概是23左右,这样确实比之前的方式快了很多,但是如果高并发访问,那么一个用户有可能需要23次磁盘IO,那么100万用户,那么会造成效率极其低下。所以为了减少红黑树的高度,那么就得增加树的宽度,就是不再像红黑树一样每个节点只能保存一个数据,可以引入另外一种数据结构,一个节点可以保存多个数据,这样宽度就会增加从而降低树的高度。这种数据结构例如BTree就满足。
下面我们来看看B+Tree(多路平衡搜索树)结构中如何避免这个问题:
B+Tree结构:
-
每一个节点,可以存储多个key(有n个key,就有n个指针)
-
节点分为:叶子节点、非叶子节点
-
叶子节点,就是最后一层子节点,所有的数据都存储在叶子节点上
-
非叶子节点,不是树结构最下面的节点,用于索引数据,存储的的是:key+指针
-
-
为了提高范围查询效率,叶子节点形成了一个双向链表,便于数据的排序及区间范围查询
拓展:
非叶子节点都是由key+指针域组成的,一个key占8字节,一个指针占6字节,而一个节点总共容量是16KB,那么可以计算出一个节点可以存储的元素个数:16*1024字节 / (8+6)=1170个元素。
查看mysql索引节点大小:show global status like 'innodb_page_size'; -- 节点大小:16384
当根节点中可以存储1170个元素,那么根据每个元素的地址值又会找到下面的子节点,每个子节点也会存储1170个元素,那么第二层即第二次IO的时候就会找到数据大概是:1170*1170=135W。也就是说B+Tree数据结构中只需要经历两次磁盘IO就可以找到135W条数据。
对于第二层每个元素有指针,那么会找到第三层,第三层由key+数据组成,假设key+数据总大小是1KB,而每个节点一共能存储16KB,所以一个第三层一个节点大概可以存储16个元素(即16条记录)。那么结合第二层每个元素通过指针域找到第三层的节点,第二层一共是135W个元素,那么第三层总元素大小就是:135W*16结果就是2000W+的元素个数。
结合上述分析B+Tree有如下优点:
千万条数据,B+Tree可以控制在小于等于3的高度
所有的数据都存储在叶子节点上,并且底层已经实现了按照索引进行排序,还可以支持范围查询,叶子节点是一个双向链表,支持从小到大或者从大到小查找
2.3 语法
创建索引
create [ unique ] index 索引名 on 表名 (字段名,... ) ;
案例:为tb_emp表的name字段建立一个索引
create index idx_emp_name on tb_emp(name);
在创建表时,如果添加了主键和唯一约束,就会默认创建:主键索引、唯一约束
查看索引
show index from 表名;
案例:查询 tb_emp 表的索引信息
show index from tb_emp;
删除索引
drop index 索引名 on 表名;
案例:删除 tb_emp 表中name字段的索引
drop index idx_emp_name on tb_emp;
注意事项:
主键字段,在建表时,会自动创建主键索引
添加唯一约束时,数据库实际上会添加唯一索引
DAY05
1. Mybatis基础操作
学习完mybatis入门后,我们继续学习mybatis基础操作。
1.1 需求
需求说明:
-
根据资料中提供的《tlias智能学习辅助系统》页面原型及需求,完成员工管理的需求开发。
通过分析以上的页面原型和需求,我们确定了功能列表:
-
查询
-
根据主键ID查询
-
条件查询
-
-
新增
-
更新
-
删除
-
根据主键ID删除
-
根据主键ID批量删除
-
1.2 准备
实施前的准备工作:
-
准备数据库表
-
创建一个新的springboot工程,选择引入对应的起步依赖(mybatis、mysql驱动、lombok)
-
application.properties中引入数据库连接信息
-
创建对应的实体类 Emp(实体类属性采用驼峰命名)
-
准备Mapper接口 EmpMapper
准备数据库表
-- 部门管理 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());
创建一个新的springboot工程,选择引入对应的起步依赖(mybatis、mysql驱动、lombok)
application.properties中引入数据库连接信息
提示:可以把之前项目中已有的配置信息复制过来即可
#驱动类名称 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
创建对应的实体类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; }
准备Mapper接口:EmpMapper
/*@Mapper注解:表示当前接口为mybatis中的Mapper接口 程序运行时会自动创建接口的实现类对象(代理对象),并交给Spring的IOC容器管理 */ @Mapper public interface EmpMapper { }
完成以上操作后,项目工程结构目录如下:
1.3 删除
1.3.1 功能实现
页面原型:
当我们点击后面的"删除"按钮时,前端页面会给服务端传递一个参数,也就是该行数据的ID。 我们接收到ID后,根据ID删除数据即可。
功能:根据主键删除数据
-
SQL语句
-- 删除id=17的数据 delete from emp where id = 17;
Mybatis框架让程序员更关注于SQL语句
-
接口方法
@Mapper public interface EmpMapper { //@Delete("delete from emp where id = 17") //public void delete(); //以上delete操作的SQL语句中的id值写成固定的17,就表示只能删除id=17的用户数据 //SQL语句中的id值不能写成固定数值,需要变为动态的数值 //解决方案:在delete方法中添加一个参数(用户id),将方法中的参数,传给SQL语句 /** * 根据id删除数据 * @param id 用户id */ @Delete("delete from emp where id = #{id}")//使用#{key}方式获取方法中的参数值 public void delete(Integer id); }
@Delete注解:用于编写delete操作的SQL语句
如果mapper接口方法形参只有一个普通类型的参数,#{…} 里面的属性名可以随便写,如:#{id}、#{value}。但是建议保持名字一致。
-
测试
-
在单元测试类中通过@Autowired注解注入EmpMapper类型对象
-
@SpringBootTest class SpringbootMybatisCrudApplicationTests { @Autowired //从Spring的IOC容器中,获取类型是EmpMapper的对象并注入 private EmpMapper empMapper; @Test public void testDel(){ //调用删除方法 empMapper.delete(16); } }
1.3.2 日志输入
在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。具体操作如下:
-
打开application.properties文件
-
开启mybatis的日志,并指定输出到控制台
#指定mybatis输出日志的位置, 输出控制台 mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
开启日志之后,我们再次运行单元测试,可以看到在控制台中,输出了以下的SQL语句信息:
但是我们发现输出的SQL语句:delete from emp where id = ?,我们输入的参数16并没有在后面拼接,id的值是使用?进行占位。那这种SQL语句我们称为预编译SQL。
1.3.3 预编译SQL
1.3.3.1 介绍
预编译SQL有两个优势:
-
性能更高
-
更安全(防止SQL注入)
性能更高:预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)
更安全(防止SQL注入):将敏感字进行转义,保障SQL的安全性。
1.3.3.2 SQL注入
SQL注入:是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。
由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。
测试1:使用资料中提供的程序,来验证SQL注入问题
第1步:进入到DOS
第2步:执行以下命令,启动程序
#启动存在SQL注入的程序 java -jar sql_Injection_demo-0.0.1-SNAPSHOT.jar
第3步:打开浏览器输入http://localhost:9090/login.html
发现竟然能够登录成功:
以上操作为什么能够登录成功呢?
-
由于没有对用户输入内容进行充分检查,而SQL又是字符串拼接方式而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,从而完成恶意攻击。
用户在页面提交数据的时候人为的添加一些特殊字符,使得sql语句的结构发生了变化,最终可以在没有用户名或者密码的情况下进行登录。
测试2:使用资料中提供的程序,来验证SQL注入问题
第1步:进入到DOS
第2步:执行以下命令,启动程序:
#启动解决了SQL注入的程序 java -jar sql_prepared_demo-0.0.1-SNAPSHOT.jar
第3步:打开浏览器输入http://localhost:9090/login.html
发现无法登录:
以上操作SQL语句的执行:
把整个
' or '1'='1
作为一个完整的参数,赋值给第2个问号(' or '1'='1
进行了转义,只当做字符串使用)
1.3.3.3 参数占位符
在Mybatis中提供的参数占位符有两种:${...} 、#{...}
-
#{...}
-
执行SQL时,会将#{…}替换为?,生成预编译SQL,会自动设置参数值
-
使用时机:参数传递,都使用#{…}
-
-
${...}
-
拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题
-
使用时机:如果对表名、列表进行动态设置时使用
-
注意事项:在项目开发中,建议使用#{...},生成预编译SQL,防止SQL注入安全。
1.4 新增
功能:新增员工信息
1.4.1 基本新增
员工表结构:
SQL语句:
insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values ('songyuanqiao','宋远桥',1,'1.jpg',2,'2012-10-09',2,'2022-10-01 10:00:00','2022-10-01 10:00:00');
接口方法:
@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); } }
日志输出:
1.4.2 主键返回
概念:在数据添加成功后,需要获取插入数据库数据的主键。
如:添加套餐数据时,还需要维护套餐菜品关系表数据。
业务场景:在前面讲解到的苍穹外卖菜品与套餐模块的表结构,菜品与套餐是多对多的关系,一个套餐对应多个菜品。既然是多对多的关系,是不是有一张套餐菜品中间表来维护它们之间的关系。
在添加套餐的时候,我们需要在界面当中来录入套餐的基本信息,还需要来录入套餐与菜品的关联信息。这些信息录入完毕之后,我们一点保存,就需要将套餐的信息以及套餐与菜品的关联信息都需要保存到数据库当中。其实具体的过程包括两步,首先第一步先需要将套餐的基本信息保存了,接下来第二步再来保存套餐与菜品的关联信息。套餐与菜品的关联信息就是往中间表当中来插入数据,来维护它们之间的关系。而中间表当中有两个外键字段,一个是菜品的ID,就是当前菜品的ID,还有一个就是套餐的ID,而这个套餐的 ID 指的就是此次我所添加的套餐的ID,所以我们在第一步保存完套餐的基本信息之后,就需要将套餐的主键值返回来供第二步进行使用。这个时候就需要用到主键返回功能。
那要如何实现在插入数据之后返回所插入行的主键值呢?
-
默认情况下,执行插入操作时,是不会主键值返回的。如果我们想要拿到主键值,需要在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); }
测试:
@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()); } }
1.5 更新
功能:修改员工信息
点击"编辑"按钮后,会查询所在行记录的员工信息,并把员工信息回显在修改员工的窗体上(下个知识点学习)
在修改员工的窗体上,可以修改的员工数据:用户名、员工姓名、性别、图像、职位、入职日期、归属部门
思考:在修改员工数据时,要以什么做为条件呢?
答案:员工id
SQL语句:
update emp set username = 'linghushaoxia', name = '令狐少侠', gender = 1 , image = '1.jpg' , job = 2, entrydate = '2012-01-01', dept_id = 2, update_time = '2022-10-01 12:12:12' where id = 18;
接口方法:
@Mapper public interface EmpMapper { /** * 根据id修改员工信息 * @param emp */ @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); } }
1.6 查询
1.6.1 根据ID查询
在员工管理的页面中,当我们进行更新数据时,会点击 “编辑” 按钮,然后此时会发送一个请求到服务端,会根据Id查询该员工信息,并将员工数据回显在页面上。
SQL语句:
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp;
接口方法:
@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); } }
执行结果:
而在测试的过程中,我们会发现有几个字段(deptId、createTime、updateTime)是没有数据值的
1.6.2 数据封装
我们看到查询返回的结果中大部分字段是有值的,但是deptId,createTime,updateTime这几个字段是没有值的,而数据库中是有对应的字段值的,这是为什么呢?
原因如下:
-
实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装。
-
如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。
解决方案:
-
起别名
-
结果映射
-
开启驼峰命名
起别名:在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(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会自动通过驼峰命名规则映射
驼峰命名规则: abc_xyz => abcXyz
表中字段名:abc_xyz
类中属性名:abcXyz
# 在application.properties中添加: mybatis.configuration.map-underscore-to-camel-case=true
要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。
1.6.3 条件查询
在员工管理的列表页面中,我们需要根据条件查询员工信息,查询条件包括:姓名、性别、入职时间。
通过页面原型以及需求描述我们要实现的查询:
-
姓名:要求支持模糊匹配
-
性别:要求精确匹配
-
入职时间:要求进行范围查询
-
根据最后修改时间进行降序排序
SQL语句:
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp where name like '%张%' and gender = 1 and entrydate between '2010-01-01' and '2020-01-01 ' order by update_time desc;
接口方法:
-
方式一
@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); }
以上方式注意事项:
方法中的形参名和SQL语句中的参数占位符名保持一致
模糊查询使用${...}进行字符串拼接,这种方式呢,由于是字符串拼接,并不是预编译的形式,所以效率不高、且存在sql注入风险。
-
方式二(解决SQL注入风险)
-
使用MySQL提供的字符串拼接函数:concat('%' , '关键字' , '%')
-
@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都是预编译的SQL语句(性能高、安全)
1.6.4 参数名说明
在上面我们所编写的条件查询功能中,我们需要保证接口中方法的形参名和SQL语句中的参数占位符名相同。
当方法中的形参名和SQL语句中的占位符参数名不相同时,就会出现以下问题:
参数名在不同的SpringBoot版本中,处理方案还不同:
-
在springBoot的2.x版本(保证参数名一致)
springBoot的父工程对compiler编译插件进行了默认的参数parameters配置,使得在编译时,会在生成的字节码文件中保留原方法形参的名称,所以#{…}里面可以直接通过形参名获取对应的值
-
在springBoot的1.x版本/单独使用mybatis(使用@Param注解来指定SQL语句中的参数名)
在编译时,生成的字节码文件当中,不会保留Mapper接口中方法的形参名称,而是使用var1、var2、...这样的形参名字,此时要获取参数值时,就要通过@Param注解来指定SQL语句中的参数名
2. Mybatis的XML配置文件
Mybatis的开发有两种方式:
-
注解
-
XML
2.1 XML配置文件规范
使用Mybatis的注解方式,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件中。
在Mybatis中使用XML映射文件方式开发,需要符合一定的规范:
-
XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)
-
XML映射文件的namespace属性为Mapper接口全限定名一致
-
XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致。
<select>标签:就是用于编写select查询语句的。
resultType属性,指的是查询返回的单条记录所封装的类型。
2.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 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"> </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>
运行测试类,执行结果:
2.3 MybatisX的使用
MybatisX是一款基于IDEA的快速开发Mybatis的插件,为效率而生。
MybatisX的安装:
可以通过MybatisX快速定位:
MybatisX的使用在后续学习中会继续分享
学习了Mybatis中XML配置文件的开发方式了,大家可能会存在一个疑问:到底是使用注解方式开发还是使用XML方式开发?
官方说明:入门_MyBatis中文网
结论:使用Mybatis的注解,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句。
3. Mybatis动态SQL
3.1 什么是动态SQL
在页面原型中,列表上方的条件是动态的,是可以不传递的,也可以只传递其中的1个或者2个或者全部。
而在我们刚才编写的SQL语句中,我们会看到,我们将三个条件直接写死了。 如果页面只传递了参数姓名name 字段,其他两个字段 性别 和 入职时间没有传递,那么这两个参数的值就是null。
此时,执行的SQL语句为:
这个查询结果是不正确的。正确的做法应该是:传递了参数,再组装这个查询条件;如果没有传递参数,就不应该组装这个查询条件。
比如:如果姓名输入了"张", 对应的SQL为:
select * from emp where name like '%张%' order by update_time desc;
如果姓名输入了"张",,性别选择了"男",则对应的SQL为:
select * from emp where name like '%张%' and gender = 1 order by update_time desc;
SQL语句会随着用户的输入或外部条件的变化而变化,我们称为:动态SQL。
在Mybatis中提供了很多实现动态SQL的标签,我们学习Mybatis中的动态SQL就是掌握这些动态SQL标签。
3.2 动态SQL-if
<if>
:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。
<if test="条件表达式"> 要拼接的sql语句 </if>
接下来,我们就通过<if>
标签来改造之前条件查询的案例。
3.2.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); } }
执行的SQL语句:
下面呢,我们修改测试方法中的代码,再次进行测试,观察执行情况:
@Test public void testList(){ //姓名为null List<Emp> list = empMapper.list(null, (short)1, null, null); for(Emp emp : list){ System.out.println(emp); } }
执行结果:
再次修改测试方法中的代码,再次进行测试:
@Test public void testList(){ //传递的数据全部为null List<Emp> list = empMapper.list(null, null, null, null); for(Emp emp : list){ System.out.println(emp); } }
执行的SQL语句:
以上问题的解决方案:使用<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 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>
测试方法:
@Test public void testList(){ //只有性别 List<Emp> list = empMapper.list(null, (short)1, null, null); for(Emp emp : list){ System.out.println(emp); } }
执行的SQL语句:
3.2.2 更新员工
案例:完善更新员工功能,修改为动态更新员工数据信息
-
动态更新员工信息,如果更新时传递有值,则更新;如果更新时没有传递值,则不更新
-
解决方案:动态SQL
修改Mapper接口:
@Mapper public interface EmpMapper { //删除@Update注解编写的SQL语句 //update操作的SQL语句编写在Mapper映射文件中 public void update(Emp emp); }
修改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"> <!--更新操作--> <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); }
执行的SQL语句:
再次修改测试方法,观察SQL语句执行情况:
@Test public void testUpdate2(){ //要修改的员工信息 Emp emp = new Emp(); emp.setId(20); emp.setUsername("Tom222"); //调用方法,修改员工数据 empMapper.update(emp); }
执行的SQL语句:
以上问题的解决方案:使用<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>
再次执行测试方法,执行的SQL语句:
小结
-
<if>
-
用于判断条件是否成立,如果条件为true,则拼接SQL
-
形式:
<if test="name != null"> … </if>
-
-
<where>
-
where元素只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR
-
-
<set>
-
动态地在行首插入 SET 关键字,并会删掉额外的逗号。(用在update语句中)
-
3.3 动态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 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"> <!--删除操作--> <delete id="deleteByIds"> delete from emp where id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> </delete> </mapper>
执行的SQL语句:
3.4 动态SQL-sql&include
问题分析:
-
在xml映射文件中配置的SQL,有时可能会存在很多重复的片段,此时就会存在很多冗余的代码
我们可以对重复的代码片段进行抽取,将其通过<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>
DAY06
SpringBootWeb案例
在这个案例中,前端开发人员已经将前端工程开发完毕了。 我们需要做的,就是参考接口文档完成后端功能的开发,然后结合前端工程进行联调测试即可。
完成后的成品效果展示:
今天的主要内容如下:
准备工作
部门管理
员工管理
下面我们就进入到今天的第1个内容准备工作
的学习。
1. 准备工作
准备工作的学习,我们先从"需求"和"环境搭建"开始入手。
1.1 需求&环境搭建
1.1.1 需求说明
1、部门管理
部门管理功能开发包括:
-
查询部门列表
-
删除部门
-
新增部门
-
修改部门
2、员工管理
员工管理功能开发包括:
-
查询员工列表(分页、条件)
-
删除员工
-
新增员工
-
修改员工
1.1.2 环境搭建
步骤:
-
准备数据库表(dept、emp)
-
创建springboot工程,引入对应的起步依赖(web、mybatis、mysql驱动、lombok)
-
配置文件application.properties中引入mybatis的配置信息,准备对应的实体类
-
准备对应的Mapper、Service(接口、实现类)、Controller基础结构
第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,'2007-01-01',2,now(),now()), (17,'chenyouliang','123456','陈友谅',1,'17.jpg',NULL,'2015-03-21',NULL,now(),now());
第2步:创建一个SpringBoot工程,选择引入对应的起步依赖(web、mybatis、mysql驱动、lombok) (版本选择2.7.5版本,可以创建完毕之后,在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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.5</version> <relativePath/> </parent> <groupId>com.itheima</groupId> <artifactId>tlias-web-management</artifactId> <version>0.0.1-SNAPSHOT</version> <name>tlias-web-management</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
创建项目工程目录结构:
第3步:配置文件application.properties中引入mybatis的配置信息,准备对应的实体类
-
application.properties (直接把之前项目中的复制过来)
#数据库连接 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/tlias spring.datasource.username=root spring.datasource.password=1234 #开启mybatis的日志输出 mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl #开启数据库表字段 到 实体类属性的驼峰映射 mybatis.configuration.map-underscore-to-camel-case=true
-
实体类
/*部门类*/ @Data @NoArgsConstructor @AllArgsConstructor public class Dept { private Integer id; private String name; private LocalDateTime createTime; private LocalDateTime updateTime; }
/*员工类*/ @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; private Integer deptId; private LocalDateTime createTime; private LocalDateTime updateTime; }
第4步:准备对应的Mapper、Service(接口、实现类)、Controller基础结构
数据访问层:
-
DeptMapper
package com.itheima.mapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface DeptMapper { }
-
EmpMapper
package com.itheima.mapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface EmpMapper { }
业务层:
-
DeptService
package com.itheima.service; //部门业务规则 public interface DeptService { }
-
DeptServiceImpl
package com.itheima.service.impl; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; //部门业务实现类 @Slf4j @Service public class DeptServiceImpl implements DeptService { }
-
EmpService
package com.itheima.service; //员工业务规则 public interface EmpService { }
-
EmpServiceImpl
package com.itheima.service.impl; import com.itheima.service.EmpService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; //员工业务实现类 @Slf4j @Service public class EmpServiceImpl implements EmpService { }
控制层:
-
DeptController
package com.itheima.controller; import org.springframework.web.bind.annotation.RestController; //部门管理控制器 @RestController public class DeptController { }
-
EmpController
package com.itheima.controller; import org.springframework.web.bind.annotation.RestController; //员工管理控制器 @RestController public class EmpController { }
项目工程结构:
1.2 开发规范
了解完需求也完成了环境搭建了,我们下面开始学习开发的一些规范。
开发规范我们主要从以下几方面介绍:
1、开发规范-REST
我们的案例是基于当前最为主流的前后端分离模式进行开发。
在前后端分离的开发模式中,前后端开发人员都需要根据提前定义好的接口文档,来进行前后端功能的开发。
后端开发人员:必须严格遵守提供的接口文档进行后端功能开发(保障开发的功能可以和前端对接)
而在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。
什么是REST风格呢?
-
REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。
传统URL风格如下:
http://localhost:8080/user/getById?id=1 GET:查询id为1的用户 http://localhost:8080/user/saveUser POST:新增用户 http://localhost:8080/user/updateUser POST:修改用户 http://localhost:8080/user/deleteUser?id=1 GET:删除id为1的用户
我们看到,原始的传统URL呢,定义比较复杂,而且将资源的访问行为对外暴露出来了。
基于REST风格URL如下:
http://localhost:8080/users/1 GET:查询id为1的用户 http://localhost:8080/users POST:新增用户 http://localhost:8080/users PUT:修改用户 http://localhost:8080/users/1 DELETE:删除id为1的用户
其中总结起来,就一句话:通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。
在REST风格的URL中,通过四种请求方式,来操作数据的增删改查。
-
GET : 查询
-
POST :新增
-
PUT :修改
-
DELETE :删除
我们看到如果是基于REST风格,定义URL,URL将会更加简洁、更加规范、更加优雅。
注意事项:
REST是风格,是约定方式,约定不是规定,可以打破
描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:users、emps、books…
2、开发规范-统一响应结果
前后端工程在进行交互时,使用统一响应结果 Result。
package com.itheima.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Result { private Integer code;//响应码,1 代表成功; 0 代表失败 private String msg; //响应信息 描述字符串 private Object data; //返回的数据 //增删改 成功响应 public static Result success(){ return new Result(1,"success",null); } //查询 成功响应 public static Result success(Object data){ return new Result(1,"success",data); } //失败响应 public static Result error(String msg){ return new Result(0,msg,null); } }
3、开发流程
我们在进行功能开发时,都是根据如下流程进行:
-
查看页面原型明确需求
-
根据页面原型和需求,进行表结构设计、编写接口文档(已提供)
-
-
阅读接口文档
-
思路分析
-
功能接口开发
-
就是开发后台的业务功能,一个业务功能,我们称为一个接口
-
-
功能接口测试
-
功能开发完毕后,先通过Postman进行功能接口测试,测试通过后,再和前端进行联调测试
-
-
前后端联调测试
-
和前端开发人员开发好的前端工程一起测试
-
2. 部门管理
我们按照前面学习的开发流程,开始完成功能开发。首先按照之前分析的需求,完成部门管理
的功能开发。
开发的部门管理功能包含:
-
查询部门
-
删除部门
-
新增部门
-
更新部门
2.1 查询部门
2.1.1 原型和需求
查询的部门的信息:部门ID、部门名称、修改时间
通过页面原型以及需求描述,我们可以看到,部门查询,是不需要考虑分页操作的。
2.1.2 接口文档
部门列表查询
-
基本信息
请求路径:/depts 请求方式:GET 接口描述:该接口用于部门列表数据查询
-
请求参数
无
-
响应数据
参数格式:application/json
参数说明:
参数名 类型 是否必须 备注 code number 必须 响应码,1 代表成功,0 代表失败 msg string 非必须 提示信息 data object[ ] 非必须 返回的数据 |- id number 非必须 id |- name string 非必须 部门名称 |- createTime string 非必须 创建时间 |- updateTime string 非必须 修改时间 响应数据样例:
{ "code": 1, "msg": "success", "data": [ { "id": 1, "name": "学工部", "createTime": "2022-09-01T23:06:29", "updateTime": "2022-09-01T23:06:29" }, { "id": 2, "name": "教研部", "createTime": "2022-09-01T23:06:29", "updateTime": "2022-09-01T23:06:29" } ] }
2.1.3 思路分析
2.1.4 功能开发
通过查看接口文档:部门列表查询
请求路径:/depts
请求方式:GET
请求参数:无
响应数据:json格式
DeptController
@Slf4j @RestController public class DeptController { @Autowired private DeptService deptService; //@RequestMapping(value = "/depts" , method = RequestMethod.GET) @GetMapping("/depts") public Result list(){ log.info("查询所有部门数据"); List<Dept> deptList = deptService.list(); return Result.success(deptList); } }
@Slf4j注解源码:
DeptService(业务接口)
public interface DeptService { /** * 查询所有的部门数据 * @return 存储Dept对象的集合 */ List<Dept> list(); }
DeptServiceImpl(业务实现类)
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override public List<Dept> list() { List<Dept> deptList = deptMapper.list(); return deptList; } }
DeptMapper
@Mapper public interface DeptMapper { //查询所有部门数据 @Select("select id, name, create_time, update_time from dept") List<Dept> list(); }
2.1.5 功能测试
功能开发完成后,我们就可以启动项目,然后打开postman,发起GET请求,访问 :http://localhost:8080/depts
2.2 前后端联调
完成了查询部门的功能,我们也通过postman工具测试通过了,下面我们再基于前后端分离的方式进行接口联调。具体操作如下:
1、将资料中提供的"前端环境"文件夹中的压缩包,拷贝到一个没有中文不带空格的目录下
2、拷贝到一个没有中文不带空格的目录后,进行解压(解压到当前目录)
3、启动nginx
4、打开浏览器,访问:http://localhost:90
5、测试:部门管理 - 查询部门列表
说明:只要按照接口文档开发功能接口,就能保证前后端程序交互
后端:严格遵守接口文档进行功能接口开发
前端:严格遵守接口文档访问功能接口
2.3 删除部门
查询部门的功能我们搞定了,下面我们开始完成删除部门
的功能开发。
2.3.1 需求
点击部门列表后面操作栏的 "删除" 按钮,就可以删除该部门信息。 此时,前端只需要给服务端传递一个ID参数就可以了。 我们从接口文档中也可以看得出来。
2.3.2 接口文档
删除部门
-
基本信息
请求路径:/depts/{id} 请求方式:DELETE 接口描述:该接口用于根据ID删除部门数据
-
请求参数 参数格式:路径参数
参数说明:
参数名 类型 是否必须 备注 id number 必须 部门ID 请求参数样例:
/depts/1
-
响应数据 参数格式:application/json
参数说明:
参数名 类型 是否必须 备注 code number 必须 响应码,1 代表成功,0 代表失败 msg string 非必须 提示信息 data object 非必须 返回的数据 响应数据样例:
{ "code":1, "msg":"success", "data":null }
2.3.3 思路分析
接口文档规定:
前端请求路径:/depts/{id}
前端请求方式:DELETE
问题1:怎么在controller中接收请求路径中的路径参数?
@PathVariable问题2:如何限定请求方式是delete?
@DeleteMapping
2.3.4 功能开发
通过查看接口文档:删除部门
请求路径:/depts/{id}
请求方式:DELETE
请求参数:路径参数 {id}
响应数据:json格式
DeptController
@Slf4j @RestController public class DeptController { @Autowired private DeptService deptService; @DeleteMapping("/depts/{id}") public Result delete(@PathVariable Integer id) { //日志记录 log.info("根据id删除部门"); //调用service层功能 deptService.delete(id); //响应 return Result.success(); } //省略... }
DeptService
public interface DeptService { /** * 根据id删除部门 * @param id 部门id */ void delete(Integer id); //省略... }
DeptServiceImpl
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override public void delete(Integer id) { //调用持久层删除功能 deptMapper.deleteById(id); } //省略... }
DeptMapper
@Mapper public interface DeptMapper { /** * 根据id删除部门信息 * @param id 部门id */ @Delete("delete from dept where id = #{id}") void deleteById(Integer id); //省略... }
2.3.5 功能测试
删除功能开发完成后,重新启动项目,使用postman,发起DELETE请求:
2.3.6 前后端联调
打开浏览器,测试后端功能接口:
2.4 新增部门
我们前面已完成了查询部门
、删除部门
两个功能,也熟悉了开发的流程。下面我们继续完成新增部门
功能。
2.4.1 需求
点击 "新增部门" 按钮,弹出新增部门对话框,输入部门名称,点击 "保存" ,将部门信息保存到数据库。
2.4.2 接口文档
添加部门
-
基本信息
请求路径:/depts 请求方式:POST 接口描述:该接口用于添加部门数据
-
请求参数
格式:application/json
参数说明:
参数名 类型 是否必须 备注 name string 必须 部门名称 请求参数样例:
{ "name": "教研部" }
-
响应数据
参数格式:application/json
参数说明:
参数名 类型 是否必须 备注 code number 必须 响应码,1 代表成功,0 代表失败 msg string 非必须 提示信息 data object 非必须 返回的数据 响应数据样例:
{ "code":1, "msg":"success", "data":null }
2.4.3 思路分析
接口文档规定:
前端请求路径:/depts
前端请求方式:POST
前端请求参数 (Json格式):{ "name": "教研部" }
问题1:如何限定请求方式是POST?
@PostMapping问题2:怎么在controller中接收json格式的请求参数?
@RequestBody //把前端传递的json数据填充到实体类中
2.4.4 功能开发
通过查看接口文档:新增部门
请求路径:/depts
请求方式:POST
请求参数:json格式
响应数据:json格式
DeptController
@Slf4j @RestController public class DeptController { @Autowired private DeptService deptService; @PostMapping("/depts") public Result add(@RequestBody Dept dept){ //记录日志 log.info("新增部门:{}",dept); //调用service层添加功能 deptService.add(dept); //响应 return Result.success(); } //省略... }
DeptService
public interface DeptService { /** * 新增部门 * @param dept 部门对象 */ void add(Dept dept); //省略... }
DeptServiceImpl
@Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override public void add(Dept dept) { //补全部门数据 dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); //调用持久层增加功能 deptMapper.inser(dept); } //省略... }
DeptMapper
@Mapper public interface DeptMapper { @Insert("insert into dept (name, create_time, update_time) values (#{name},#{createTime},#{updateTime})") void inser(Dept dept); //省略... }
2.4.5 功能测试
新增功能开发完成后,重新启动项目,使用postman,发起POST请求:
2.4.6 前后端联调
打开浏览器,测试后端功能接口:
2.4.7 请求路径
我们部门管理的查询
、删除
、新增
功能全部完成了,接下来我们要对controller层的代码进行优化。
首先我们先来看下目前controller层代码:
以上三个方法上的请求路径,存在一个共同点:都是以
/depts
作为开头。(重复了)
在Spring当中为了简化请求路径的定义,可以把公共的请求路径,直接抽取到类上,在类上加一个注解@RequestMapping,并指定请求路径"/depts"。代码参照如下:
优化前后的对比:
注意事项:一个完整的请求路径,应该是类上@RequestMapping的value属性 + 方法上的 @RequestMapping的value属性
3. 员工管理
完成了部门管理的功能开发之后,我们进入到下一环节员工管理功能的开发。
基于以上原型,我们可以把员工管理功能分为:
-
分页查询(今天完成)
-
带条件的分页查询(今天完成)
-
删除员工(今天完成)
-
新增员工(后续完成)
-
修改员工(后续完成)
那下面我们就先从分页查询功能开始学习。
3.1 分页查询
3.1.1 基础分页
3.1.1.1 需求分析
我们之前做的查询功能,是将数据库中所有的数据查询出来并展示到页面上,试想如果数据库中的数据有很多(假设有十几万条)的时候,将数据全部展示出来肯定不现实,那如何解决这个问题呢?
使用分页解决这个问题。每次只展示一页的数据,比如:一页展示10条数据,如果还想看其他的数据,可以通过点击页码进行查询。
要想从数据库中进行分页查询,我们要使用LIMIT
关键字,格式为:limit 开始索引 每页显示的条数
查询第1页数据的SQL语句是:
select * from emp limit 0,10;查询第2页数据的SQL语句是:
select * from emp limit 10,10;查询第3页的数据的SQL语句是:
select * from emp limit 20,10;观察以上SQL语句,发现: 开始索引一直在改变 , 每页显示条数是固定的
开始索引的计算公式: 开始索引 = (当前页码 - 1) * 每页显示条数
我们继续基于页面原型,继续分析,得出以下结论:
-
前端在请求服务端时,传递的参数
-
当前页码 page
-
每页显示条数 pageSize
-
-
后端需要响应什么数据给前端
-
所查询到的数据列表(存储到List 集合中)
-
总记录数
-
后台给前端返回的数据包含:List集合(数据列表)、total(总记录数)
而这两部分我们通常封装到PageBean对象中,并将该对象转换为json格式的数据响应回给浏览器。
@Data @NoArgsConstructor @AllArgsConstructor public class PageBean { private Long total; //总记录数 private List rows; //当前页数据列表 }
3.1.1.2 接口文档
员工列表查询
-
基本信息
请求路径:/emps 请求方式:GET 接口描述:该接口用于员工列表数据的条件分页查询
-
请求参数
参数格式:queryString
参数说明:
参数名称 是否必须 示例 备注 name 否 张 姓名 gender 否 1 性别 , 1 男 , 2 女 begin 否 2010-01-01 范围匹配的开始时间(入职日期) end 否 2020-01-01 范围匹配的结束时间(入职日期) page 是 1 分页查询的页码,如果未指定,默认为1 pageSize 是 10 分页查询的每页记录数,如果未指定,默认为10 请求数据样例:
/emps?name=张&gender=1&begin=2007-09-01&end=2022-09-01&page=1&pageSize=10
-
响应数据
参数格式:application/json
参数说明:
名称 类型 是否必须 默认值 备注 其他信息 code number 必须 响应码, 1 成功 , 0 失败 msg string 非必须 提示信息 data object 必须 返回的数据 |- total number 必须 总记录数 |- rows object [] 必须 数据列表 item 类型: object |- id number 非必须 id |- username string 非必须 用户名 |- name string 非必须 姓名 |- password string 非必须 密码 |- entrydate string 非必须 入职日期 |- gender number 非必须 性别 , 1 男 ; 2 女 |- image string 非必须 图像 |- job number 非必须 职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师 |- deptId number 非必须 部门id |- createTime string 非必须 创建时间 |- updateTime string 非必须 更新时间 响应数据样例:
{ "code": 1, "msg": "success", "data": { "total": 2, "rows": [ { "id": 1, "username": "jinyong", "password": "123456", "name": "金庸", "gender": 1, "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg", "job": 2, "entrydate": "2015-01-01", "deptId": 2, "createTime": "2022-09-01T23:06:30", "updateTime": "2022-09-02T00:29:04" }, { "id": 2, "username": "zhangwuji", "password": "123456", "name": "张无忌", "gender": 1, "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg", "job": 2, "entrydate": "2015-01-01", "deptId": 2, "createTime": "2022-09-01T23:06:30", "updateTime": "2022-09-02T00:29:04" } ] } }
3.1.1.3 思路分析
分页查询需要的数据,封装在PageBean对象中:
3.1.1.4 功能开发
通过查看接口文档:员工列表查询
请求路径:/emps
请求方式:GET
请求参数:跟随在请求路径后的参数字符串。 例:/emps?page=1&pageSize=10
响应数据:json格式
EmpController
import com.itheima.pojo.PageBean; import com.itheima.pojo.Result; import com.itheima.service.EmpService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController @RequestMapping("/emps") public class EmpController { @Autowired private EmpService empService; //条件分页查询 @GetMapping public Result page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize) { //记录日志 log.info("分页查询,参数:{},{}", page, pageSize); //调用业务层分页查询功能 PageBean pageBean = empService.page(page, pageSize); //响应 return Result.success(pageBean); } }
@RequestParam(defaultValue="默认值") //设置请求参数默认值
EmpService
public interface EmpService { /** * 条件分页查询 * @param page 页码 * @param pageSize 每页展示记录数 * @return */ PageBean page(Integer page, Integer pageSize); }
EmpServiceImpl
import com.itheima.mapper.EmpMapper; import com.itheima.pojo.Emp; import com.itheima.pojo.PageBean; import com.itheima.service.EmpService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDate; import java.util.List; @Slf4j @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Override public PageBean page(Integer page, Integer pageSize) { //1、获取总记录数 Long count = empMapper.count(); //2、获取分页查询结果列表 Integer start = (page - 1) * pageSize; //计算起始索引 , 公式: (页码-1)*页大小 List<Emp> empList = empMapper.list(start, pageSize); //3、封装PageBean对象 PageBean pageBean = new PageBean(count , empList); return pageBean; } }
EmpMapper
@Mapper public interface EmpMapper { //获取总记录数 @Select("select count(*) from emp") public Long count(); //获取当前页的结果列表 @Select("select * from emp limit #{start}, #{pageSize}") public List<Emp> list(Integer start, Integer pageSize); }
3.1.1.5 功能测试
功能开发完成后,重新启动项目,使用postman,发起POST请求:
3.1.1.6 前后端联调
打开浏览器,测试后端功能接口:
3.1.2 分页插件
3.1.2.1 介绍
前面我们已经完了基础的分页查询,大家会发现:分页查询功能编写起来比较繁琐。
在Mapper接口中定义两个方法执行两条不同的SQL语句:
查询总记录数
指定页码的数据列表
在Service当中,调用Mapper接口的两个方法,分别获取:总记录数、查询结果列表,然后在将获取的数据结果封装到PageBean对象中。
大家思考下:在未来开发其他项目,只要涉及到分页查询功能(例:订单、用户、支付、商品),都必须按照以上操作完成功能开发
结论:原始方式的分页查询,存在着"步骤固定"、"代码频繁"的问题
解决方案:可以使用一些现成的分页插件完成。对于Mybatis来讲现在最主流的就是PageHelper。
PageHelper是Mybatis的一款功能强大、方便易用的分页插件,支持任何形式的单标、多表的分页查询。
在执行empMapper.list()方法时,就是执行:select * from emp 语句,怎么能够实现分页操作呢?
分页插件帮我们完成了以下操作:
先获取到要执行的SQL语句:select * from emp
把SQL语句中的字段列表,变为:count(*)
执行SQL语句:select count(*) from emp //获取到总记录数
再对要执行的SQL语句:select * from emp 进行改造,在末尾添加 limit ? , ?
执行改造后的SQL语句:select * from emp limit ? , ?
3.1.2.2 代码实现
当使用了PageHelper分页插件进行分页,就无需再Mapper中进行手动分页了。 在Mapper中我们只需要进行正常的列表查询即可。在Service层中,调用Mapper的方法之前设置分页参数,在调用Mapper方法执行查询之后,解析分页结果,并将结果封装到PageBean对象中返回。
1、在pom.xml引入依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.2</version> </dependency>
2、EmpMapper
@Mapper public interface EmpMapper { //获取当前页的结果列表 @Select("select * from emp") public List<Emp> page(Integer start, Integer pageSize); }
3、EmpServiceImpl
@Override public PageBean page(Integer page, Integer pageSize) { // 设置分页参数 PageHelper.startPage(page, pageSize); // 执行分页查询 List<Emp> empList = empMapper.list(name,gender,begin,end); // 获取分页结果 Page<Emp> p = (Page<Emp>) empList; //封装PageBean PageBean pageBean = new PageBean(p.getTotal(), p.getResult()); return pageBean; }
3.1.2.3 测试
功能开发完成后,我们重启项目工程,打开postman,发起GET请求,访问 :http://localhost:8080/emps?page=1&pageSize=5
后端程序SQL输出:
3.2 分页查询(带条件)
完了分页查询后,下面我们需要在分页查询的基础上,添加条件。
3.2.1 需求
通过员工管理的页面原型我们可以看到,员工列表页面的查询,不仅仅需要考虑分页,还需要考虑查询条件。 分页查询我们已经实现了,接下来,我们需要考虑在分页查询的基础上,再加上查询条件。
我们看到页面原型及需求中描述,搜索栏的搜索条件有三个,分别是:
-
姓名:模糊匹配
-
性别:精确匹配
-
入职日期:范围匹配
select * from emp where name like concat('%','张','%') -- 条件1:根据姓名模糊匹配 and gender = 1 -- 条件2:根据性别精确匹配 and entrydate = between '2000-01-01' and '2010-01-01' -- 条件3:根据入职日期范围匹配 order by update_time desc;
而且上述的三个条件,都是可以传递,也可以不传递的,也就是动态的。 我们需要使用前面学习的Mybatis中的动态SQL 。
3.2.2 思路分析
3.2.3 功能开发
通过查看接口文档:员工列表查询
请求路径:/emps
请求方式:GET
请求参数:
参数名称 是否必须 示例 备注 name 否 张 姓名 gender 否 1 性别 , 1 男 , 2 女 begin 否 2010-01-01 范围匹配的开始时间(入职日期) end 否 2020-01-01 范围匹配的结束时间(入职日期) page 是 1 分页查询的页码,如果未指定,默认为1 pageSize 是 10 分页查询的每页记录数,如果未指定,默认为10
在原有分页查询的代码基础上进行改造:
EmpController
@Slf4j @RestController @RequestMapping("/emps") public class EmpController { @Autowired private EmpService empService; //条件分页查询 @GetMapping public Result page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize, String name, Short gender, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { //记录日志 log.info("分页查询,参数:{},{},{},{},{},{}", page, pageSize,name, gender, begin, end); //调用业务层分页查询功能 PageBean pageBean = empService.page(page, pageSize, name, gender, begin, end); //响应 return Result.success(pageBean); } }
EmpService
public interface EmpService { /** * 条件分页查询 * @param page 页码 * @param pageSize 每页展示记录数 * @param name 姓名 * @param gender 性别 * @param begin 开始时间 * @param end 结束时间 * @return */ PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end); }
EmpServiceImpl
@Slf4j @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Override public PageBean page(Integer page, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) { //设置分页参数 PageHelper.startPage(page, pageSize); //执行条件分页查询 List<Emp> empList = empMapper.list(name, gender, begin, end); //获取查询结果 Page<Emp> p = (Page<Emp>) empList; //封装PageBean PageBean pageBean = new PageBean(p.getTotal(), p.getResult()); return pageBean; } }
EmpMapper
@Mapper public interface EmpMapper { //获取当前页的结果列表 public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end); }
EmpMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.itheima.mapper.EmpMapper"> <!-- 条件分页查询 --> <select id="list" resultType="com.itheima.pojo.Emp"> select * from emp <where> <if test="name != null and name != ''"> name like concat('%',#{name},'%') </if> <if test="gender != null"> and gender = #{gender} </if> <if test="begin != null and end != null"> and entrydate between #{begin} and #{end} </if> </where> order by update_time desc </select> </mapper>
3.2.4 功能测试
功能开发完成后,重启项目工程,打开postman,发起GET请求:
控制台SQL语句:
3.2.5 前后端联调
打开浏览器,测试后端功能接口:
3.3 删除员工
查询员完成之后,我们继续开发新的功能:删除员工。
3.3.1 需求
当我们勾选列表前面的复选框,然后点击 "批量删除" 按钮,就可以将这一批次的员工信息删除掉了。也可以只勾选一个复选框,仅删除一个员工信息。
问题:我们需要开发两个功能接口吗?一个删除单个员工,一个删除多个员工
答案:不需要。 只需要开发一个功能接口即可(删除多个员工包含只删除一个员工)
3.3.2 接口文档
删除员工
-
基本信息
请求路径:/emps/{ids} 请求方式:DELETE 接口描述:该接口用于批量删除员工的数据信息
-
请求参数
参数格式:路径参数
参数说明:
参数名 类型 示例 是否必须 备注 ids 数组 array 1,2,3 必须 员工的id数组 请求参数样例:
/emps/1,2,3
-
响应数据
参数格式:application/json
参数说明:
参数名 类型 是否必须 备注 code number 必须 响应码,1 代表成功,0 代表失败 msg string 非必须 提示信息 data object 非必须 返回的数据 响应数据样例:
{ "code":1, "msg":"success", "data":null }
3.3.3 思路分析
接口文档规定:
前端请求路径:/emps/{ids}
前端请求方式:DELETE
问题1:怎么在controller中接收请求路径中的路径参数?
@PathVariable问题2:如何限定请求方式是delete?
@DeleteMapping问题3:在Mapper接口中,执行delete操作的SQL语句时,条件中的id值是不确定的是动态的,怎么实现呢?
Mybatis中的动态SQL:foreach
3.3.4 功能开发
通过查看接口文档:删除员工
请求路径:/emps/{ids}
请求方式:DELETE
请求参数:路径参数 {ids}
响应数据:json格式
EmpController
@Slf4j @RestController @RequestMapping("/emps") public class EmpController { @Autowired private EmpService empService; //批量删除 @DeleteMapping("/{ids}") public Result delete(@PathVariable List<Integer> ids){ empService.delete(ids); return Result.success(); } //条件分页查询 @GetMapping public Result page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize, String name, Short gender, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { //记录日志 log.info("分页查询,参数:{},{},{},{},{},{}", page, pageSize,name, gender, begin, end); //调用业务层分页查询功能 PageBean pageBean = empService.page(page, pageSize, name, gender, begin, end); //响应 return Result.success(pageBean); } }
EmpService
public interface EmpService { /** * 批量删除操作 * @param ids id集合 */ void delete(List<Integer> ids); //省略... }
EmpServiceImpl
@Slf4j @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Override public void delete(List<Integer> ids) { empMapper.delete(ids); } //省略... }
EmpMapper
@Mapper public interface EmpMapper { //批量删除 void delete(List<Integer> ids); //省略... }
EmpMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.itheima.mapper.EmpMapper"> <!--批量删除员工--> <select id="delete"> delete from emp where id in <foreach collection="ids" item="id" open="(" close=")" separator=","> #{id} </foreach> </select> <!-- 省略... --> </mapper>
3.3.5 功能测试
功能开发完成后,重启项目工程,打开postman,发起DELETE请求:
控制台SQL语句:
3.3.6 前后端联调
打开浏览器,测试后端功能接口: