背景
1.Mybatis generator为mybatis生成了大量并没有实际使用的代码,实际发现只有 selectByExample,insertSelective 两个方法使用较多,而且为每个实体类生成一个 *Example 类。 *Example 这个类是大量相似的代码,完全可以抽象统一起来
2.还有生成不同风格的 DynamicSql 风格的mapper 层代码,混乱可读性差。
框架选型
jpa
jpa只需要简单生成entity,mapper 层继承 org.springframework.data.jpa.repository.JpaRepository
接口,只需要写方法名,就有了单表操作的能力,比起Mybatis generator 少了大量的不必要代码。
entity
@Data
@Entity
public class OrderRefund {
/**
* 主键ID
*/
@Id
private Long id;
//省略其他代码
...
}
单表操作
方法名字只需要遵守命名规范,就可以完成单表操作。以下相当于select * from order_refund where order_refund_no =?
public interface OrderRefundMapper extends JpaRepository<OrderRefund, Long>{
List<OrderRefund> findByOrderRefundNo(String orderRefundNo);
}
分页
只需要在参数重加上 Pageable 返回值使用 Page 就可完成分页,注意 jpa中的page 是从0开始,而不是1。
public interface OrderRefundMapper extends JpaRepository<OrderRefund, Long>{
Page<OrderRefund> findByOrderRefundNoIn(Collection<String> orderRefundNo, Pageable pageable);
}
自定义sql和返回对象
JPQL查询
使用自定义对象来接收结果,支持JPQL查询,但是不支持原生sql
对象定义如下
@Data
@AllArgsConstructor
public class CustomizeResult {
private String supplierRefundNo;
private String supplierName;
}
JPQL查询
public interface OrderRefundMapper extends JpaRepository<OrderRefund, Long> {
//自定义sql,及返回结果
@Query("select new com.example.jpademo.entity.CustomizeResult(orderRefundNo,supplierName) " +
"from OrderRefund where orderRefundNo = :orderRefundNo")
List<CustomizeResult> customizeQuery(@Param("orderRefundNo") String orderRefundNo);
}
原生sql
使用原生sql 必须定义接口来收集结果或者使用List<Object[]>
public interface CustomizeResultInterface {
String getSupplierRefundNo();
String getSupplierName();
default String toStringInfo() {
return "name=" + getSupplierName() + "; num=" + getSupplierRefundNo();
}
}
//原生sql
@Query(value = "select order_refund_no as supplierRefundNo ,supplier_name as supplierName" +
" from order_refund where order_refund_no=:orderRefundNo",
nativeQuery = true)
List<CustomizeResultInterface> customizeSqlQuery(@Param("orderRefundNo") String orderRefundNo);
分页
//原生sql
@Query(value = "select order_refund_no as supplierRefundNo ,supplier_name as supplierName" +
" from order_refund where order_refund_no=:orderRefundNo",
countQuery = "select count (1) supplierName +\n" +
" from order_refund where order_refund_no=:orderRefundNo",
nativeQuery = true)
Page<CustomizeResultInterface> customizeSqlQuery(@Param("orderRefundNo") String orderRefundNo,Pageable pageable);
动态sql
OutInstruct outInstruct = new OutInstruct();
outInstruct.setOutInstructNo("admin");
ExampleMatcher exampleMatcher = ExampleMatcher.matching()
.withMatcher("outInstructNo", ExampleMatcher.GenericPropertyMatcher::startsWith);
Example<OutInstruct> example = Example.of(outInstruct, exampleMatcher);
outInstructMapper.findOne(example);
Jpa 中 Example 中的功能有限,不支持非字符串的范围查询,只支持等值查询。如果需要可以采用下面的方案
多继承一个接口
public interface OrderRefundMapper extends JpaRepository<OrderRefund, Long> ,JpaSpecificationExecutor<OrderRefund>
使用 根据条件动态拼接sql
orderRefundMapper.findAll((Specification<OrderRefund>) (root, query, criteriaBuilder) -> {
Predicate predicate1 = criteriaBuilder.equal(root.get("orderRefundNo"), "ORD3298534907620");
Predicate predicate2 = criteriaBuilder.equal(root.get("outInstructNo"), "ORD3298534907620");
return criteriaBuilder.and(predicate1, predicate2);
});
连表查询
举个 onetoMany的栗子:
定义指令单实体类:
@Data
@Entity
@Table(name = "out_instruct_10")
public class OutInstruct implements Serializable {
/**
* 自增主键
*/
@Id
private Long id;
@Column(name = "out_instruct_no")
private String outInstructNo;
//省略其他代码
...
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "out_instruct_no",referencedColumnName = "out_instruct_no")
List<OutInstructDt> dtList;
}
定义出库指令单详情实体:
@Data
@Entity
@Table(name = "out_instruct_dt_10")
public class OutInstructDt {
/**
* 主键自增
*/
@Id
private Long id;
@Column(name = "out_instruct_no")
private String outInstructNo;
//省略其他代码
...
}
@JoinColumn中name 字段为详情表的外键字段(不必真的在数据库建立外键,逻辑外键即可),referencedColumnName 字段为主表的 主键(同样不必真的是数据库的主键,逻辑主键即可)
@OneToMany(fetch = FetchType.LAZY) 这里定义为懒加载,JOIN FETCH中的fetch,是可以在单条select语句中,初始化对象中的关联或集合。
如示例中OutInstruct
的dtList
成员,它是lazy成员,默认情况下是不会被初始化的,也就是说如果通过getDtList()
访问成员的时候,就会报LazyInitializationException
的异常。
例如以下代码会抛出LazyInitializationException
的异常:
OutInstruct outInstruct = new OutInstruct();
outInstruct.setOutInstructNo("111");
ExampleMatcher exampleMatcher = ExampleMatcher.matching()
.withMatcher("outInstructNo", ExampleMatcher.GenericPropertyMatcher::startsWith);
Example<OutInstruct> example = Example.of(outInstruct, exampleMatcher);
Optional<OutInstruct> outInstruct1=outInstructMapper.findOne(example);
outInstruct1.ifPresent(x -> System.out.println(x.getDtList()));
如果想有连表查详情的能力,比较简单的方式有以下两种:
第一:
在方法上加上事物的注解:如果在同一个事务上下文内,是可以获取到lazy成员的,但在长事务或者多线程的场景下,这种方法就不合适。
第二:
使用JpaSpecificationExecutor api手动fetch相关懒加载成员:
Optional<OutInstruct> outInstruct1 = outInstructMapper.findOne((Specification<OutInstruct>) (root, query, criteriaBuilder) -> {
root.fetch("dtList");
return criteriaBuilder.equal(root.get("outInstructNo"), "1111");
});
outInstruct1.ifPresent(x -> System.out.println(x.getDtList()));
mybatis+mybatiscodehelper 插件
http://118.24.53.162/#/methodNameToSql
通过方法名字 生成sql,其规范和jpa的一样。比起Mybatis generator 少了大量的不必要代码。缺点就是每一种查询条件都需要写一个sql,没有动态拼接sql 的能力,可能会有大量的单表操作的方法。
分页功能可以基于mybatis的分页插件。
mybatis plus
Mybatis 本身就是自定义sql 的这里主要就说一下 mybatis plus 对单表操作及动态sql 的封装,其中的com.baomidou.mybatisplus.core.mapper.BaseMapper
com.baomidou.mybatisplus.extension.service.IService
这两个接口提供了大量单表的curd,基本能满足所有的单表操作要求。
动态sql
QueryWrapper,UpdateWrapper提供了动态select ,update的功能。
QueryWrapper<OutInstruct> queryWrapper=new QueryWrapper<>();
queryWrapper.lambda().eq(OutInstruct::getBatchNo,"12");
outInstructMapper.selectCount(queryWrapper);
UpdateWrapper<OutInstruct> outInstructUpdateWrapper=new UpdateWrapper<>();
outInstructUpdateWrapper.lambda().set(OutInstruct::getBatchNo,"123");
outInstructMapper.update(null,outInstructUpdateWrapper);
分页
定义插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
在参数列表中加入IPage 就可完成分页
@Select("select * from out_instruct where id > #{identity}")
IPage<OutInstruct> pageSql(IPage<OutInstruct> page, @Param("identity") Integer id);
对比
接入成本
- Jpa:换了持久层框架,会修改所有持久层代码,接入成本比较大
- mybatiscodehelper 插件:和原来一样只是安装一个插件,基本没有接入成本
- mybatis plus :可以和原有的mybatis兼容,基本没有接入成本
学习成本
- Jpa:换了持久层框架,如果之前没有用过,有一定的学习成本。
- mybatiscodehelper 插件:原生mybatis,没有学习成本。
- mybatis plus:和原来的mybatis差不多,基本没有学习成本。
易用性
- Jpa:单表操作只需要按照规范写方法名,支持动态sql,分页。
- mybatiscodehelper 插件:单表操作只需要按照规范写方法名,不支持动态sql。
- mybatis plus:提供了丰富的单表操作方法, 支持动态sql。
性能
单条数据的操作逻辑都是一样的,只有批量数据的操作逻辑不太一样,这里以批量插入为例,比较一下不同框架的性能
建立测试表
-- auto-generated definition
create table test_table
(
id int auto_increment
primary key,
uuid varchar(64) null,
create_time timestamp null,
data varchar(64) null
);
mybatis plus | jpa | 自定义sql | |
---|---|---|---|
10000条 | 时间太久不想等 | 时间太久不想等 | 9s |
5000条 | 406 s | 时间太久不想等 | 7s |
2000条 | 148 s | 252 s | 6s |
why?
mybatis plus慢在哪里
查看源码 mybatis plus 中的批量插入 使用的是批量模式
sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
把SQL语句发个数据库,数据库预编译好,数据库等待需要运行的参数,接收到参数后一次运行,ExecutorType.BATCH只打印一次SQL语句,多次设置参数步骤。
mybatis plus 源码简单来看相当于以下代码:
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);) {
TestTableMapper testTableMapper = sqlSession.getMapper(TestTableMapper.class);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
list.forEach(testTableMapper::insert);
sqlSession.flushStatements();
sqlSession.commit();
stopWatch.stop();
System.out.println(stopWatch.shortSummary());
}
sql日志中 也证实只发了一句sql 多次发送参数的过程
翻译成原始的jdbc就是这样:
String sql="INSERT INTO test_table ( uuid, create_time, data ) VALUES ( ?, ?, ? )";
try (Connection connection = dataSource.getConnection()) {
PreparedStatement statement= connection.prepareStatement(sql);
list.forEach(x->{
try {
statement.setTimestamp(2, Timestamp.valueOf(x.getCreateTime()));
statement.setString(1,x.getUuid());
statement.setString(3,x.getData());
statement.addBatch();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
});
statement.executeBatch();
}
jpa 慢在哪里
查看jpa 中的源码
发现循环调用save 方法,真的过分,慢的合情合理
@Transactional
@Override
public <S extends T> List<S> saveAll(Iterable<S> entities) {
Assert.notNull(entities, "Entities must not be null!");
List<S> result = new ArrayList<S>();
for (S entity : entities) {
result.add(save(entity));
}
return result;
}
自定义sql快在哪里
自定义sql insert()values (),(),() 只执行一句sql 快的飞起,但是有一点需要注意, sql 的长度是有限制的,mysql默认接受sql的大小是1048576(1M)
结论
jpa,mybatis-plus,虽然很方便,提供了很多但表操作的api,以及动态sql的支持,但是要小心使用他们的批量操作的功能,性能上比起自定义sql差很多。