什么是MyBatis?
MyBatis是一款优秀的 持久层 框架,用于简化JDBC的开发。
MyBatis本是 Apache的一个开源项目iBatis,2010年这个项目由apache迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。
在上面我们提到了两个词:一个是持久层,另一个是框架。
要使用 MyBatis, 只需将 mybatis-x.x.x.jar 文件置于类路径(classpath
)中即可。
如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>x.x.x</version>
</dependency>
spring-boot 整合mybatis起步依赖:
<!-- mybatis起步依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- mysql驱动包依赖 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
数据库连接信息,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
在MyBatis中,你可以通过注解直接在Mapper接口的方法上编写SQL语句,而不需要XML文件。这种方式被称为MyBatis的注解开发方式。以下是一些最常见的MyBatis注解:
@Select
- 用于查询操作。@Insert
- 用于插入操作。@Update
- 用于更新操作。@Delete
- 用于删除操作。@Results
- 定义结果集映射规则。@Result
- 结果集的细节映射。@Param
- 给方法参数指定一个名字。@Mapper
注解 - 表示是mybatis中的Mapper接口示例代码如下:
首先,定义一个Mapper接口:
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") int id);
@Insert("INSERT INTO user(name, email) VALUES(#{name}, #{email})")
int insertUser(@Param("name") String name, @Param("email") String email);
@Update("UPDATE user SET name = #{name}, email = #{email} WHERE id = #{id}")
int updateUser(@Param("id") int id, @Param("name") String name, @Param("email") String email);
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteUser(@Param("id") int id);
}
这些注解中的字符串表示SQL语句,#{}
内部表示参数绑定,@Param
注解用于给参数命名,以便在SQL语句中指引参数的来源。
如果需要复杂的结果映射,可以使用@Results
和@Result
注解:
@Results({
@Result(property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "email", column = "email")
})
@Select("SELECT id, name, email FROM user")
List<User> getAllUsers();
@Results
注解定义了如何从数据库结果集中提取数据并填充到对象属性中。property
是对象的属性名,column
是数据库表中的列名。
为了使上述Mapper工作,还需要配置MyBatis以识别对应的Mapper接口,具体配置取决于你是使用纯MyBatis框架还是集成在Spring Boot项目中。
在纯MyBatis项目中,你通常在MyBatis的配置文件中指定Mapper接口的位置:
<mappers>
<mapper class="your.package.UserMapper"/>
</mappers>
在Spring Boot项目中,你可能会使用@MapperScan
注解或者在每个Mapper接口上使用@Mapper
注解来自动扫描并注册:
@MapperScan("your.package")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
或者使用@Mapper
注解单独标记每个接口:
@Mapper
public interface UserMapper {
// ...方法定义
}
确保正确配置了MyBatis和数据源,这样上述注解的方法就可以在应用程序中正常工作了。
数据库连接池技术,避免频繁的创建连接、销毁连接而带来的资源浪费。
没有使用数据库连接池:
数据库连接池是个容器,负责分配、管理数据库连接(Connection)
允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏
数据库连接池的好处:
要怎么样实现数据库连接池呢?
官方(sun)提供了数据库连接池标准(javax.sql.DataSource
接口)
功能:获取连接
public Connection getConnection() throws SQLException;
第三方组织必须按照DataSource接口实现
常见的数据库连接池:
现在使用更多的是:Hikari
、Druid
(性能更优越)
Druid(德鲁伊)
Druid连接池是阿里巴巴开源的数据库连接池项目
功能强大,性能优秀,是Java语言最好的数据库连接池之一
?
如果我们想把默认的数据库连接池切换为Druid数据库连接池,只需要完成以下两步操作即可:
参考官方地址:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
<dependency>
<!-- Druid连接池依赖 -->
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
方式1:
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.druid.username=root
spring.datasource.druid.password=123456
方式2:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=123456
在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。
介绍
预编译SQL有两个优势:
性能更高:预编译SQL,编译一次之后会将编译后的SQL语句缓存起来,后面再次执行这条语句时,不会再次编译。(只是输入的参数不同)
更安全(防止SQL注入):将敏感字进行转义,保障SQL的安全性。
SQL注入
SQL注入:是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法。
由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。
SQL注入(SQL Injection)是针对基于SQL语言的数据库系统的一种注入攻击技术。攻击者通过在应用程序的输入域中输入恶意的SQL代码片段来欺骗后端数据库执行不当的SQL命令。这种攻击可以用来绕过应用程序的安全措施,获取未授权的数据访问,甚至是控制数据库服务器的更严重的攻击。
以下是一个非常简化的SQL注入攻击的示例:
假设一个登录表单提交后台的SQL查询语句是这样的:
SELECT * FROM users WHERE username = '$username' AND password = '$password';
其中 $username
和 $password
是用户的输入。
如果用户在用户名或密码的输入框内输入:
' OR '1'='1
那么合并后的SQL语句将会成为:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '' OR '1'='1';
这个条件 '1'='1'
始终为真,所以该SQL语句实质上会选择表中所有的用户,从而绕过了身份验证。
在Mybatis中提供的参数占位符有两种:${...}
、#{...}
#{…}
${…}
注意事项:在项目开发中,建议使用#{…},生成预编译SQL,防止SQL注入安全。
概念:在数据添加成功后,需要获取插入数据库数据的主键。
在使用Spring Boot整合Mybatis进行数据插入操作的时候,可以将生成的主键返回到实体类中。Mybatis提供了几种方式来实现主键的返回。最常用的两种是使用useGeneratedKeys
和@Options
注解。
使用useGeneratedKeys
在Mybatis的Mapper XML配置文件中,你可以在<insert>
标签中设置useGeneratedKeys="true"
和keyProperty
属性来自动接受数据库生成的主键值。
例如,假设有一个名为User
的实体类和对应的users
数据表,id
字段是自增主键:
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users(name, email) VALUES(#{name}, #{email})
</insert>
这里,keyProperty
对应到实体类User
中的id
属性。当执行此插入语句后,Mybatis会将生成的主键值回填到传入的User
对象的id
属性中。
使用@Options
注解
在Mapper接口的方法上使用@Options
注解也可以返回主键。注解里要设置useGeneratedKeys=true
以及keyProperty
指向对应的主键属性。
例如:
public interface UserMapper {
//会自动将生成的主键值,赋值给user对象的id属性
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("INSERT INTO users(name, email) VALUES(#{name}, #{email})")
int insertUser(User user);
}
使用@Options
注解后,执行insertUser
方法插入数据时,User
实体对象的id
字段将会被自动填充。
我们看到查询返回的结果中大部分字段是有值的,但是deptId,createTime,updateTime这几个字段是没有值的,而数据库中是有对应的字段值的,这是为什么呢?
原因如下:
解决方案:
起别名:在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
要使用驼峰命名前提是 实体类的属性 与 数据库表中的字段名严格遵守驼峰命名。
假设我们现在有一个需求:在员工管理的列表页面中,我们需要根据条件查询员工信息,查询条件包括:姓名、性别、入职时间。
通过页面原型以及需求描述我们要实现的查询:
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注入风险。
@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语句(性能高、安全)
在上面我们所编写的条件查询功能中,我们需要保证接口中方法的形参名和SQL语句中的参数占位符名相同。
当方法中的形参名和SQL语句中的占位符参数名不相同时,就会出现以下问题:
参数名在不同的SpringBoot版本中,处理方案还不同:
springBoot的父工程对compiler编译插件进行了默认的参数parameters配置,使得在编译时,会在生成的字节码文件中保留原方法形参的名称,所以#{…}里面可以直接通过形参名获取对应的值
在编译时,生成的字节码文件当中,不会保留Mapper接口中方法的形参名称,而是使用var1、var2、…这样的形参名字,此时要获取参数值时,就要通过@Param注解来指定SQL语句中的参数名
Mybatis的开发有两种方式:
使用Mybatis的注解方式,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件中。
在Mybatis中使用XML映射文件方式开发,需要符合一定的规范:
XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)
XML映射文件的namespace属性为Mapper接口全限定名一致
XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致。
<select>标签:就是用于编写select查询语句的。
- resultType属性,指的是查询返回的单条记录所封装的类型。
第1步:创建XML映射文件
resources
目录下,创建和接口一样的包名。Mapper
接口名一致第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
接口全限定名例如:com.example.mapper.XxxMapper
。
<?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.example.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.example.mapper.EmpMapper">
<!--查询操作-->
<select id="list" resultType="com.example.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>
运行测试类,执行结果:
在页面原型中,列表上方的条件是动态的,是可以不传递的,也可以只传递其中的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标签。
<if>
:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。
<if test="条件表达式">
要拼接的sql语句
</if>
接下来,我们就通过<if>
标签来改造之前条件查询的案例。
示例:把SQL语句改造为动态SQL方式
<select id="list" resultType="com.example.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>
<select id="list" resultType="com.example.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.example.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语句:
假设有一个案例:完善更新员工功能,修改为动态更新员工数据信息。
修改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.example.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.example.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>
<set>
案例需求:员工删除功能(既支持删除单条记录,又支持批量删除)
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.example.mapper.EmpMapper">
<!--删除操作-->
<delete id="deleteByIds">
delete from emp where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
</mapper>
执行的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.example.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>