本文由 通用mapper使用归纳而来
- 通用Mapper虽然方便,终于不用手写XML,但仍然要写POJO和Mapper接口。如果你们公司出于效率考虑,只允许增删改使用TkMapper,查询语句要自己手写XML(如果不需要手写sql则可以不用xml文件),那么你还要手动创建mapper.xml,工作量还是有一些的。
- 建议简单的增删改可以交给通用Mapper提供的接口,而查询最好自己手写SQL,做到接口和SQL分离,方便后期SQL优化及维护。
- 使用通用mapper需要注意的是 , 只能在一张表行进行操作!!!如果你要操作的是两张表+,则你需要手写sql语句
三个接口的作用
/**
* 自定义BaseMapper可以让数据库表对应的mapper接口继承(我们一般数据库表对应的mapper接口直接继承Mapper<T>,
* 比如【public interface ActDetailMapper extends Mapper<ActDetail>】但这样不够强大)
* <p>
* Mapper接口:拥有基本的增、删、改、查方法
* IdListMapper:支持根据IdList批量查询和删除
* InsertListMapper:支持批量插入
* 没有批量更新
* <p>
* 注意:一定要加@RegisterMapper,注册接口
* * @author bravo
* @date 2020-01-22 21:00
*/
@RegisterMapper
public interface BaseMapper<T> extends Mapper<T>, IdListMapper<T, Long>, InsertListMapper<T> {
}
常用sql
/**
* @author qiyu
* @date 2020-09-03 19:00:10
*/
@SpringBootTest
public class TkMapperTest {
@Autowired
private TkUserMapper userMapper;
/**
* INSERT INTO tk_user ( id,name,age,create_time,update_time,deleted )
* VALUES( null,'bravo',16,null,null,null );
*
* 注意,如果不带xxxSelective,默认会插入null,null也算一种赋值,所以数据库设置的默认值不会生效
*/
@Test
public void testInsert() {
TkUserPojo tkUserPojo = new TkUserPojo();
// 虽然我只设置了两个值,但实际执行会插入全量字段,默认null
tkUserPojo.setName("bravo");
tkUserPojo.setAge(16);
userMapper.insert(tkUserPojo);
}
/**
* INSERT INTO tk_user ( id,name,age ) VALUES( null,'bravoSelective',16 );
*
* 只会插入id,name,age三个字段,此时数据库默认值会生效
*/
@Test
public void testInsertSelective() {
TkUserPojo tkUserPojo = new TkUserPojo();
tkUserPojo.setName("bravoSelective");
tkUserPojo.setAge(16);
userMapper.insertSelective(tkUserPojo);
}
/**
* INSERT INTO tk_user ( id,name,age,create_time,update_time,deleted )
* VALUES
* ( null,'bravo1',18,null,null,null ) ,
* ( null,'bravo2',19,null,null,null ) ,
* ( null,'bravo3',20,null,null,null ) ,
* ( null,'bravo4',21,null,null,null )
* ...
*
* insertList(list)方法底层是insert()而不是insertSelective(),数据库默认值不会生效
*/
@Test
public void testInsertBatch() {
// 准备10个数据
List<TkUserPojo> list = new ArrayList<>();
TkUserPojo tkUserPojo = null;
for (int i = 1; i <= 10; i++) {
tkUserPojo = new TkUserPojo();
tkUserPojo.setName("bravo" + i);
tkUserPojo.setAge(17 + i);
list.add(tkUserPojo);
}
// 批量插入
userMapper.insertList(list);
}
/**
* UPDATE tk_user SET name = 'bravoSelective',age = 100,create_time = null,update_time = null,deleted = null
* WHERE id = 6;
*
* 没设置的值会被更新为null,可能产生两个问题:
* 1.数据库默认值不会生效
* 2.不想更新的字段被更新为null(严重错误)
*/
@Test
public void testUpdate() {
TkUserPojo tkUserPojo = new TkUserPojo();
tkUserPojo.setId(6L);
tkUserPojo.setName("bravoSelective");
tkUserPojo.setAge(100);
userMapper.updateByPrimaryKey(tkUserPojo);
}
/**
* UPDATE tk_user SET name = 'bravoSelective',age = 100
* WHERE id = 6;
*
* 只更新设置的字段
*/
@Test
public void testUpdateSelective() {
TkUserPojo tkUserPojo = new TkUserPojo();
tkUserPojo.setId(6L);
tkUserPojo.setName("bravoSelective");
tkUserPojo.setAge(100);
userMapper.updateByPrimaryKeySelective(tkUserPojo);
}
/**
* DELETE
* FROM tk_user
* WHERE id in (1,2,3,4,5);
*
* deleteByIdList底层用的是IN,是物理删除DELETE,不是逻辑删除UPDATE
*/
@Test
public void testDeleteBatch() {
List<Long> ids = Arrays.asList(1L, 2L, 3L, 4L, 5L);
userMapper.deleteByIdList(ids);
}
/**
* SELECT COUNT(*)
* FROM tk_user
* WHERE ( ( name like 'bravo' ) );
*
* 有两点注意:
* 1.这里的'name'只是是POJO的属性,而不是数据库字段
* 2.模糊匹配要自己写%
*/
@Test
public void testCount() {
Example example = new Example(TkUserPojo.class);
example.createCriteria().andLike("name", "bravo");
int i = userMapper.selectCountByExample(example);
System.out.println("count记录数为:" + i);
}
/**
* SELECT id , name FROM tk_user WHERE ( ( age = ? ) )
*
* 虽然通用Mapper也能只查询指定列,但是复用性不好,每次都要在service层重新写一遍
*/
@Test
public void testSelectByExample() {
Example example = new Example(TkUserPojo.class);
// 指定查询列只查询id和name,指定条件列为age
example.selectProperties("id", "name").createCriteria().andEqualTo("age", 16);
userMapper.selectByExample(example);
}
/**
* SELECT id,name,age,create_time,update_time,deleted
* FROM tk_user
* WHERE id in (1,2,3,4,5);
*
* selectByIdList底层也是用IN
*/
@Test
public void testSelectBatch() {
List<Long> ids = Arrays.asList(1L, 2L, 3L, 4L, 5L);
userMapper.selectByIdList(ids);
}
}
注意点一
- 通用Mapper的方法会自动转换驼峰,但手写的SQL需要单独开启才能转换
application.yml
server:
port: 5678
spring:
datasource:
url: jdbc:mysql:///test?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
# 打印SQL
logging:
level:
com.bravo.happy.dao: debug
mybatis:
# 即使引入通用Mapper,还是可以使用MyBatis原生的方式手写SQL,指定XML扫描位置即可
mapper-locations: classpath:mapper/**/*.xml
# 通用Mapper的方法会自动转换驼峰,但手写的SQL需要开启才能转换
configuration:
map-underscore-to-camel-case: on
注意点二
需要在启动器类加上@MapperScan
/**
* 注意,这里的@MapperScan是tk包下的,而不是org,扫码mapper接口所在路径
* @author qiyu
*/
@MapperScan("com.bravo.happy.dao")
@SpringBootApplication
public class HappyDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HappyDemoApplication.class, args);
}
}
默认情况下,如果没有指定resources,目前认为自动会将src/main/resources下的静态文件放到target里头的classes文件夹下的package下的文件夹里。
但是如果你在pom.xml中配置了resource,那么就不会将src/main/resources下的静态文件放到target里
所以一旦我们手动指定了resource的路径,则还需要把src/main/resources这个路径也指定进去
<!--作用是将:配置目录下的的要求后缀名文件拷贝到target内-->
<resources>
<!--如果不指定这个路径,则默认会把src/main/resources当成静态资源,一旦指定后,默认路径失效,所以如果src/main/resources下有静态资源,则需连同配置-->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
注意点三
- insertList(list)方法底层是insert()而不是insertSelective(),数据库默认值不会生效。
- deleteByIdList底层用的是IN,是物理删除DELETE,不是逻辑删除UPDATE
/**
* INSERT INTO tk_user ( id,name,age,create_time,update_time,deleted )
* VALUES
* ( null,'bravo1',18,null,null,null ) ,
* ( null,'bravo2',19,null,null,null ) ,
* ( null,'bravo3',20,null,null,null ) ,
* ( null,'bravo4',21,null,null,null )
* ...
*
* insertList(list)方法底层是insert()而不是insertSelective(),数据库默认值不会生效
*/
@Test
public void testInsertBatch() {
// 准备10个数据
List<TkUserPojo> list = new ArrayList<>();
TkUserPojo tkUserPojo = null;
for (int i = 1; i <= 10; i++) {
tkUserPojo = new TkUserPojo();
tkUserPojo.setName("bravo" + i);
tkUserPojo.setAge(17 + i);
list.add(tkUserPojo);
}
// 批量插入
userMapper.insertList(list);
}
/**
* DELETE
* FROM tk_user
* WHERE id in (1,2,3,4,5);
*
* deleteByIdList底层用的是IN,是物理删除DELETE,不是逻辑删除UPDATE
*/
@Test
public void testDeleteBatch() {
List<Long> ids = Arrays.asList(1L, 2L, 3L, 4L, 5L);
userMapper.deleteByIdList(ids);
}
虽然,批量操作会比循环单个操作速度快,因为批量插入时是1w条SQL是一起发送给MySQL服务端执行的,而1w次循环插入则是发送了1w次SQL。1次网络连接和1w次网络连接,差距还是很大的。假设一次网络IO耗时1秒,那么一万次请求往返就会徒增1w秒耗时,这是不能接受的。
但批量操作没办法按条件,所以推荐:
通用Mapper提供的接口方法适合简单的单表增删改,对于查询和批量操作最好自己手写SQL。更何况实际编程批量操作的场景不会特别多,写几个SQL不会很耗费精力。
注意点四
没办法按条件批量操作(只能根据where id in(,)),所以需要自己手写sql,如果要求根据name批量删除记录,你会怎么写SQL?
<!--批量删除(逻辑删除)-->
<update id="deleteBatch">
<foreach collection="list" item="user" separator=";">
UPDATE tk_user set deleted = 1
<where>
<if test="user.name != null and user.name.trim() != ''">
and name = #{user.name}
</if>
and deleted = 0
</where>
</foreach>
</update>
大部分人会这样写,你可能以为我是说上面的SQL语法有问题,其实上面的SQL是可以执行的,但逻辑不严谨,有可能引发致命BUG。
想像一下,如果user.name == null,上面的SQL最终会变成:
UPDATE tk_user set deleted = 1 WHERE deleted = 0
也就是删除所有数据!!!
不仅仅是我们手写的SQL,通用Mapper提供的方法一不小心也可能发生全表操作。比如:
@Test
public void testUpdate() {
String name = null;
// 设置了条件查询,但是name却为null
Example example = new Example(TkUserPojo.class);
example.createCriteria().andEqualTo("name", name);
TkUserPojo tkUserPojo = new TkUserPojo();
tkUserPojo.setAge(99);
userMapper.updateByExampleSelective(tkUserPojo, example);
}
打印的SQL会进行全表更新:UPDATE tk_user SET age = 99;(where name=null没有了)
包括默认接口提供的一些批量操作,会在list.size()==0时发生全表修改:
@Test
public void testDelete() {
// 传入空的List
userMapper.deleteByIdList(new ArrayList<>());
}
DELETE FROM tk_user;
【where id in (null)】
所以,不论是手写SQL还是使用通用Mapper等框架,一定要经常注意如果条件失效时是否会造成全表范围的修改,最好是加一些必要的判断:
● 集合是否为空,比如deleteByIdList(new ArrayList())删除全表
● 条件字段是否全部为空
手写sql优点
有些公司之所以引入通用Mapper、MyBatis-Plus的同时却强制查询要手写SQL
- 方便优化。将sql与代码分离,便于集中优化
- 精准控制查询的列数,手写SQL便于精确控制查询的列(只查你需要的,不查多余的)
通用Mapper其实能控制查询的列,要通过Example对象:
/**
* SELECT id , name FROM tk_user WHERE ( ( age = ? ) )
*
* 虽然通用Mapper也能只查询指定列,但是复用性不好,每次都要在service层重新写一遍
*/
@Test
public void testSelectByExample() {
Example example = new Example(TkUserPojo.class);
// 指定查询列只查询id和name,指定条件列为age
example.selectProperties("id", "name").createCriteria().andEqualTo("age", 16);
userMapper.selectByExample(example);
}
但其他诸如selectByPrimaryKey()方法是全列查询,因为mapper对象无法直接设置selectProperties()。