JavaWeb学习笔记(Day08+Day09)之Mybatis入门+基础操作

一、快速入门

1. 准备工作
(1) 创建springboot工程,并导入 mybatis的起步依赖、mysql的驱动包。
(2) 配置Mybatis

在springboot项目中,可以编写application.properties文件,配置数据库连接信息。我们要连接数据库,就需要配置数据库连接的基本信息,包括:driver-class-name、url 、username,password。

bash 复制代码
#驱动类名称
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
(3) 编写SQL语句

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

UserMapper:

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

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

核心逻辑:

@Mapper 注解

核心作用:表示是mybatis中的Mapper接口。程序运行时,框架会自动生成接口的实现类对象(代理对象),并给交Spring的IOC容器管理。

@Select 注解

核心作用 :标注这是一个查询方法,括号内的字符串就是要执行的 SQL 语句(查询 user 表的 id、name、age、gender、phone 所有列的全部数据)。

public List<User> list() 方法定义

返回值 List<User> :MyBatis 会把 SQL 查询到的每一行数据,自动封装成一个 User 对象(要求 User 类的属性名和 SQL 查询的列名一致,比如 id 对应 Userid 属性),所有 User 对象最终放到 List 里返回。

**Q:**为什么 List<User> 是返回值但是没有 return 语句

A:这是一个接口的方法定义,不是普通类的方法实现 ------ 接口只规定「要返回什么」,不用写具体的返回逻辑;真正的 return 语句,是 MyBatis 动态生成的实现类帮你写的

(4) 单元测试yyiyi
java 复制代码
@SpringBootTest
class SpringbootMybatisQuickstartApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testListUser() {
        List<User> userList = userMapper.list();
        userList.stream().forEach(user -> {
            System.out.println(user);
        });
    }
}
2. 解决SQL警告与提示

默认我们在UserMapper接口上的@Select注解中编写SQL语句是没有提示的。如果想让idea给出提示,可以做如下配置:

配置完成之后,发现SQL语句中的关键字有提示了,但还存在不识别表名(列名)的情况:

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

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

二、数据库连接池

1. 介绍

(1) 没有使用数据库连接池:

客户端执行SQL语句:要先创建一个新的连接对象,然后执行SQL语句,SQL语句执行后又需要关闭连接对象从而释放资源,每次执行SQL时都需要创建连接、销毁连接,这种频繁的重复创建销毁的过程是比较耗费计算机的性能。

(2) 使用数据库连接池:

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

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

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

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

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

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

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

① 功能:获取连接

java 复制代码
public Connection getConnection() throws SQLException;

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

(2) 常见的数据库连接池: Hikari(springboot默认) 、Druid(性能更优越)

三、lombok

1. 介绍

lombok通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,并可以自动化生成日志变量,简化java开发、提高效率。

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

(1) 在pom.xml文件中引入依赖

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

(2) 在实体类上添加注解

说明:@Data注解中不包含无参构造、全参构造方法,通常在实体类上,还会添加上:全参构造、无参构造

java 复制代码
import lombok.Data;

@Data //getter方法、setter方法、toString方法、hashCode方法、equals方法
@NoArgsConstructor //无参构造
@AllArgsConstructor//全参构造
public class User {
    private Integer id;
    private String name;
    private Short age;
    private Short gender;
    private String phone;
}

四、Mybatis基础操作

1. 需求
  • 根据资料中提供的《tlias智能学习辅助系统》页面原型及需求,完成员工管理的需求开发。
2. 准备

(1) 实施前的准备工作:

  • 准备数据库表

  • 创建一个新的springboot工程,选择引入对应的起步依赖(mybatis、mysql驱动、lombok)

  • application.properties中引入数据库连接信息

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

  • 准备Mapper接口 EmpMapper

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

java 复制代码
@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;
}

**Q:**为什么用 LocalDate/LocalDateTime?

A: 这两个类是Java 8 引入的java.time包下的新日期时间 API(也叫 JSR 310),用来替代旧的java.util.Datejava.util.Calendar------ 旧 API 存在线程不安全、月份从 0 开始(易出错)、API 设计混乱 等问题,而LocalDate/LocalDateTime是不可变、线程安全的,且语义更清晰,是现在开发和面试中的主流用法。

① LocalDate:仅表示 "日期"(年 - 月 - 日):

LocalDate只包含日期维度(年、月、日),完全不包含时间(时、分、秒)和时区信息,比如 "2026-01-24" 就是一个典型的 LocalDate 值。

② LocalDateTime:表示 "日期 + 时间"(年 - 月 - 日 时 - 分 - 秒):

LocalDateTime包含日期 + 时间维度(年、月、日、时、分、秒、纳秒),但依然不包含时区信息,比如 "2026-01-24 15:30:45" 就是一个 LocalDateTime 值。

3. 删除
(1) 页面原型

当我们点击后面的"删除"按钮时,前端页面会给服务端传递一个参数,也就是该行数据的ID。 我们接收到ID后,根据ID删除数据即可。

(2) 代码实现

① 接口方法

java 复制代码
@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}。但是建议保持名字一致。

② 测试

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

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

}
(3) 日志输入

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

  • 打开application.properties文件

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

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

② 开启日志之后,我们再次运行单元测试,可以看到在控制台中,输出了以下的SQL语句信息:

但是我们发现输出的SQL语句:delete from emp where id = ?,我们输入的参数16并没有在后面拼接,id的值是使用?进行占位。那这种SQL语句我们称为预编译SQL。

4. 预编译SQL
(1) 预编译SQL有两个优势:
  • 性能更高

  • 更安全(防止SQL注入)

① 性能更高:预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)

② 更安全(防止SQL注入):将敏感字进行转义,保障SQL的安全性。

(2) SQL注入

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

由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。

② 测试1:使用资料中提供的程序,来验证SQL注入问题

第1步:进入到DOS

第2步:执行以下命令,启动程序

bash 复制代码
#启动存在SQL注入的程序
java -jar sql_Injection_demo-0.0.1-SNAPSHOT.jar 

第3步:打开浏览器输入`http://localhost:9090/login.html\`

发现竟然能够登录成功:

以上操作为什么能够登录成功呢?

  • 由于没有对用户输入内容进行充分检查,而SQL又是字符串拼接方式而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,从而完成恶意攻击。
(3) 参数占位符

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

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

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

② ${...}
  • 拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题

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

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

5. 新增
(1) 代码实现

① SQL语句:

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

② 接口方法:

java 复制代码
@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);

}

③ 测试类:

java 复制代码
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);
    }
}

Q1: 为什么要写 (short)1?(给 gender/job 赋值)

A1:核心原因:

  • Java 中,直接写1默认是int类型(4 个字节),而gender/jobShort类型(2 个字节)------把大类型的 int 直接赋值给小类型的 Short,编译器会报错(类型不兼容);
  • (short)1是 "强制类型转换",把int类型的1转换成short类型,才能匹配Short属性的类型要求。
    Q2: 为什么要写 LocalDate.of(2000,1,1)?(给 entrydate 赋值)

A2: LocalDate.of(年, 月, 日)是 LocalDate 类提供的静态方法 ,作用是 "创建一个指定年月日的 LocalDate 对象",刚好匹配entrydate的类型要求。

(2) 主键返回

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

② 业务场景:在前面讲解到的苍穹外卖菜品与套餐模块的表结构,菜品与套餐是多对多的关系。

  • 一个套餐可以包含多个菜品(比如 "早餐套餐" 包含包子、豆浆);
  • 一个菜品可以属于多个套餐(比如 "包子" 可以在 "早餐套餐" 和 "加班套餐" 里)。

在添加套餐的时候,我们需要在界面当中来录入套餐的基本信息,**还需要来录入套餐与菜品的关联信息。**这些信息录入完毕之后,我们一点保存,就需要将套餐的信息以及套餐与菜品的关联信息都需要保存到数据库当中。

具体的过程包括两步,**首先第一步先需要将套餐的基本信息保存了,接下来第二步再来保存套餐与菜品的关联信息。**套餐与菜品的关联信息就是往中间表当中来插入数据,来维护它们之间的关系。而中间表当中有两个外键字段,一个是当前菜品的ID,还有一个就是套餐的ID,而这个套餐的 ID 指的就是此次我所添加的套餐的ID,所以我们在第一步保存完套餐的基本信息之后,就需要将套餐的主键值返回来供第二步进行使用。这个时候就需要用到主键返回功能。

③ 代码实现:

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

2). 主键返回代码实现:

java 复制代码
@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);

}

3). 测试:

java 复制代码
@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());
    }
}
6. 更新
(1) 代码实现

① SQL语句:

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;

② 接口方法:

java 复制代码
@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);
    
}

③ 测试类:

java 复制代码
@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);
    }
}
7. 查询
(1) 根据ID查询的代码实现

① SQL语句:

sql 复制代码
select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time from emp;

② 接口方法:

java 复制代码
@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);
}

③ 测试类:

java 复制代码
@SpringBootTest
class SpringbootMybatisCrudApplicationTests {
    @Autowired
    private EmpMapper empMapper;

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

④ 执行结果:

而在测试的过程中,我们会发现有几个字段(deptId、createTime、updateTime)是没有数据值的

(2) 数据封装
① 问题

我们看到查询返回的结果中大部分字段是有值的,但是deptId,createTime,updateTime这几个字段是没有值的,而数据库中是有对应的字段值的,原因如下:

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

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

② 解决方案

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

java 复制代码
@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);

2). 开启驼峰命名:(推荐)

bash 复制代码
# 在application.properties中添加:
mybatis.configuration.map-underscore-to-camel-case=true

要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。

(3) 条件查询
① 代码实现

1). SQL语句:

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;

2). 接口方法:

java 复制代码
@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);

}
相关推荐
xqqxqxxq2 小时前
《智能仿真无人机平台(多线程V2.0)技术笔记》(线程进阶: 无人机自动防空平台开发教程)
笔记·无人机·cocos2d
三伏5222 小时前
Cortex-M3权威指南Cn第七章——笔记
笔记·cortex-m3
丝斯20112 小时前
AI学习笔记整理(56)——大模型微调
人工智能·笔记·学习
峥嵘life2 小时前
Android16 EDLA【CTS】CtsNetTestCases存在fail项
android·java·linux·学习·elasticsearch
楼田莉子3 小时前
Linux进程间通信——管道
linux·运维·服务器·c++·学习
理人综艺好会3 小时前
Web学习之网络通信
学习
科技林总3 小时前
【系统分析师】5.4 数据库设计与建模
学习
whale fall3 小时前
【雅思-口语】与豆包聊天:出国旅游日常聊天英文 + 中文对照合集
笔记·学习·旅游
驱动探索者3 小时前
Intel Xeon 服务器 CPU 学习
运维·服务器·学习·xeon