一,JDBC的操作流程
1. 创建数据库连接池DataSource
2. 通过DataSource获取数据库连接Connection
3. 编写要执⾏带?占位符的SQL语句
4. 通过Connection及SQL创建操作命令对象Statement
5. 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值
6. 使⽤Statement执⾏SQL语句
7. 查询操作:返回结果集ResultSet,更新操作:返回更新的数量
8. 处理结果集
9. 释放资源
在以上的步骤中,1,2,4,6,7,9可以提取出来让mybatic去做
二,mybatis
• MyBatis是⼀款优秀的持久层框架,用于简化JDBC的开发
• 持久层:指的就是持久化操作的层,通常指数据访问层(dao),是⽤来操作数据库的
简单来说MyBatis是更简单完成程序和数据库交互的框架,也就是更简单的操作和读取数据库⼯具
2.1 MyBatis入门
Mybatis操作数据库的步骤:
1. 准备工作(创建springboot⼯程、数据库表准备、实体类)
2. 引⼊Mybatis的相关依赖,配置Mybatis(数据库连接信息)
3. 编写SQL语句(注解/XML)
4. 测试
Mybatis的持久层接⼝规范⼀般都叫XxxMapper
@Mapper注解:表⽰是MyBatis中的Mapper接⼝
• 程序运⾏时,框架会⾃动⽣成接⼝的实现类对象(代理对象),并给交Spring的IOC容器管理
• @Select注解:代表的就是select查询,也就是注解对应⽅法的具体实现内容.
测试类上添加了注解@SpringBootTest,该测试类在运⾏时,就会⾃动加载Spring的运⾏环境. 我们通过@Autowired这个注解,注⼊我们要测试的类,就可以开始进⾏测试了
@Select("select * from user_info where username=#{username} and password =#{password}") List<UserInfo> getListByNameAndPassword(String username,String password);在这段代码中,如果改成
@Select("select * from user_info where username=#{username1} and password =#{password1}") List<UserInfo> getListByNameAndPassword(String username,String password);就会报错,如下:
[password, param1, username, param2] 是框架自动帮我们生成的参数,其中框架会生成参数名对应的参数和按照参数顺序生成的param1,param2........
@Select("select * from user_info where username=#{param1} and password =#{param2}") List<UserInfo> getListByNameAndPassword(String username,String password);当改成这样的时候,运行成功
生成的这四个参数,password, username是无序的。param1, param2是有序的
三,MyBatis的基础操作
3.1 打印⽇志
在Mybatis当中我们可以借助⽇志,查看到sql语句的执⾏、执⾏传递的参数以及执⾏结果
在配置⽂件中进⾏配置即可
3.2 参数传递
需求:查找id=4的⽤⼾,对应的SQL就是:select*fromuser_infowhereid=4
@Select("select * from user_info where id =4") UserInfo getUserInfoById();但是这样的话, 只能查找id=4的数据,所以SQL语句中的id值不能写成固定数值,需要变为动态的数值
解决⽅案:在queryById⽅法中添加⼀个参数(id),将⽅法中的参数,传给SQL语句
使⽤ #{} 的⽅式获取⽅法中的参数
@Select("select * from user_info where id = #{id}") UserInfo getUserInfoById(Integer id);如果mapper接⼝⽅法形参只有⼀个普通类型的参数,#{…}⾥⾯的属性名可以随便写,如:#{id}、# {value}。建议和参数名保持⼀致
也可以通过 @Param ,设置参数的别名,如果使⽤ @Param 设置别名,#{...}⾥⾯的属性名必须和 @Param 设置的⼀样
@Select("select * from user_info where username=#{username} and password =#{password}") List<UserInfo> getListByNameAndPassword2(@Param("username1") String username, @Param("password1") String password);3.3 增(Insert)
@Insert("insert into user_info (`username`,`password`, `age`) values(#{username},#{password}, #{age})") Integer insertUser(UserInfo userInfo);sql语句:
insert into user_info (username, `password`, age, gender) values ("zhaoliu","zhaoliu",19,1)把SQL中的常量替换为动态的参数
Mapper接⼝:
@Insert("insert into user_info (`username`,`password`, `age`) values(#{username},#{password}, #{age})") Integer insertUser(UserInfo userInfo);直接使⽤UserInfo对象的属性名来获取参数
测试代码:
@Test void insertUser() { UserInfo userInfo = new UserInfo("zhaoliu", "zhaoliu", 17); Integer i = userInfoMapper.insertUser(userInfo); System.out.println("插入行数:"+i + ", 自增Id:"+ userInfo.getId()); }如果设置了 @Param 属性,#{...}需要使⽤参数.属性来获取
@Insert("insert into user_info (username, `password`, age, gender, phone) values (#{userInfo.username},#{userInfo.password},#{userInfo.age},# {userInfo.gender},#{userInfo.phone})") Integer insert(@Param("userInfo") UserInfo userInfo);返回主键
Insert 语句默认返回的是受影响的⾏数
但有些情况下,数据插⼊之后,还需要有后续的关联操作,需要获取到新插⼊数据的id
如果想要拿到⾃增id,需要在Mapper接⼝的⽅法上添加⼀个Options的注解
@Options(useGeneratedKeys = true, keyProperty = "id") @Insert("insert into user_info (`username`,`password`, `age`) values(#{username},#{password}, #{age})") Integer insertUser(UserInfo userInfo);• useGeneratedKeys:这会令MyBatis使⽤JDBC的getGeneratedKeys⽅法来取出由数据库内 部⽣成的主键(⽐如:像MySQL和SQLServer这样的关系型数据库管理系统的⾃动递增字 段),默认值:false.
• keyProperty:指定能够唯⼀识别对象的属性,MyBatis会使⽤getGeneratedKeys的返回值或 insert 语句的selectKey⼦元素设置它的值,默认值:未设置(unset)
注意:设置 useGeneratedKeys=true 之后,⽅法返回值依然是受影响的⾏数,⾃增 id 会设置在上述
keyProperty 指定的属性中.
3.4 删(Delete)
Mapper接⼝
@Delete("delete from user_info where id =#{id}") Integer deleteUserById(Integer id);3.5 改(Update)
@Update("update user_info set username= #{username} where id = #{id}") Integer updateUserById(UserInfo userInfo);@Test void updateUserById() { UserInfo userInfo = new UserInfo(); userInfo.setId(5); userInfo.setUsername("ll"); System.out.println(userInfoMapper.updateUserById(userInfo)); }@Update("update user_info set username= #{username} where id = #{id}") Integer updateUserById2(@Param("userInfo") UserInfo userInfo);当加上注解的时候会报错。
修正:
@Update("update user_info set username= #{userInfo.username} where id = #{userInfo.id}") Integer updateUserById2(@Param("userInfo") UserInfo userInfo);3.6 查(Select)
查询时发现,有⼏个字段是没有赋值的,只有Java对象属性和数据库字段⼀模⼀样时,才会进 ⾏赋值
当我们多查询一些数据的时候:
@Select("select id, username, `password`, age, gender, phone, delete_flag, create_time, update_time from user_info") List<UserInfo> queryAllUser();有很多null
从运⾏结果上可以看到,我们SQL语句中,查询了delete_flag,create_time,update_time,但是这⼏个 属性却没有赋值
MyBatis 会根据⽅法的返回结果进⾏赋值.
⽅法⽤对象UserInfo接收返回结果,MySQL查询出来数据为⼀条,就会⾃动赋值给对象.
⽅法⽤List接收返回结果,MySQL查询出来数据为⼀条或多条时,也会⾃动赋值给List.
但如果MySQL查询返回多条,但是⽅法使⽤UserInfo接收,MyBatis执⾏就会报错
原因分析:
当⾃动映射查询结果时,MyBatis会获取结果中返回的列名并在Java类中查找相同名字的属性(忽略⼤⼩写)。这意味着如果发现了ID列和id属性,MyBatis会将列ID的值赋给id属性
解决办法:
1. 起别名 2. 结果映射 3. 开启驼峰命名
3.6.1 起别名
在SQL语句中,给列名起别名,保持别名和实体类属性名⼀样
@Select("select id, username, password, age, gender, phone, " + "delete_flag as deleteFlag, create_time as createTime, update_time as updateTime from user_info") List<UserInfo> getList2();3.6.2 结果映射
@Results({ @Result(column = "delete_flag", property = "deleteFlag"), @Result(column = "create_time", property = "createTime"), @Result(column = "update_time", property = "updateTime"), }) @Select("select * from user_info") List<UserInfo> getList2();其中,results的源码为:
如果其他SQL,也希望可以复⽤这个映射关系,可以给这个Results定义⼀个名称
@Results(id = "BaseMap", value ={ @Result(column = "delete_flag", property = "deleteFlag"), @Result(column = "create_time", property = "createTime"), @Result(column = "update_time", property = "updateTime"), }) @Select("select * from user_info") List<UserInfo> getList2();这样的话,不仅getList2可以用这个results,getList也可以使用这个results
@ResultMap("BaseMap") @Select("select * from user_info") List<UserInfo> getList();使⽤ id 属性给该 Results 定义别名,使⽤ @ResultMap 注解来复⽤其他定义的 ResultMap
3.6.3 开启驼峰命名(推荐)
通常数据库列使用蛇形命名法进行命名(下划线分割各个单词),而Java属性⼀般遵循驼峰命名法约定. 为了在这两种命名方式之间启用自动映射,需要将mapUnderscoreToCamelCase 设置为true。
四,MyBatis XML配置文件
Mybatis的开发有两种方式:
1. 注解
2. XML
使用Mybatis的注解方式,主要是来完成⼀些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置⽂件中
MyBatis XML的方式需要以下两步:
1. 配置数据库连接字符串和MyBatis
2. 写持久层代码
XML的配置文件写法:
1)添加依赖(mybatis和mysql)
2)配置文件(数据库信息,xml文件地址)
3)写接口文件
4)写xml的实现
其中resultType为返回结果的类型。需要注意的是如果返回的为集合时,resultType设置为集合中元素的类型
4.1 配置连接字符串和MyBatis
# 数据库连接配置 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true username: root password: '123456' driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: # 配置打印 MyBatis日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true #配置驼峰⾃动转换 mapper-locations: classpath:/mapper/**.xml4.2 写持久层代码
持久层代码分两部分
1. ⽅法定义Interface
2. ⽅法实现:XXX.xml
<mapper>标签:需要指定 namespace 属性,表⽰命名空间,值为mapper接⼝的全限定名,包括全包名.类名
<select>查询标签:是⽤来执⾏数据库的查询操作的
id :是和 Interface (接⼝)中定义的⽅法名称⼀样的,表⽰对接⼝的具体实现⽅法。
resultType :是返回的数据类型,也就是开头我们定义的实体类.
4.3 增删改查操作
4.3.4 查(Select)
同样的,使⽤XML的⽅式进⾏查询,也存在数据封装的问题 我们把SQL语句进⾏简单修改,查询更多的字段内容,有的属性会没有被赋值
解决办法和注解类似:
1. 起别名 2. 结果映射 3. 开启驼峰命名
<resultMap id="BaseMap" type="com.luoyaoyun.mybatisdemo.model.UserInfo"> <id column="id" property="id"></id> <result column="delete_flag" property="deleteFlag"></result> <result column="create_time" property="createTime"></result> <result column="update_time" property="updateTime"></result> </resultMap> <!-- <select id="getList" resultType="com.luoyaoyun.mybatisdemo.model.UserInfo">--> <select id="getList" resultMap="BaseMap"> select * from user_info </select>五,其他查询操作
5.1 多表查询
多表查询和单表查询类似,只是SQL不同⽽已
5.1.1 准备⼯作
建⼀张⽂章表,进⾏多表关联查询. ⽂章表的uid,对应⽤⼾表的id
5.1.2 数据查询
对应Model:
@Data public class ArticleInfo { private Integer id; private String title; private String content; private Integer uid; private Integer deleteFlag; private Date createTime; private Date updateTime; // 用户相关信息 private String username; private Integer age; private Integer gender; private String phone; }@Mapper public interface ArticleInfoMapper { ArticleInfo getArticleInfoById(Integer articleId); }<?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.luoyaoyun.mybatisdemo.mapper.ArticleInfoMapper"> <select id="getArticleInfoById" resultType="com.luoyaoyun.mybatisdemo.model.ArticleInfo"> <!-- select * from article_info where id = #{articleId}--> SELECT ta.* , tb.username, tb.age, tb.gender, tb.phone FROM article_info ta left join user_info tb on ta.uid= tb.id where ta.id=#{articleId} </select> </mapper>Mybatis 不分单表还是多表,主要就是三部分:SQL,映射关系和实体类 通过映射关系,把SQL运⾏结果和实体类关联起来
多表查询的缺点:
1)性能低
2)会产生慢sql
所以会进行优化,以上述多表查询的例子来看,可以在dao层中实现 ta中id=1的单表查询,得到了uid之后,再实现ta中uid=tb的id的单表查询
5.1.3 一对多
需求:根据用户ID查询该用户的文章列表
SQL
SELECT ta.id AS user_id, ta.username, ta.age, ta.gender, ta.phone, tb.id AS article_id, tb.title, tb.content FROM user_info ta LEFT JOIN article_info tb ON ta.id = tb.uid WHERE ta.id = 1补充实体类UserInfo,添加articleInfoList
@NoArgsConstructor @Data public class UserInfo { private Integer id; private String username; private String password; private Integer age; private Integer gender; private String phone; private Integer deleteFlag; private Date createTime; private Date updateTime; private List<ArticleInfo> articleInfoList; }接⼝定义和接⼝实现
5.2 #{} 和${}
1. 先看Interger类型的参数
@Select("select * from user_info where id = #{id}") UserInfo getUserInfoById(Integer id);我们输入的参数并没有在后面拼接,id的值是使用 ? 进行占位. 这种SQL我们称之为"预编译SQL"
把 #{} 改成 ${} 再观察打印的⽇志:
可以看到,这次的参数是直接拼接在SQL语句中了.
2. 接下来我们再看String类型的参数
@Select("select username, 'password', age, gender, phone from user_info where username= ${name}") UserInfo queryByName(String name);可以看到,这次的参数依然是直接拼接在SQL语句中了,但是字符串作为参数时,需要添加引号 ‘ ’ ,使用${},不会拼接引号 ‘ ’ ,导致程序错误
修正代码如下:
@Select("select username, 'password', age, gender, phone from user_info where username= '${name}'") UserInfo queryByName(String name);从上面两个例子可以看出:
#{} 使用的是预编译SQL,通过 ? 占位的方式,提前对SQL进⾏编译,然后把参数填充到SQL语句 中。#{} 会根据参数类型,自动拼接引号 ' '
${} 会直接进行字符替换,⼀起对SQL进行编译,如果参数为字符串,需要加上引号 ' '
参数为数字类型时,也可以加上,查询结果不变,但是可能会导致索引失效,性能下降.
5.2.2 #{} 和${}区别
#{} 和${} 的区别就是预编译SQL和即时SQL的区别
当客⼾发送⼀条SQL语句给服务器后,⼤致流程如下:
1. 解析语法和语义,校验SQL语句是否正确
2. 优化SQL语句,制定执⾏计划
3. 执⾏并返回结果 ⼀条SQL如果⾛上述流程处理,我们称之为ImmediateStatements(即时SQL)
1. 性能更高
绝大多数情况下,某⼀条SQL语句可能会被反复调⽤执⾏,或者每次执⾏的时候只有个别的值不同(⽐如select的where⼦句值不同,update的set⼦句值不同,insert的values值不同).如果每次都需要 经过上⾯的语法解析,SQL优化、SQL编译等,则效率就明显不⾏了.
预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后⾯再次执⾏这条语句时,不会再次编译 (只是输⼊的参数不同),省去了解析优化等过程,以此来提⾼效率
2. 更安全(防止SQL注入)
SQL注⼊:是通过操作输⼊的数据来修改事先定义好的SQL语句,以达到执⾏代码对服务器进⾏攻击的方法。
由于没有对⽤⼾输⼊进⾏充分检查,⽽SQL⼜是拼接⽽成,在⽤⼾输⼊参数时,在参数中添加⼀些 SQL关键字,达到改变SQL运⾏结果的⽬的,也可以完成恶意攻击。
sql 注⼊代码: ' or 1='1
5.3 排序功能
从上⾯的例⼦中,可以得出结论:${}会有SQL注⼊的⻛险,所以我们尽量使⽤#{}完成查询
既然如此,是不是${}就没有存在的必要性了呢? 当然不是.
接下来看下${}的使⽤场景:
@Select("select * from user_info order by id #{order}") List<UserInfo> getListByOrder(String order);此处报错是因为desc不能加单引号,但是当传入string类型时,#{}会自动加单引号
所以此处只能用${},同时为了防止sql注入问题,可以限制为选项,从而不让自由输入
除此之外,还有表名作为参数时,也只能使用 ${},不能加引号的都只能用${}
5.4 like查询
like 使用 #{} 报错
@Select("select id, username, age, gender, phone, delete_flag, create_time, update_time " + "from user_info where username like '%#{key}%' ") List<UserInfo> queryAllUserByLike(String key);把#{}改成${}可以正确查出来,但是${}存在SQL注⼊的问题,所以不能直接使⽤${}.
解决办法:使⽤mysql的内置函数concat()来处理,实现代码如下:
@Select("select id, username, age, gender, phone, delete_flag, create_time, update_time " + "from user_info where username like concat('%',#{key},'%')") List<UserInfo> queryAllUserByLike(String key);六. 数据库连接池
数据库连接池负责分配、管理和释放数据库连接,它允许应⽤程序重复使⽤⼀个现有的数据库连接, ⽽不是再重新建⽴⼀个.
没有使⽤数据库连接池的情况:每次执⾏SQL语句,要先创建⼀个新的连接对象,然后执⾏SQL语句,SQL 语句执⾏完,再关闭连接对象释放资源.这种重复的创建连接,销毁连接⽐较消耗资源
使⽤数据库连接池的情况:程序启动时,会在数据库连接池中创建⼀定数量的Connection对象,当客⼾ 请求数据库连接池,会从数据库连接池中获取Connection对象,然后执⾏SQL,SQL语句执⾏完,再把 Connection归还给连接池
优点: 1. 减少了⽹络开销 2. 资源重⽤ 3. 提升了系统的性能
6.2 使用
常⻅的数据库连接池:
• C3P0
• DBCP
• Druid
• Hikari
1. Hikari : SpringBoot默认使⽤的数据库连接池
2. Druid
如果我们想把默认的数据库连接池切换为Druid数据库连接池,只需要引⼊相关依赖即可
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-3-starter</artifactId> <version>1.2.21</version> </dependency>如果SpringBoot版本为2.X,使⽤druid-spring-boot-starter依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.17</version> </dependency>