1 概述
本文主要讲述了如何使用MyBatis
中的映射器以及动态SQL
的配置。
2 MyBatis
配置文件概览
MyBatis
配置文件主要属性如下:
:相关设置,键值对形式
:类型别名
:类型处理器
:对象工厂
:插件,包含若干个
:环境配置,包含若干个
,在
中可以指定事务管理器
以及数据源
:数据库厂商标识
:映射器,包含若干个
注意顺序不能颠倒,否则启动时会发生异常。
3 准备步骤
由于本文大部分的代码都只给出了关键的语句而没有完整的工程,因此如果想要实现一遍请clone
此处的代码(Kotlin
请clone
此处),并:
- 利用
resources/sql
下的脚本文件创建数据库以及数据表,并插入相应数据 - 修改
MyBatis
、Spring
、dhcp2
等依赖为最新版本并修改MySQL
驱动为对应版本 - 修改
applicationContext.xml
文件中的数据库用户名,密码以及数据库URL
,可能需要修改驱动 - 开启数据库服务并进行测试,运行
MainTest
中的测试方法即可,正常来说会出现如下结果:
4 映射器概述
MyBatis
的映射器由一个接口加上XML
映射文件组成,是最复杂的组件,映射文件常用元素如下:
:查询语句
/
/
:插入
/更新
/删除
语句,返回操作所影响的行数,比如插入了两行,操作成功了影响的行数则为两行,返回整数2
:自定义的SQL
:提供映射规则
下面先来看一下最常用的。
4.1
示例(在mapper/UserDao.xml
直接添加即可):
其中id
是唯一标识符,接受一个Integer
,返回com.pojo.User
对象,结果集自动映射到com.pojo.User
中。
常用属性如下:
id
:语句的全局唯一标识符
paramterType
:表示传入SQL
语句的参数类型的全限定名或别名,可选,能自动推断resultType
:执行SQL
后返回的类型resultMap
:与resultType
类似,resultType
默认一一对应映射,比如表字段名为id
,则映射到实体类的id
中,而resultMap
需要手动定义映射关系,这样就可以把表字段中的id
映射到实体类的id1
,或id2
,或id3
,resultType
与resultMap
两者需要指定一个,不能同时存在flushCache
:设置调用SQL
后是否要求MyBatis
清空之前查询的本地缓存以及二级缓存,默认false
useCache
:启动二级缓存,默认true
timeout
:超时参数,单位秒fetchSize
:获取记录的总条数设定statementType
:使用哪个JDBC
的Statement
,取值可以为STATEMENT
/PREPARED
/CALLABLE
,分别表示Statement
/PreparedStatement
/CallableStatement
resultSetType
:针对JDBC
的ResultSet
,可设置为FORWARD_ONLY
/SCROLL_SENSITIVE
/SCROLL_INSENSITIVE
,分别表示只允许向前访问
/双向滚动,不及时更新
/双向滚动,及时更新
并修改UserDao
,添加一个selectById
方法:
User selectById(Integer id);
可以直接测试了:
@Test
public void selectById()
{
System.out.println(dao.selectById(1));
}
下面来看一下如何传递多个参数。
4.2 传递参数
有了最基本的select
后,传递id
这种单一参数很容易,但是实际情况中很多时候需要传递多个参数,MyBatis
中传递多个参数有两种方式:
- 通过
Map
传递 - 通过
JavaBean
传递
4.2.1 Map
可以使用Map
传递多个参数,示例如下:
参数名name
以及age
是Map
的键。
接着在UserDao
下添加:
User selectByMap(Map map);
然后在主类中使用Map
添加键值对:
@Test
public void selectByMap()
{
Map map = new HashMap<>();
map.put("name","111");
map.put("age","33");
System.out.println(dao.selectByMap(map));
}
这样就能传递多个参数进行查询了。
4.1.2 使用JavaBean
传递多个参数的另一种方法是利用JavaBean
传递,创建一个POJO
类:
@Getter
@Setter
@Builder
@ToString
public class UserPOJO {
private String name;
private Integer age;
}
修改UserDao
接口方法:
public User selectByPOJO(UserPOJO user)
接着修改映射文件,实际上修改parameterType
即可:
注意访问传递的参数时直接使用POJO
类的属性名即可,无须加上类似UserPOJO.
的前缀。
最后进行测试:
@Test
public void selectByPOJO()
{
UserPOJO pojo = UserPOJO.builder().age(33).name("111").build();
System.out.println(dao.selectByPOJO(pojo));
}
4.2
用于插入,大部分属性与相同,下面是几个特有属性:
keyProperty
:将插入操作的返回值赋给POJO
类的某个属性keyColumn
:用于设置主键列的位置,当表中第1列不是主键时需要设置该参数,联合主键可以使用逗号分隔useGeneratedKeys
:使用JDBC
的getGeneratedKeys
获取数据库内部产生的主键,默认false
比如典型的主键回填
如下:
insert into user(name, age) values (#{name}, #{id})
这样就会利用数据库生成的自增主键回填到User
的id
属性中,UserDao
接口如下:
int insertUser1(User user);
一般来说插入操作返回一个整数,表示操作影响的行数,因此可以设置返回值为int
,测试如下:
@Test
public void insertUser1()
{
User user = User.builder().age((short) 88).name("test1").build();
System.out.println(dao.insertUser1(user));
System.out.println(user.getId());
}
另外如果不支持自增主键,可以使用selectKey
自定义生成主键,比如:
select if(max(id) is null,1,max(id)+1) as newId from user
insert into user(id,name,age) values(#{id},#{name},#{age})
中的keyProperty
指定了新主键newId
返回给pers.pojo.User
的id
属性,order
设置执行顺序,BEFORE
/AFTER
表示执行
之后/之前再执行插入语句。
测试:
@Test
public void insertUser2()
{
User user = User.builder().age((short) 10).name("test2").build();
System.out.println(dao.insertUser2(user));
System.out.println(user.getId());
}
4.3
/
返回一个整数,属性与
/类似,简单示例如下:
update user set name=#{name}, age=#{age} where id = #{id}
delete from user where id = #{id}
同理update/delete
返回一个整数,表示操作影响的行数,因此设置UserDao
接口如下:
int updateUser(User user);
int deleteUser(Integer id);
测试:
@Test
public void updateUser()
{
User user = User.builder().id(3).name("3333333").age((short)11).build();
selectAll();
System.out.println(dao.updateUser(user));
selectAll();
}
@Test
public void deleteUser()
{
selectAll();
System.out.println(dao.deleteUser(3));
selectAll();
}
4.4
用于定义SQL
的一部分,以方便后面的SQL
语句引用,比如:
id,name,age
UserDao
接口:
List selectBySqlColumn();
测试:
@Test
public void selectBySqlColumn()
{
System.out.println(dao.selectBySqlColumn());
}
5
上面提高过,
比
要强大,但是需要手动定义映射关系,一个常见的
如下:
5.1 使用Map
查询SQL
的结果可以使用Map
/POJO
存储,使用Map
存储不需要手动编写
,默认表属性名是键值对的键:
可用List
来接收返回结果,一条记录映射到一个Map
对象,Map
中的key
是select
的字段名。
示例的UserDao
方法如下:
List
其中Map
类型为Map
,测试方法如下:
@Test
public void selectReturnMap()
{
dao.selectReturnMap().forEach(System.out::println);
}
5.2 使用POJO
如果使用POJO
存储返回的对象时,需要先定义一个POJO
类,可以在上面的UserPOJO
基础上加上一个id
属性:
@Getter
@Setter
@Builder
@ToString
public class UserPOJO {
private Integer id;
private String name;
private Integer age;
}
接着编写映射文件:
其中property
指定POJO
的属性,column
是表字段名,最后配合使用,指定
resultMap
为对应id
:
返回结果可以用List
接收:
List selectReturnPOJO();
测试方法:
@Test
public void selectReturnPOJO()
{
dao.selectReturnPOJO().forEach(System.out::println);
}
6 级联查询
级联查询就是利用主键与外键的关系进行组合查询,比如表A
的一个外键引用了表B
的一个主键,查询A
时,通过A
的外键将B
的相关记录返回,这就是级联查询。常见的级联查询有三种:
- 一对一
- 一对多
- 多对多
MyBatis
支持一对一以及一对多级联,没有对多对多级联提供支持,但是可以用多个一对多级联实现多对多级联。下面分别来看一下。
6.1 一对一
一对一级联查询是最常见的级联查询,可以通过
中的
进行配置,通常使用的属性如下:
property
:映射到实体类的对象属性column
:指定表中对应的字段javaType
:指定映射到实体对象属性的类型select
:指定引入嵌套查询的子SQL
语句,用于关联映射中的嵌套查询
下面通过一个例子进行说明,例子分五步:
- 创建数据表
- 创建实体类
- 编写映射文件
- 修改持久层接口
- 添加测试方法
6.1.1 数据表
为了方便新增表以及数据都写在一起:
use test;
drop table if exists idcard;
drop table if exists person;
create table idcard(
id int(10) primary key auto_increment,
code char(18) collate utf8mb4_unicode_ci default null
);
create table person(
id int(10) primary key,
name varchar(20) collate utf8mb4_unicode_ci default null,
age smallint default null,
idcard_id int(10) default null,
key idcard_id(idcard_id),
constraint idcard_id foreign key (idcard_id) references idcard(id)
);
insert into idcard(`code`) values('123456789123456789');
insert into person(`id`,`name`,`age`,`idcard_id`) values (1,'111',22,1);
6.1.2 实体类
@Data
public class IdCard {
private Integer id;
private String code;
}
@Data
public class Person {
private Integer id;
private String name;
private Integer age;
private IdCard card;
}
另外还需要创建一个映射结果的POJO
类:
@Data
public class PersonPOJO {
private Integer id;
private String name;
private Short age;
private String code;
}
6.1.3 映射文件
映射文件分为两个:
IdCardMapper.xml
PersonMapper.xml
首先是IdCardMapper.xml
,加上一个即可,注意
namespace
的位置填写正确,对应dao
的位置。
其次是PersonMapper.xml
:
首先第一个
先指定id
等属性,接着是
:
property
是实体类属性,注意类型为IdCard
column
是表字段名,类型为int(10)
javaType
是通过后面的select
返回的类型,可以理解成是property
的类型,也就是IdCard
select
指定嵌套查询使用的SQL
,对应于IdCardDao.xml
中的selectCodeById
接着在一个中的
resultMap
指定该map
的id
即可。使用这种方法执行的是两次SQL
:
- 一次是
select * from person where id=?
- 一次是
select * from idcard where id=?
最后再把结果整起起来,开启调试可以发现实际上也是执行了两条SQL
:
而第二个
中,在
里面没有了select
属性,直接将结果映射到SelectPersonById
中,这是执行一条SQL
语句的结果:
select p.*,ic.code from person p,idcard ic where p.idcard_id = ic.id and p.id=#{id}
实际查询如下:
如果需要重要可以将其配置成
,比如:
而最后一个是进行连接查询,无需额外的
,实际执行情况如下:
6.1.4 Dao
接口
这个比较简单:
public interface PersonDao {
Person selectPersonById1(Integer id);
Person selectPersonById2(Integer id);
PersonPOJO selectPersonById3(Integer id);
}
6.1.5 测试
@Test
public void selectPersonById()
{
System.out.println(dao.selectPersonById1(1));
System.out.println(dao.selectPersonById2(1));
System.out.println(dao.selectPersonById3(1));
}
注意在测试之前,需要修改配置文件mybatis-config.xml
:
前两个
表示开启延迟加载以及按需加载,后面一个是设置调试开关,最后在下面的
加上
对应的xml
的位置。
要注意的一个是
需要写在
的前面。
另外因为Dao
接口没有加上@Mapper
注解,因此需要在applicationContext.xml
中手动加上Dao
位置:
测试结果:
6.2 一对多
一对多的级联查询与一对一处理有相似之处,主要是映射文件中的
配置,例子也是和上面一样分五步。
6.2.1 数据表
需要两张表:
user
order
user
可以沿用前面的user
表,而order
表如下:
use test;
drop table if exists orders;
create table orders(
id int(10) primary key auto_increment,
ordersn varchar(10) collate utf8mb4_unicode_ci default null,
user_id int(10) default null,
key user_id(user_id),
constraint user_id foreign key (user_id) references user(id)
);
insert into orders(`ordersn`,`user_id`) values ('testorder1',1),('testorder2',1),('testorder3',1);
6.2.2 实体类
添加实体类Orders
:
@Data
public class Orders {
private Integer id;
private String ordersn;
}
同时创建一个带Orders
的User
:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserWithOrders {
private Integer id;
private String name;
private Short age;
private List ordersList;
}
6.2.3 映射文件
两个:
OrdersMapper.xml
UserWithOrdersMapper.xml
首先是OrdersMapper.xml
,只有一个简单的:
接着是UserWithOrdersMapper.xml
:
相比起一对一的级联,重点改变的就是其中的
,重要属性如下:
property
:指定实体类的属性字段ofType
:指定集合中的类型column
:将哪些值传递给select
中的方法select
:嵌套查询的语句
第二个
类似,将查询的结果直接映射到Orders
的属性上面。最后一种是直接使用连接查询。
6.2.4 Dao
接口
public interface OrdersDao {
List selectOrdersById(Integer id);
}
public interface UserWithOrdersDao {
UserWithOrders selectUserOrders1(Integer id);
UserWithOrders selectUserOrders2(Integer id);
List selectUserOrders3(Integer id);
}
6.2.5 测试
@Test
public void selectUserOrders()
{
System.out.println(dao.selectUserOrders1(1));
System.out.println(dao.selectUserOrders2(1));
System.out.println(dao.selectUserOrders3(1));
}
6.3 多对多
MyBaits
其实不支持多对多级联,但是可以通过多个一对多级联实现,比如一个订单对应多个商品,一个商品对应多个订单,这样两者就是多对多级联关系,这样使用一个中间表,就可以转换为两个一对多关系。
下面同样通过五个步骤实现多对多级联。
6.3.1 数据表
需要订单表、商品表以及一个中间表,由于订单表Orders
之前已创建,这里只需要创建两个表:
use test;
create table product(
id int(10) primary key auto_increment,
name varchar(10) collate utf8mb4_unicode_ci default null,
price double default null
);
create table orders_detail(
id int(10) primary key auto_increment,
orders_id int(10) default null,
product_id int(10) default null,
key orders_id(orders_id),
key product_id(product_id),
constraint orders_id foreign key (orders_id) references orders(id),
constraint product_id foreign key (product_id) references product(id)
);
insert into product(`name`,`price`) values('product1',1.1),('product2',2.2),('product3',3.3);
insert into orders_detail(`orders_id`,`product_id`) values(1,1),(1,2),(1,3),(2,1),(2,3);
6.3.2 实体类
订单类可以沿用之前的,只需要两个实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
private Integer id;
private String name;
private Double price;
private List orders;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrdersWithProduct {
private Integer id;
private String ordersn;
private List products;
}
6.3.3 映射文件
这里的多对多级联实质上是通过每次指定不同的OrdersId
去查询对应的Product
实现的,也就是分成了多次的一对多级联。
6.3.4 Dao
接口
public interface OrdersWithProductDao {
List selectOrdersAndProduct();
}
6.3.5 测试
@Test
public void test()
{
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
OrdersWithProductDao dao = context.getBean(OrdersWithProductDao.class);
System.out.println(dao.selectOrdersAndProduct());
}
7 动态SQL
最后来看一下动态SQL
,动态SQL
可以避免手动拼接SQL
语句,比如在某些条件成立的情况下添加and xxx=xxxx
之类的操作。先来看一下最常用的
。
7.1
类似Java
中的if
语句,最简单的例子如下:
也就是说当test
中的条件成立时,便添加and xxx
语句。注意test
这个属性是
必须的,不能省略。
(注这里用到了一个关键的1=1
,仅作说明使用,实际开发请勿使用1=1
进行拼接)
Dao
接口:
List selectByIf(User user);
测试:
@Test
public void testIf()
{
System.out.println(dao.selectByIf(User.builder().age((short) 33).name("111").build()));
}
7.2
+
+
类似Java
中的switch
语句:
类似switch
类似case
类似default
当其中一个
成立时,语句便结束,类似于自动加上了"break"
。
示例:
Dao
接口:
List selectByChoose(User user);
测试:
@Test
public void testChoose()
{
System.out.println(dao.selectByChoose(User.builder().age((short)33).build()));
}
7.3
主要功能:
- 加前缀
- 加后缀
- 替换某些首部/尾部内容
这里是一个使用
来实现
的例子:
Dao
接口:
List selectByTrim(User user);
测试:
@Test
public void testTrim()
{
System.out.println(dao.selectByTrim(User.builder().build()));
System.out.println(dao.selectByTrim(User.builder().name("test2").build()));
}
7.4
最常用的就是拼接查询条件,比如有多个查询条件,仅仅使用多个
的话会出现首个
有一个多余的and
的问题,而使用
会进行智能处理,当然也对or
适用,例子如下:
Dao
接口:
List selectByWhere(User user);
测试:
@Test
public void testWhere()
{
System.out.println(dao.selectByWhere(User.builder().build()));
System.out.println(dao.selectByWhere(User.builder().name("111").build()));
System.out.println(dao.selectByWhere(User.builder().age((short)-3).build()));
}
7.5
一般配合update
语句使用,比如:
update user
name = #{name},
age = #{age}
where id=#{id}
Dao
接口:
int updateBySet(User user);
测试:
@Test
public void testSet()
{
System.out.println(dao.updateBySet(User.builder().name("999999").age((short)39).id(1).build()));
System.out.println(dao.selectByWhere(User.builder().build()));
}
7.6
主要用于in
中,可以认为是一个集合,典型的使用场景是select xxx from xxx where xxx in
。
的主要属性有:
item
:每个元素的别名index
:每个元素的下标collection
:
的类型,有list
、array
、map
三种,当传入单个参数且该参数类型为List
时,则为list
,传入单个参数且该参数类型为数组时,则为array
,否则应将其封装成Map
,并设置属性值为map
open
:语句开始标志close
:语句结束标志
例子:
Dao
接口:
List selectByForeach(List id);
测试:
@Test
public void testForeach()
{
System.out.println(dao.selectByForeach(List.of(1,2,3)));
}
7.7
可用于对字符串进行拼接,对于字符串拼接,MySQL
使用的是concat
,而Oracle
使用的是||
,而MyBatis
提供了
可以屏蔽这种DBMS
之间的差异,无需修改xml
即可进行移植,例子如下:
Dao
接口:
List selectByBind(User user);
测试:
@Test
public void testBind()
{
System.out.println(dao.selectByBind(User.builder().name("test1").build()));
}
8 源码
此处给出了实现所有例子后的代码,仅供参考,但不建议直接clone
,建议从初始化工程开始逐步实现。
Java
版:
Kotlin
版: