一文搞懂MyBatis的使用
在学习Java
的过程,我们可能使用了JDBC
来访问数据库获取数据,但是JDBC
也有比较大的问题
sql
语句写在java程序中,需求修改的话则需要修改Java
代码sql
占位符的拼接传递比较麻烦- 数据库对象映射成
Pojo
也比较麻烦
MyBatis
这个框架通过对JDBC
的封装,开发者只需要按照约定的配置进行配置开发,就可以方便的进行ORM
操作
O(Object),R(Relational),M(Mapping)
:对象关系映射,大白话就是Java
对象映射成数据库某张表中的一行记录,一行记录又可映射成一个Java
对象
开始上车
安装Mybatis
,传统的模式自然免不了找到对应的Jar
包添加成依赖,但通过Maven
工具可以方便的控制项目依赖,需要在pom.xml
中添加对应依赖,关于Maven
这里不介绍过多,不知道GVC
怎么写的可以在mvnrepository.com/ maven仓库中搜索对应的即可,比如搜索MyBatis
,选择对应的版本,复制如图所示的依赖配置到pom.xml
中,刷新maven
依赖即可下载,这里除了Mybatis
还需要下载mysql-connector
,Java
链接数据库需要通过驱动,不同厂商有各自的实现
创建一个Maven项目,创建过程忽略
这里以Mysql
为例子,在maven
依赖中添加如下依赖,一个是mybatis
,一个是mysql
的驱动
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
按照官网提供的配置进行本地化配置
- 先配置一份mybatis的主配置文件
- 下面的配置为最简化的配置,需要替换掉
dataSource
中的四个标签值,并且这里我选择了创建一个jdbc.properties
文件用来管理数据库链接信息,通过properties
标签的resource
属性进行导入,需要注意文件的存放位置,默认会从resource
目录开始找(如果不用这种方式,直接写上一个字符串即可)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties"></properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--配置对应的表映射文件,先配置,待会会进行配置-->
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
jdbc.properties
创建,自行替换
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/db
username=root
password=12345678
UserMapper.xml
创建,这里需要说明一下,mapper
标签中的namespace
属性,作用类似于唯一包名,主要用于区分不同mapper
文件中,增删改查标签中id
相同的情况,如有多个mapper
文件,里面的查询标签id
一致,但是加上namespace.
进行拼接就可以区分。这里主要演示mybatis
,对应的表设计可和sql
自行处理,这里提供参考(学习Mybatis
的使用才是文章内容,不了Mysql
和Sql
的应该先去了解)
<?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="user">
<insert id="insertUser">
insert into t_user
(`username`, `password`, `age`, `gender`)
values ('ben', '123', 40, '0')
</insert>
</mapper>
目前resources
目录下应该有如下文件
编写一个测试类来验证一个最基础的mybatis使用
- 需要创建一个
SqlSessionFactoryBuilder
对象 - 通过
SqlSessionFactoryBuilder
对象的build
方法创建SqlSessionFactory
对象,需要传入一个InpuStream
类型的参数,实际上是指上面编写好的mybatis-config.xml
- 通过
SqlSessionFactory
的openSession
方法创建SqlSession
对象,方法可以接受一个boolean
参数,由于配置中配置的是JDBC
的事务管理器,所以事务默认是不提交的,可以通过传入true
来开启自动提交(可以理解为执行一次增删改就自动提交一次),不传或传入false
则表示开启事务(得手动提交) - 由于需要执行的是插入数据操作,所以调用
sqlSession.insert
方法(会有对应的update
和delete
方法等),需要传入一个唯一id
,这个id
指的就是UserMapper.xml
中mapper
标签下配置的insert
标签的id
,如果成功,则会返回影响的数据条数
public class Main {
public static void main(String[] args) throws IOException {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory build = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// SqlSession sqlSession = build.openSession(true); // 如果传入true则开启自动提交
SqlSession sqlSession = build.openSession();
try{
// 像目前只有一个mapper文件,可以不用加前缀,直接insertUser即可,否则最好加上namespace避免重名
int count = sqlSession.insert("user.insertUser"); // 会返回影响的条数
if (count == 1) {
System.out.println("插入成功成功");
}else{
System.out.println("插入数据失败");
}
sqlSession.commit(); // 记得要提交事务
}catch(Exception e){
// 抛出异常时事务回滚
if(sqlSession != null){
sqlSession.rollback();
}
}finally{
// 结束时关闭资源
if(sqlSession != null){
sqlSession.close();
}
}
}
}
当执行程序后,可以看到数据库成功增加了一条记录
关于SqlSessionFactoryBuilder
的入参输入流,代码演示使用了mybatis
提供的一个工具类,如果你想,也可以替换成任何自己实现的InputStream
对象,只要能创建即可
new FileInputStream("xxxx");
ClassLoader.getSystemClassLoader().getResourceAsStream("xxxx");
实际上,MyBatis
提供的方法底层就是通过这个ClassLoader
对象进行调用的,原理是从类路径开始查找对应资源- ...任何产出
InputStream
的方式
关于日志配置
日志的输出可以让开发过程更清晰的看到SQL
的拼接执行等信息,这里需要十分注意settings
标签的顺序位置,错了的话会报错,具体顺序报错信息会提示你
<properties resource="jdbc.properties"></properties>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"></setting>
</settings>
<environments default="development">
这里使用了mybatis
内置的标准日志实现,添加配置后再次执行,就能看到如图所示的日志信息,方便开发调试
亦可配置如SLF4J,Log4j等,其中的区别可能是性能或配置需求等,自行抉择
动态插入
上面的实例中,插入语句是写死的值,显然实际开发中对应的值是需要前端将用户注册的参数穿过来,此时可以使用#{}
进行占位,作用等价于JDBC
中的?
占位符(底层用的是PreparedStatement没有SQL注入问题)
- 执行对应方法时,传入
Map
集合 - 传入
Pojo
对象,但是需要编写对应的getter
方法
测试insert传入Map
先改写mapper.xml
,将原本写死的地方改成#{Map的key名}
<mapper namespace="user">
<insert id="insertUser">
insert into t_user
(username, password, age, gender)
values (#{username}, #{password}, #{age}, #{gender})
</insert>
</mapper>
public class Main {
public static void main(String[] args) throws IOException {
// 简单的工具类封装了SqlSession
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
Map<String, Object> user = new HashMap();
user.put("username", "jack");
user.put("password", "jinwandalaohu");
user.put("age", 60);
user.put("gender", "0");
int count = sqlSession.insert("user.insertUser", user);
if (count == 1) {
System.out.println("插入成功");
} else {
System.out.println("insert failed");
}
sqlSession.close();
}
}
再次执行,可以看到#{}
被替换成了?
,并且日志输出了每个占位符对应的值和数据类型,并且数据成功插入
需要注意的是,如果使用Map
当参数传入,占位符填写了不存在的key
,则最终插入的值会是null
测试传入Pojo类
先建立一个User
类,比较关键的地方在于,需要编写对应的getter
方法
public class User {
private Long id;
private String username;
private String password;
private Character gender;
private Integer age;
public User() {
}
public User(String username, String password, Character gender, Integer age) {
this.username = username;
this.password = password;
this.gender = gender;
this.age = age;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Character getGender() {
return gender;
}
public void setGender(Character gender) {
this.gender = gender;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
Usermapper.xml
的内容不需要修改
public class Main {
public static void main(String[] args) throws IOException {
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
User user = new User("Susan", "qweasd", '1', 29);
int count = sqlSession.insert("user.insertUser", user);
if (count == 1) {
System.out.println("插入成功");
} else {
System.out.println("insert failed");
}
sqlSession.close();
}
}
执行后依然可以看到插入成功
使用Pojo
类和Map
最大的区别点在于,必须传入存在的getter
方法名,举个例子,Mybatis
会把getUsername
的get
去掉,首字母U
改成小写的u
,最终占位符传入的就是username
,如果不存在getter
则会报错,而不是传入null
在Mapper
的编写中,有一个parameterType
参数类型可以传入全限定类名,但一般这个可以忽略不写,如果要写格式如下
<insert id="insertUser" parameterType="cn.mgl.pojo.User">
动态删除
在UserMapper.xml
中,添加一个delete
标签,由于传参是个简单数据类型,且只有一个参数,占位符的key
可以随便写
<mapper namespace="user">
<delete id="deleteUser">
delete from t_user where id = #{suibiannixie}
</delete>
</mapper>
调用sqlSession
的delete
方法,传入id
public class Main {
public static void main(String[] args) throws IOException {
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
int count = sqlSession.delete("user.deleteUser",8);
if (count == 1) {
System.out.println("删除成功");
} else {
System.out.println("delete failed");
}
sqlSession.close();
}
}
可以看到8
被成功替换了#{suibiannixie}
动态修改
这里演示只根据用户id
修改用户名
<mapper namespace="user">
<update id="updateUser">
update t_user set username=#{username} where id = #{id}
</update>
</mapper>
调用sqlSession
的update
方法,传入User
对象
public static void main(String[] args) throws IOException {
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
User user = new User();
user.setUsername("tester");
user.setId(14L);
int count = sqlSession.delete("user.updateUser",user);
if (count == 1) {
System.out.println("更新用户名成功");
} else {
System.out.println("update failed");
}
sqlSession.close();
}
更新成功如下
使用Mybatis查询数据
与增删改有点区别,他们都是返回数据表中被影响的数据行数,而查询则会返回一个封装好的查询结果集,比如查询用户表,那么返回结果就是单个用户对象,或者用户对象的集合
编写mapper
,图方便用*
<mapper namespace="user">
<select id="selectById">
select * from t_user where id = #{id}
</select>
</mapper>
执行sqlSession
的selectOne
方法
public static void main(String[] args) throws IOException {
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
Object user = sqlSession.selectOne("user.selectById", 1);
System.out.println(user);
sqlSession.close();
}
执行程序后可以看到如下报错
意思就是mybatis
检测到你没有为select
标签提供对应的结果映射类型,不提供的话它没有办法知道如何处理对应的结果集,再次修改mapper
,添加一个resultType
标签,填写全限定类名
<mapper namespace="user">
<select id="selectById" resultType="cn.mgl.pojo.User">
select * from t_user where id = #{id}
</select>
</mapper>
再次执行便能查到对应用户信息,这里需要注意一下几点
当用户表字段用Pojo类命名格式不一致
为了演示效果,为用户表额外添加一个underScoreCase(下划线命名)
的字段,并给对应数据添加日期类型的值
Pojo
类中添加对应的字段和getter方法,java
命名规范是小驼峰
此时再次执行查询程序,可以看到对象映射并没有成功,对应的createdAt
字段为null
原因就是因为属性名没有对上,有以下几种解决方案
- 通过
sql
编写别名,让查询结果字段名对的上created_at as createdAt
<select id="selectById" resultType="cn.mgl.pojo.User">
select id, username, password, gender, age, created_at as createdAt
from t_user
where id = #{id}
</select>
- 通过
mybatis
提供的设置选项,开启驼峰命名自动映射,前提是转换后的名字能对得上,如数据库的字段为a_b,转化后就成了aB
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"></setting>
<setting name="logImpl" value="STDOUT_LOGGING"></setting>
</settings>
- 通过
select
标签的resultMap
映射(后面会说)
前两种方式任选其一配置后,再次执行查询语句,可以看到createdAt
字段已经有值了
查询多条数据
sqlSession
查多条就需要调用selectList
方法,虽然返回的是List
集合,但resultType
仍然指定实体类名,新增一个select
标签
<select id="selectAll" resultType="cn.mgl.pojo.User">
select id, username, password, gender, age, created_at as createdAt
from t_user
</select>
调用selectList
方法,接受类型为User
集合
public static void main(String[] args) throws IOException {
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
List<User> users = sqlSession.selectList("user.selectAll");
users.forEach(System.out::println);
sqlSession.close();
}
查询成功
实际上,查询结果为单条的情况下也可以使用List进行接收
使用接口代理形式
上述的使用方式有一个非常大的问题在于,开发者需要记住且拼接对应的映射id
,这种方式并不便于开发维护,所以Mybatis
还提供了一个更为方便使用的代理模式
首先新建一个接口,这里命名为UserDao
public interface UserDao {
int insertUser(User user);
int deleteUser(Long id);
int updateUser(User suer);
User selectById(Long id);
List<User> selectAll();
}
接着配置对应的mapper
,这里需要注意的是,namespace
不能再乱写了,需要写成UserDao
的全限定类名,并且所有sql
标签的id
属性也要和接口中的方法名保持一致,如果不一致,则会抛出对应的异常
<mapper namespace="cn.mgl.dao.UserDao">
<insert id="insertUser" parameterType="cn.mgl.pojo.User">
insert into t_user
(username, password, age, gender)
values (#{username}, #{password}, #{age}, #{gender})
</insert>
<delete id="deleteUser">
delete from t_user where id = #{suibiannixie}
</delete>
<update id="updateUser">
update t_user set username=#{username} where id = #{id}
</update>
<select id="selectById" resultType="cn.mgl.pojo.User">
select id, username, password, gender, age, created_at as createdAt
from t_user
where id = #{id}
</select>
<select id="selectAll" resultType="cn.mgl.pojo.User">
select id, username, password, gender, age, created_at as createdAt
from t_user
</select>
</mapper>
调用执行的时候不再使用namespace
拼接id
的方式,而是使用sqlSession
的getMapper
方法,传入对应的Mapper
接口的类对象,mybatis
底层实现会去进行解析反射等一系列操作,最终生成一个代理对象,开发者只需要调用接口中的方法即可完成数据库操作,实现的效果完全等价的
public static void main(String[] args) {
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
List<User> users = mapper.selectAll();
users.forEach(System.out::println);
sqlSession.close();
}
这样的方式更为常用
关于#{}和${}
学过JDBC开发的都知道,有个PreparedStatement
和Statement
对象
#{}
底层实现对应着PreparedStatement
,有防SQL注入
的作用,先编译再进行占位符替换,上面所有的示例中都是使用这种方式,用的最多${}
底层对应着Statement
,有SQL注入
的风险,一般在SQL
语句需要对保留关键字进行拼接的情况下使用,能不用就不用
举几个需要使用${}
的情况
- 查询结果集需要根据
username
排序,排序方式需要自定义传入,编写如下SQL
<select id="selectAllOfSort" resultType="cn.mgl.pojo.User">
select id, username, password, gender, age, created_at as createdAt
from t_user order by username ${sort}
</select>
这种情况下才适合使用,引用如果使用#{}
,会将传入的asc
或者desc
加上字符的引号,那么对应的语句便不合法,因为关键字是不需要加引号的
- 表名不能写死的情况下,需要动态修改执行,可以使用,原理还是引号导致的语法问题
SQL
语句使用了in(????)
,如delete from xxx where id in (${keys})
此时也可以使用,原理同上- 模糊查询的情况下,如
select * from xxx where username like '%${key}'
模糊查询下如果不想使用${}
,可以选择#{}
,但是需要一定拼接技巧
- 使用
concat
函数,语句可以这样写select * from xxx where username like concat('%',#{key},'%')
,最终出来的效果就是合法的语句 - 使用双引号包括
%
,select * from xxx where username like "%"#{key}"%"
typeAliases 类型别名
主要用于给数据类型起别名,在mybatis-config.xml
中进行配置,需要注意顺序,在后续的mapper
文件中,指定类型时就不再需要写冗长的全限定类名,在使用的时候是不区分大小写的
<typeAliases>
<typeAlias type="cn.mgl.pojo.User" alias="User"/>
</typeAliases>
像这个查询语句的resultType
,原本写的全限定类名就可以替换成简短的别名
<select id="selectAll" resultType="User">
select id, username, password, gender, age, created_at as createdAt
from t_user
</select>
此时如果一个类名配置一次显然也会麻烦,恰好提供了一个package
标签,只需要指定包名,那么这个包下的所有类都会生成一个对应的简类名别名供使用
<typeAliases>
<package name="cn.mgl.pojo"/>
</typeAliases>
Mybatis配置文件配置项理解
其实这一环在官网的文档中有说明,遇到具体的配置问题可以随时查询 mybatis.org/mybatis-3/z…
下面只简单的讲解
properties
提供了更灵活的配置使用,像上述demo中就通过资源文件动态配置了JDBC
信息数据,可以使用resource
或者url
属性进行文件配置导入,也可使用子标签<property name="a" value="1"/>
配置属性
- 前提是资源文件在类路径下才能找到资源
environments
用于配置环境信息,有一个default
属性,可以用于执行采用那个environment
标签的配置信息,需要和对应标签的id
一致
environment
,这里配置的是具体的数据源信息,其中的id
属性就是给其父标签environments
的default
属性使用的- 一个
environment
可以理解为就是一个环境,对应着一个SqlSessionFactory
对象
transactionManager
用于配置事务管理器,其中有个type
属性可供使用
JDBC
,配置为这个值,则采用JDBC
的事务机制,相当于默认开启事务并且不会自动提交,需要开发者手动的进行commit
事务MANAGED
,配置这个值会将事务机制交给其他容器管理,如果没有对应容器则没有事务,执行一次DML(增删改查)
就提交一次
dataSource
用于指定数据源相关的信息,此标签有一个type
属性可以配置连接池的策略,不同的type
决定着子标签的property
可以配置不同的属性值
UNPOOLED
采用最传统的方式获取数据库链接,并且不会有连接池的概念,每一次访问都是新的链接
-
driver
配置jdbc
驱动的java
全限定类名url
配置数据库的jdbc
地址username
配置数据库用户名password
配置数据库用户密码defaultTransactionlsolationLevel
配置默认的事务隔离级别(此处为Mysql
的知识点)defaultNetworkTimeout
配置等待数据库操作的默认网络超时时间
POOLED
采用了连接池的规范实现
-
- 上述
UNPOOLED
策略列出的所有都可以配置 poolMaximumActiveConnections
在任意时间可以使用的最大连接数量,默认值是10
,若一个请求占用一个线程,此时有20个请求,同一时间内只能处理10个请求,线程就会占满,第11个请求只能等待空闲线程poolMaximumldleConnections
任意时间可能存在的空闲连接数,默认值是5,如果已经有个5个线程是空闲状态,当出现第6个空闲线程时,会将此线程关闭减少数据库开销poolMaximumCheckoutTime
强制回归线程池时间,默认值为20spoolTimetoWait
当无法获取到空闲连接时,每20s打印一次日志- 其他不太常用
- 上述
property
该标签提供了更为灵活的配置方式,如将配置文件用其他文件格式的资源文件进行管理,上面第一个例子就是使用了引入外部资源的方式,配置resource
属性
mapper
用于指定SQL
映射文件路径
- 通过引入类路径下的资源文件
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
- 通过
file:///
格式的url
进行查找,一般不会使用这个,移植性太差 - 通过
Class
接口的全限定类型进行配置,mybatis
底层会进行动态代理,假设有这么一个UserMapper
类,配置完后会自动进行映射,前提是对应的namespace
和id
等需要与类名
及方法名
一一对应上,还有最关键一点:xml
文件需要和接口类放到同一个目录下
<mappers>
<mapper class="cn.mgl.mapper.UserMapper"/>
</mappers>
- 通过配置包名,这个是基于方式3的前提下,更便捷的配置映射,原本使用的
mapper
标签更改为package
标签,整个包下的接口都会批量的进行映射
<mappers>
<package name="cn.mgl.mapper"/>
</mappers>
传入数据后获取到自增的id
正常情况下,执行一个新增方法,会传入一个对应的实体类,但一般这个实体类是没有id
的,如果需要插入成功后数据表中对应的自增id
,要么自己根据条件再查询一遍获取,或者可以通过配置,在insert
标签中开启useGeneratedKeys
和指定propertKey
<insert id="insertUser" parameterType="cn.mgl.pojo.User" useGeneratedKeys="true" keyProperty="id">
insert into t_user
(username, password, age, gender)
values (#{username}, #{password}, #{age}, #{gender})
</insert>
后续的使用中,没有添加上述属性时,传入的User
实例是不会有id
属性的,配置完了再次执行查看输出
public static void main(String[] args) {
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
User user = new User("ma", "!23", '1', 20);
mapper.insertUser(user);
System.out.println(user);
sqlSession.close();
}
可以看到id
成功被赋值了
Mybatis的参数处理
使用的代码示例当中,语句标签的parameterType
上面说过可以省略,sql
中使用的#{key}
实际是个省略写法,他的完整写法如下
...sql忽略... where name=#{name, javaType=String, jdbcType=VARCHAR}
一般情况是都忽略不用写的,像7种基本数据类型以及对应的包装类,还有String,Date
这些参数类型,mybatis
会自己进行类型推导,最终由推导结果来觉得调用JDBC
中的setString,setInt
等等
多参数处理
之前演示的都是单参数形式,多参数的处理有一些规则需要遵守,先编写一个需要传入两个参数的sql
及方法
<select id="giveTwoParameter" resultType="cn.mgl.pojo.User">
select id, username, password, gender, age, created_at as createdAt
from t_user
where username = ${usernmae}
and age = #{age}
</select>
新增一个接口方法
User giveTwoParameter(String username,Integer age);
执行方法,传入一个姓名和年龄
public static void main(String[] args) {
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
mapper.giveTwoParameter("ma",20);
sqlSession.close();
}
不出意外的话可以看到报错,提示username
找不到,可以用的占位符key
有 [arg1, arg0, param1, param2]
,需要说明一下,如果使用arg
则后面的序号从0
开始,如果使用param
,则后面的序号从1
开始
只需要按照提示,将原本的#{username}
和#{age}
替换成arg格式
或param格式
的即可
实际上,mybatis
在底层实现中,有一个Map
集合进行数据收集
Map<String,Object> map = new HashMap<>();
map.put("arg0",key);
map.put("param1",key);
// 每个参数都以此类推
@Param注解的使用
MyBatis
在代理生成对象时,会去检查入参的参数是否带有@Parmas
的注解,如果有,则会新建一个Map
集合用来存放最终的key and value
,当使用了这个注解后,等价于把默认的arg格式
的key
给替换成我们自定义的key
,但param格式
的key还保留着。修改接口中的参数,添加对应的注解值
User giveTwoParameter(@Param("username") String username,@Param("age") Integer age);
对应的UserMapper.xml
中占位符使用注解传入的key
即可
可以尝试用Map去接受返回值
当不知道用什么对应的实体类接收SQL
的返回值时,可以用Map
类型,Mybatis
会自动转换成键值对的形式,但一般不建议这么做,可读性太差了
当数据库字段与Java对象属性无法匹配时
- 可以通过
sql
语句的as
关键字对需要查询出数据的列名进行重命名成Java
属性对应的name
- 上面演示过的通过配置
setting
标签中的,驼峰转换自动映射,前提是数据库的列名和Java
的属性名都遵循规范,如字段名为user_name
,则对应的Java
属性名为userName
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"></setting>
</settings>
- 通过定义
resultMap
来手动映射,以示例中出现过的的User
类举例
<resultMap id="customMap" type="cn.mgl.pojo.User">
<id property="id" column="id"></id>
<result property="age" column="age"></result>
<result property="username" column="username"></result>
<result property="password" column="password"></result>
<result property="gender" column="gender"></result>
<result property="age" column="age"></result>
<result property="createdAt" column="created_at"></result>
</resultMap>
<select id="getByXxx" resultMap="customMap"></select>
这里的id
为配置主键,其他的属性用result
标签,其中property
为Java
对象的属性,column
为查询结果集的字段名,后续在指定select
标签的返回值时,不再指定resultType
,而是指定resultMap
,传入的就是定义map
时的id
Mybatis内置的类型
像一些常规的int、long,map,string
等类型,不需要定义就可以在resultType
中使用,这是因为框架内置了映射,具体的内置配置可以查看 mybatis.org/mybatis-3/z…,翻到类型别名处,文档又说明
Mybatis比较高级的特性--动态SQL
所谓的动态,就是能方便开发者进行一些有条件及便携的sql
语句拼接
if标签
以一个常见的场景来说,需要查询某个数据列表,可以提供一些查询条件,这些条件可传可不传,此时就可以使用if
标签了
<select id="testIfLabel" parameterType="user">
select * from t_user
where 1 = 1
<if test="gender != null">
and gender = #{gender}
</if>
</select>
这里where
语句后面的1=1
是为了保证即使if
标签的条件没有通过,也不会报语句错误。当传入的User
对象的gender
对象不为空,则会进行拼接,否则不会,执行到1=1
就结束了
where标签
在上面的例子中,为了不报错需要书写1=1
这种特殊处理,where
标签能够更智能的处理这种情况
- 在
where
标签下的if
标签,如果条件不通过,则不会生成子句 - 会自动去掉某些条件前多余的
and
或者or
,参考上面if
标签的使用中and gender = #{gender}
中的and
利用where
标签改造一下
<select id="testWhereLabel" parameterType="user">
select * from t_user
<where>
<if test="gender != null">
and gender = #{gender}
</if>
</where>
</select>
此时若过gender
为null
,最终的sql
为select * from t_user
,反之则是select * from t_user where gender = #{gender}
,这里的and
会被智能的去掉,如果有多个子标签,则and
会被保留
trim标签
个人用的不太多,主要作用是可以在语句前后做增加和删除
prefix
:添加前缀suffix
:添加后缀prefixOverrides
:删除前缀suffixOverrides
:删除后缀
简单的演示下使用
<select id="testTrimLabel" parameterType="user">
select * from t_user
<trim prefix="where" suffixOverrides=",">
<if test="gender != null">
gender = #{gender},
</if>
</trim>
</select>
当if
标签中的条件不满足时,也会智能不处理,若满足条件,则在包括的语句前添加prefix
,并且删除掉语句末尾的suffixOverrides
set标签
一般用于update
语句中,用起来其实和where
标签很类似
<update id="testSetLabel" parameterType="cn.mgl.pojo.User">
update t_user
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="gender != null and gender != ''">
gender = #{gender},
</if>
</set>
</update>
上面的语句中,实现的需求就是当username
和gender
不是null
或者空字符串时,才会拼接set
语句,并且会去掉子句后面的,
。这里有个问题就是当二者都不满足条件,就会产出一条错误的sql
语句,更推荐用if
+手写sql
的方式来保证sql
语句的正确性
choese when otherwise
这个其实就相当于if...else if...else
的语法,都是配合着使用,还是以查询为例子,可以提供username,age
的查询条件,当提供了username
则使用username
,如果没有提供username
却提供了age
,则使用age
,否则则使用默认的查询条件
<select id="testChooseLabel" parameterType="cn.mgl.pojo.User">
select * from t_user
<where>
<choose>
<when test="username != null and username != ''">
username = #{username}
</when>
<when test="gender != null and gender != ''">
gender = #{gender}
</when>
<otherwise>
age < 10
</otherwise>
</choose>
</where>
</select>
foreach标签
可用于循环遍历数组或集合,一般常用与批量增删改操作,有以下属性可以使用
collection
:需要被循环的值item
:被循环的每个元素可用别名index
:循环的索引open
:在子句生成的开头插入指定字符close
:在子句生成的结尾插入指定字符separator
:循环之间的分隔符
以删除语句为例子,这里提供了两种写法,都可以实现批量删除
<delete id="testForeachOfDel">
delete from t_user
where
<foreach collection="ids" item="id" separator=",">
id = #{id}
</foreach>
</delete>
<delete id="testForeachOfDel2">
delete from t_user
where id in (
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
)
</delete>
需要注意的是,这里collection
使用了ids
,是因为对应接口定义时参数使用了@Param("ids") List<Long> ids
,否则运行后会报错,并告知你应该使用arg0, collection, list
这些底层映射的默认可用值,显然这些可读性不好,更应该指定map
的key
,目前的设置,可用的就变为了ids
和param1
像批量插入和修改都是同理的,只要是合法的sql
语句就能利用foreach
标签批量构建出预期值
sql标签和include标签
前者用来定义sql
片段,后者用来引入到某个sql
中,用批量删除的语句来演示
<delete id="testForeachOfDel2">
<include refid="delSlice"></include>
where id in (
<foreach collection="ids" item="id" separator=",">
#{id}
</foreach>
)
</delete>
<sql id="delSlice">
delete
from t_user
</sql>
用法比较简单,可以根据需求来决定提取哪些是可以复用的语句,比如繁琐的字段名就很适合定义为片段
Mybatis的高级映射
假设当前新增了一个Dept
类存储部门信息,且每个用户都有对应的部门,此时就形成了多对一的关系,那么User
对象当中就应该多一个Dept
类型的成员属性
先定义一个部门类
package cn.mgl.pojo;
public class Dept {
private Long dept_id;
private String dept_name;
public Long getDept_id() {
return dept_id;
}
public void setDept_id(Long dept_id) {
this.dept_id = dept_id;
}
public String getDept_name() {
return dept_name;
}
public void setDept_name(String dept_name) {
this.dept_name = dept_name;
}
}
在数据库中也要创建对应的表结构
CREATE TABLE `t_dept` (
`dept_id` int DEFAULT NULL,
`dept_name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
除此之外还需要对原本User
类添加一个成员属性dept
,类型为Dept
,并添加对应的getter,setter
函数
(相关的数据库数据自行添加)
多对一映射的方式:
- 一条外链接
Sql
映射结果
<resultMap id="userMap" type="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="dept.dept_id" column="dept_id"></result>
<result property="dept.dept_name" column="dept_name"></result>
</resultMap>
<select id="testMapping" resultMap="userMap">
select a.username,
a.age,
a.gender,
a.created_at,
a.updated_at,
a.id,
b.dept_id,
b.dept_name
from t_user a
left join t_dept b on a.dept_id = b.dept_id
</select>
这种用法的select
标签不使用resultType
而是resultMap
,注意对应的写法:<result property="dept.dept_id" column="dept_id"></result>
中的dept
为User
类中的属性名,不可以随便写,这里可以不需要把所有映射都写上,如果确定类属性和结果集列名匹配得上可以不写
执行相关语句后,查询用户时也能把部门信息查出来并映射到成员嵌套对象中
- 使用映射中的关联标签
<resultMap id="userMap" type="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<association property="dept" javaType="dept">
<id property="deptId" column="dept_id"></id>
<result property="deptName" column="dept_name"></result>
</association>
<!-- <result property="dept.dept_id" column="dept_id"></result>-->
<!-- <result property="dept.dept_name" column="dept_name"></result>-->
</resultMap>
只需要把原本的手动级联映射改为association
标签,设置javaType
为Dept
类,接着配置类属性和字段名映射关系,实现效果和方式一完全一致
- 分步查询
所谓的分布查询就是将查询分为步骤一、步骤二...以此类推,比如这个实例中,可以先查询出用户表信息,这是第一步,由于用户表存储了部门id,所以可以通过查询出来的部门id再去查询相关部门信息,这是第二步
新增一个DeptDao
的接口,目前只写一个抽象方法
package cn.mgl.dao;
import cn.mgl.pojo.Dept;
public interface DeptDao {
Dept getById(Long id);
}
接着添加对应的deptMapper.xml
,编写一条根据id
查询信息的sql
(需要在mybatis-config.xml
添加mapper
)
<?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="cn.mgl.dao.DeptDao">
<select id="getById" resultType="dept">
select dept_id, dept_name
from t_dept
where dept_id = #{deptId}
</select>
</mapper>
改写原本UserMapper.xml
编写的查询,不需要外链接,只写简单的单表查询,并且association
中添加一个select
属性,指定为namespace+id
的格式,column
属性则为子语句的查询条件
<resultMap id="userMap" type="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<association property="dept" select="cn.mgl.dao.DeptDao.getById" column="dept_id"></association>
<!-- <association property="dept" javaType="dept">-->
<!-- <id property="deptId" column="dept_id"></id>-->
<!-- <result property="deptName" column="dept_name"></result>-->
<!-- </association>-->
<!-- <result property="dept.dept_id" column="dept_id"></result>-->
<!-- <result property="dept.dept_name" column="dept_name"></result>-->
</resultMap>
<select id="testMapping" resultMap="userMap">
select username,
age,
gender,
created_at,
updated_at,
id
from t_user
</select>
执行查询后,可以看到执行了两条语句,并且会自动将第一步查询出来的dept_id
当作查询参数传入第二条sql
中
这样做的好处:
- 代码的复用性增强,耦合度降低
- 支持延迟加载,访问不到的数据可以先不查询,增加查询效率
看完多对一,接下来看看一对多
- 利用
collection
标签
其实区别只是主体不同,现在要改成查询部门下的全部用户,则主体变更为部门,那么此时部门的类中应该有一个用户类型的集合才存储该部门下的所有用户(下面省略Java代码编写,自行操作)
<resultMap id="clazzCollect" type="clazz">
<id property="clazzId" column="dept_id"></id>
<result property="clazzName" column="dept_name"></result>
<collection property="collection" ofType="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
</collection>
</resultMap>
<select id="testGetByCollection" resultMap="clazzCollect">
select a.dept_id, a.dept_name, b.id, b.dept_id, b.username, b.gender, b.age
from t_dept a
left join t_user b on a.dept_id = b.dept_id
</select>
在resultMap
中,有一个collection
子标签,其中的property
写了collection
是因为在User
类中有成员属性名就是collection
,后面的ofType
填写这个集合中类型,子标签的用法就大同小异了
执行查询后,虽然sql
结果集为4
条,但会将最终映射出来的数据只有3
条(同一个部门归类),且Dept
类中的集合成功赋值
- 和一对多类似的分步查询
这个原理就一样了,先做第一步查出部门,再做第二步查出部门下所有用户
<resultMap id="clazzCollect" type="clazz">
<id property="clazzId" column="dept_id"></id>
<result property="clazzName" column="dept_name"></result>
<collection property="collection" select="cn.mgl.dao.UserDao.selectByDeptId" column="dept_id">
</collection>
</resultMap>
<select id="testGetByCollection" resultMap="clazzCollect">
select dept_id, dept_name
from t_dept
</select>
与第一种方式的区别是,原本的连表查询改为单表查询,并且collection
标签中新增select
属性,对应的查询方法自行添加,实现的最终效果也完全一致
延迟加载
所谓的延迟加载就是没用到的时候不执行对应的sql
,用到再执行,以减少查询的方式增加性能
一对多与多对一中的延迟加载机制是一样的
- 对某个查询单独使用
fetchType="lazy"
,用上面的例子演示
<resultMap id="clazzCollect" type="clazz">
<id property="clazzId" column="dept_id"></id>
<result property="clazzName" column="dept_name"></result>
<collection fetchType="lazy" property="collection" select="cn.mgl.dao.UserDao.selectByDeptId" column="dept_id">
</collection>
</resultMap>
<select id="testGetByCollection" resultMap="clazzCollect">
select dept_id, dept_name
from t_dept
</select>
- 在全局配置中,添加一个
setting
标签,设置lazyLoadingEnabled="true"
,此时全部sql
都会走延时加载
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"></setting>
<setting name="logImpl" value="STDOUT_LOGGING"></setting>
<setting name="lazyLoadingEnabled" value="true"></setting>
</settings>
若在开启全局配置后,就是想要某个sql
不走延时加载,只需要添加fetchType="eager"
即可
一般推荐全局开启,减少查询增加性能
了解Mybatis的缓存机制
如这次执行查询后,将查询结果放到内存中,如果下次还是这条查询语句,则直接从缓存中取,不需要再去数据库中查询,以此来提高效率性能,但是只对select
语句有效
一级缓存
针对的事sqlSession
下面的例子中:
SqlSessionUtil.openSqlSession
简单封装的获取sqlSession
对象mapper.test
是一条简单的selectSql
mapper.update
是一条简单的updateSql
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
List<User> test = mapper.test();
List<User> test2 = mapper.test();
System.out.println(test2);
System.out.println(test);
运行后,select
只执行了一次,第二次再次查询直接从缓存中取,所以没有sql
执行日志输出
下面演示的是一级缓存失效
- 手动执行
sqlSession的clearCache方法
,sqlSession.clearCache();
,比较简单不演示 - 在同一个
SqlSession
的情况下,两条select
语句之间执行了update,insert,delete
任一,一级缓存会被清空,代码用update
举例子
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
List<User> test = mapper.test();
User user = new User();
sqlSession.clearCache();
user.setId(14L);
mapper.updateUser(user);
List<User> test2 = mapper.test();
System.out.println(test2);
System.out.println(test);
运行后可以看到,即使再次执行相同的select
,也不会走缓存
- 不同
SqlSession
执行相同的select
查询
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
SqlSession sqlSession2 = SqlSessionUtil.openSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
UserDao mapper2 = sqlSession2.getMapper(UserDao.class);
List<User> test = mapper.test();
List<User> test2 = mapper2.test();
System.out.println(test2);
System.out.println(test);
运行后可以看到执行了两次查询
二级缓存
这个设置需要满足一定的条件才会开启,且是针对SqlSessionFactory
的,前面说过可以根据不同的环境获取不同的SqlSessionFactory
,如有两个SqlSession
,即使他们都执行一样的sql
语句,也不会走二级缓存
开启前提条件:
mybatis-config.xml
中,settings
下的标签卡其了全局缓存,默认就是true
,<setting name="cacheEnabled" value="true">
,相当于不配置就是开启- 在需要使用缓存的
SqlMapper.xml
中添加一个<cache/>
标签 - 被缓存的
pojo
类必须实现Serializable
接口,让其拥有可序列化特性 sqlSession
对象关闭或提交之后,一级缓存中的数据才会被写入二级缓存中,此时缓存才能体现
前三步比较简单,直接看第四步的代码
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
List<User> test = mapper.test();
System.out.println(test);
sqlSession.close();
SqlSession sqlSession2 = SqlSessionUtil.openSqlSession();
UserDao mapper2 = sqlSession2.getMapper(UserDao.class);
List<User> test2 = mapper2.test();
System.out.println(test2);
运行后的输出,有一句Cache Hit Ratio
且没有第二次sql
日志输出,说明缓存命中了
二级缓存失效情况
- 和一级缓存一样,中间执行过
select,update,insert
语句时,缓存也会失效 - 不同的
sqlSessionFactory
对象
想测试的话比较简单,便不再演示
查询分页
Mybatis使用PageHelper插件
这个插件是帮助处理分页需求的,正常情况下分页查询需要接上limit startIndex,pageSize
进行查询,通过这个插件可以让开发过程减少一些重复冗余的分页编写
通过maven
添加依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.2</version>
</dependency>
接着在mybatis-config.xml
中添加插件配置,注意顺序问题
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
sql
中不需要添加分页语句,只需要在查询语句执行前,调用一次startPage
方法并传入相关参数,简单讲讲传参的计算,假设以10条数据为一页,查询第2页的数据,则需要传入2,10
,后续的计算中则为(2-1)*10,10
,最终结果就是limit 10,10
public static void main(String[] args) {
SqlSession sqlSession = SqlSessionUtil.openSqlSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
PageHelper.startPage(2,10);
List<User> users = mapper.selectAll();
users.forEach(System.out::println);
PageInfo<User> tPageInfo = new PageInfo<>(users);
sqlSession.close();
}
简单的说下原理,执行了插件方法
startPage
后,会缓存分页数据到ThreadLoacl
当中,只会对执行了startPage
方法后的第一个查询语句起效
如果不用分页插件也可以实现,先查询出表的总条数,再按照分页规则自行处理sql
即可,插件只是帮忙处理了这几个步骤
注解式开发
mybatis
除了xml
以外,还提供了注解式开发,不过这种方式并不推荐使用,因为不好维护,除非是非常简单的sql
语句,能用xml
尽量用,不过也可以了解下使用方式,以基础的增删改查为例子,分别使用对应的注解,并传入sql
的字符串,效果与xml
文件配置是相同的
public interface UserAnnoDao {
@Insert("insert into t_user (username, password, age, gender) values (#{username}, #{password}, #{age}, #{gender})")
int insertUser(User user);
@Delete("delete from t_user where id = #{id}}")
int deleteUser(Long id);
@Update("update t_user set username = #{username} where id = #{id}")
int updateUser(User suer);
@Select("select * from t_user where id #{id}}")
User selectById(Long id);
}
完:)
转载自:https://juejin.cn/post/7178680539514142779