提到Java-web,我们首先想到的连接数据库,对数据库表做一些增删改查的业务。常见的数据库操作方式主要有4种,一种是JDBC操作数据库,一种是SpringBoot官方推荐的JPA方式操作数据库,一种是mybatis方式操作数据库,还有一种是mybatis-plus的操作方式。
我们常常会听到注入攻击这种手段。所谓注入攻击,就是一些不法分子,通过分析服务器接口,在前端请求参数中加入sql相关的语句,使后台程序运行时,产生违背设计初衷的结果。
举个简单的栗子,比如一个用户所在部门节点为aa,级联字段为-A-b-aa。程序接口希望查询部门下,年龄大于35的的程序员。前端参数有2个字段,一个是major,一个是age。部门级联字段放在用户的token中,无法更改。正常情况下语句如下:
select * from auth_staff where dep_cast_id like "%-A-b-aa" and age > 35 and major = "cs"
但如果某同学使用postman,把major字段改为 1" or “1”="1,在不做特殊处理时,生成的sql会是这样
select * from auth_staff where dep_cast_id like "%-A-b-aa" and age > 35 and major = "1" or "1"="1"
画面突然滑稽了起来,公司所有年龄大于35岁的程序员就都被查到了。
我们在开发时,程序难免发生各种异常,比如空指针什么的。但如果我们写的接口是一个插入数据或修改数据的功能,且一个接口中有多条插入或修改的语句。只有所有的语句都被执行时,业务才是正确的;否则数据将会错乱,需要人工修改。这时,如果中间某句话发生了空指针,偶买噶,可以删库跑路了~~
幸好,Spring框架,给我们提供了简单的处理方法,在Service上加上这段代码,当出现错误时程序将会回滚,也就很难发生数据库错乱的问题了:
@Transactional(rollbackFor = Exception.class)
@Override
public void someFun() {
//巴拉巴拉
}
在开发中,我们并不希望代码中大量出现sql语句。一方面,这样做使应用与数据库的耦合度太高,如果想换一个数据库,就需要整篇改动代码。另一方面,手动编写sql,也很容易出现错误。然而,最重要的是,大多数业务只需要编写个增删改查的接口,这些sql都是重复的机械劳动,真的好烦的。
一个好的ORM,应该可以让开发者大多数时候不必亲手编写sql,把精力聚焦在具体业务逻辑的开发上来。
在实际开发中,数据库表中的数据,通常不仅仅存业务相关的数据,还要存放一些审计数据,以给予系统最基本的追踪功能。常见的审计字段包括,什么时候创建的,创建者的id,创建者的名字,创建者的ip,什么时候修改的,修改者的id,修改者的名字,修改者的ip。
通常情况下,在删除时,尤其是主表数据,通常不希望真的删除。会在表中增加一个del字段,为1则是删除,为0则是正常。在查询时,要过滤掉del为1的数据。
web应用,常常会遇到这种问题。当两个人同时操作1条数据时,就会发生相互覆盖的问题。举个简单的例子:
某个名叫小可爱的数据实体,被戳一下能量就会+1,当能量达到5时就会释放洪荒之力。我们代码可能会这样写:
public void touch(Long pId){
XiaoKeAi xiaokeai = mXiaoKeAiService.getById(pId);
Long playerId = TokenUtil.getTokenObject().getId();
int energy = XiaoKeAi.setEnegergy();
if(energy + 1 > 5){
sendMsg(MsgType.LAUNCH_HONG_HUANG,xiaokeai,playerId);
energy = 0;
}else{
// 本过程十分耗时
handleChuoChuo(xiaokeai,playerId);
energy = energy + 1;
}
XiaoKeAi.setEnegergy(energy);
mXiaoKeAiService.save(energy);
}
大家试想,如果小可爱的能量在4时,有两个玩家在很近的时间一起发起戳戳的请求。emm,小可爱的洪荒之力就要连续爆发2遍,真是可怜。如果需求再改改,除了戳戳,小可爱还唱歌,每唱一次歌就会降低一点能量。如果两个玩家同时分别发起戳戳和点歌的需求,又会发生什么呢?
为了解决这种问题,我们会在小可爱身上加一个version字段,每进行一次操作,version字段就+1。在进行更新时,添加where version = current_version。这样,让后来的玩家请求失败,至少避免了两者相互覆盖数据错乱的悲剧。
这种乐观锁,通常用于对同一实体的并发概率不是很高的情况下。如果同一实体的操作频繁并发,比如投票系统,使用Redison这一类的可重入悲观锁更好些。
本篇的目的是讲解SpringBoot下常见的4重ORM的用法,然后做对比。为方便起见,本小节给出数据库操作的样例Entity
嗯,可能有点长,因为把4种ORM用到的注解都放上了。。。
@EntityListeners(AuditingEntityListener.class)
@Entity //表示这个类是一个实体类
@Table(name = "c2_orm_test")
@TableName("c2_orm_test")
@Data
public class OrmTestEntity {
@Column(name = "id")
@GeneratedValue(generator = "orderIdGenerator", strategy = GenerationType.SEQUENCE)
@GenericGenerator(name = "orderIdGenerator", strategy = "indi.zhifa.recipe.bailan.busy.handler.SnowIdGenerator")
@Id
@Schema(title = "主键")
@TableId(type = IdType.ASSIGN_ID)
Long id;
String appCode;
String appName;
String moduleCode;
String moduleName;
String code;
String name;
EOrmEntityStatus status;
Integer value;
String description;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.INSERT)
@Schema(title = "创建时间")
@CreatedDate
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@QueryType(PropertyType.NONE)
LocalDateTime createTime;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.INSERT)
@Schema(title = "创建用户Id")
@QueryType(PropertyType.NONE)
Long createBy;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.INSERT)
@Schema(title = "创建用户昵称")
@QueryType(PropertyType.NONE)
String createName;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.INSERT)
@Schema(title = "创建者Ip")
@QueryType(PropertyType.NONE)
String createIp;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.UPDATE)
@Schema(title = "修改时间")
@LastModifiedDate
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@QueryType(PropertyType.NONE)
LocalDateTime modifyTime;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.UPDATE)
@Schema(title = "修改用户Id")
@QueryType(PropertyType.NONE)
Long modifyBy;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.UPDATE)
@Schema(title = "修改用户昵称")
@QueryType(PropertyType.NONE)
String modifyName;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.UPDATE)
@Schema(title = "创建者Ip")
@QueryType(PropertyType.NONE)
String modifyIp;
@Schema(title = "版本")
@com.baomidou.mybatisplus.annotation.Version
@Version
Integer version;
@JSONField(serialize = false, deserialize = false)
@TableLogic
@Schema(title = "是否删除")
Boolean del;
public void initWithCreate(){
id = SnowflakeIdWorker.generateId();
createTime = LocalDateTime.now();
createBy = 0L;
createName = "sys";
createIp = "localhost";
modifyTime = null;
modifyBy = null;
modifyName = null;
modifyIp = null;
version = 0;
del = false;
}
public Object[] toParamList(){
return Arrays.asList(
id, appCode, appName, moduleCode, moduleName, code, name, status.getCode(), description, value,
createTime, createBy, createName, createIp, modifyTime, modifyBy, modifyName, modifyIp, version, del?1:0).toArray();
}
}
这里的枚举类:
@RequiredArgsConstructor
public enum EOrmEntityStatus {
DEFAULT(0,"DEFAULT"),
STATUS_1(1,"STATUS1"),
STATUS_2(2,"STATUS2"),
STATUS_3(3,"STATUS3"),
STATUS_4(4,"STATUS4"),
STATUS_5(5,"STATUS5")
;
@EnumValue
@Getter
final Integer code;
@Getter
final String name;
}
由于篇幅限制,在这里我们对于每种ORM只做两件事,一件是生成一批数据,插入数据库。一个是分页查询。
controller接口类如以下代码:
@Api(tags = "ORM测试")
@RequestMapping("/api/ormTest")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class OrmTestApi {
private final IOrmJDBCService mOrmService;
private final IOrmMybatisService mOrmMybatisService;
private final IOrmJpaService mOrmJpaService;
private final IOrmMybatisPlusService mOrmMybatisPlusService;
@Operation(summary = "JDBC创建测试数据")
@GetMapping("/jdbc/init")
public String jdbcInit(){
mOrmService.initTestData();
return "初始化数据成功";
}
@Operation(summary = "JDBC分页查询")
@GetMapping("/jdbc/page")
public PageData<OrmTestEntity> jdbcPage(
@Parameter(description = "当前页") @RequestParam(name = "current") int pCurrent,
@Parameter(description = "页大小") @RequestParam(name = "size") int pSize,
@Parameter(description = "应用码") @RequestParam(name = "appCode",required = false) String pAppCode,
@Parameter(description = "模块码") @RequestParam(name = "moduleCode",required = false) String pModuleCode,
@Parameter(description = "主码") @RequestParam(name = "code",required = false) String pCode,
@Parameter(description = "名字") @RequestParam(name = "name",required = false) String pName,
@Parameter(description = "状态") @RequestParam(name = "status",required = false) EOrmEntityStatus pStatus,
@Parameter(description = "最小值") @RequestParam(name = "valueMin",required = false) Integer pMin,
@Parameter(description = "最大值") @RequestParam(name = "valueMax",required = false) Integer pMax) {
PageData<OrmTestEntity> pageData = mOrmService.page(pCurrent,pSize,pAppCode,pModuleCode,pCode,pName,pStatus,pMin,pMax);
return pageData;
}
@Operation(summary = "mybatis创建测试数据")
@GetMapping("/mybatis/init")
public String mybatisInit(){
mOrmMybatisService.initTestData();
return "初始化数据成功";
}
@Operation(summary = "mybatis分页查询")
@GetMapping("/mybatis/page")
public PageData<OrmTestEntity> mybatisPage(
@Parameter(description = "当前页") @RequestParam(name = "current") int pCurrent,
@Parameter(description = "页大小") @RequestParam(name = "size") int pSize,
@Parameter(description = "应用码") @RequestParam(name = "appCode",required = false) String pAppCode,
@Parameter(description = "模块码") @RequestParam(name = "moduleCode",required = false) String pModuleCode,
@Parameter(description = "主码") @RequestParam(name = "code",required = false) String pCode,
@Parameter(description = "名字") @RequestParam(name = "name",required = false) String pName,
@Parameter(description = "状态") @RequestParam(name = "status",required = false) EOrmEntityStatus pStatus,
@Parameter(description = "最小值") @RequestParam(name = "valueMin",required = false) Integer pMin,
@Parameter(description = "最大值") @RequestParam(name = "valueMax",required = false) Integer pMax) {
PageData<OrmTestEntity> pageData = mOrmMybatisService.page(pCurrent,pSize,pAppCode,pModuleCode,pCode,pName,pStatus,pMin,pMax);
return pageData;
}
@Operation(summary = "JPA创建测试数据")
@GetMapping("/jpa/init")
public String jpaInit(){
mOrmJpaService.initTestData();
return "初始化数据成功";
}
@JpaQsdl
@Operation(summary = "jpa分页查询")
@GetMapping("/jpa/page")
public Page<OrmTestEntity> jpaPage(
@QuerydslPredicate(root = OrmTestEntity.class) Predicate predicate, Pageable page) {
Page<OrmTestEntity> pageData = mOrmJpaService.page(page,predicate);
return pageData;
}
@Operation(summary = "mybatis-plu创建测试数据")
@GetMapping("/mp/init")
public String mpInit(){
mOrmMybatisPlusService.initTestData();
return "初始化数据成功";
}
@JpaQsdl
@Operation(summary = "mybatis-plu分页查询")
@GetMapping("/mp/page")
public com.baomidou.mybatisplus.extension.plugins.pagination.Page<OrmTestEntity> mpPage(
@Parameter(description = "当前页") @RequestParam(name = "current") int pCurrent,
@Parameter(description = "页大小") @RequestParam(name = "size") int pSize,
@Parameter(description = "应用码") @RequestParam(name = "appCode",required = false) String pAppCode,
@Parameter(description = "模块码") @RequestParam(name = "moduleCode",required = false) String pModuleCode,
@Parameter(description = "主码") @RequestParam(name = "code",required = false) String pCode,
@Parameter(description = "名字") @RequestParam(name = "name",required = false) String pName,
@Parameter(description = "状态") @RequestParam(name = "status",required = false) EOrmEntityStatus pStatus,
@Parameter(description = "最小值") @RequestParam(name = "valueMin",required = false) Integer pMin,
@Parameter(description = "最大值") @RequestParam(name = "valueMax",required = false) Integer pMax) {
com.baomidou.mybatisplus.extension.plugins.pagination.Page<OrmTestEntity> pageData = mOrmMybatisPlusService.page(pCurrent,pSize,pAppCode,pModuleCode,pCode,pName,pStatus,pMin,pMax);
return pageData;
}
}
由于各Service都需要生成数据,故把这部分代码提取出来。
public class BaseOrmServiceImpl {
protected List<OrmTestEntity> getInitOrmTestEntityList(String pPrefix) {
final int appCnt = 3;
final int[] moduleCnts = new int[]{2,3,5};
final int statusCnt = EOrmEntityStatus.values().length - 1;
final int[] entityCnts = new int[]{8,3,5,2,1};
final int valueMin = 1;
final int valueMax = 100;
int appNo = 0;
int moduleNo = 0;
int entityNo = 0;
List<OrmTestEntity> ormTestEntityList = new ArrayList<>();
for(int appIdx=0;appIdx<appCnt;appIdx++){
int moduleCnt = moduleCnts[appIdx];
appNo++;
String appCode = pPrefix+"_app_"+(appIdx+1);
String appName = pPrefix+"-应用-"+appNo;
for (int moduleIdx=0;moduleIdx<moduleCnt;moduleIdx++){
moduleNo++;
String moduleCode = pPrefix+"_mode_"+(moduleIdx+1);
String moduleName = pPrefix+"-模块-"+moduleNo;
for(int statusIdx = 0;statusIdx<statusCnt;statusIdx++){
int entityCnt = entityCnts[statusIdx];
for(int i=0;i<entityCnt;i++){
entityNo++;
String entityCode = pPrefix+"_entity_"+entityNo;
String entityName = pPrefix+"-ORM测试实体-"+entityNo;
String description = "这个是"+entityName+"。是"+appName+"下的"+moduleName+"的一个应用。由"+pPrefix+"创建";
int status = statusIdx+1;
int value = RandomUtil.randomInt(valueMin,valueMax);
OrmTestEntity ormTestEntity = new OrmTestEntity();
ormTestEntity.initWithCreate();
ormTestEntity.setAppCode(appCode);
ormTestEntity.setAppName(appName);
ormTestEntity.setModuleCode(moduleCode);
ormTestEntity.setModuleName(moduleName);
ormTestEntity.setCode(entityCode);
ormTestEntity.setName(entityName);
ormTestEntity.setDescription(description);
ormTestEntity.setStatus(EOrmEntityStatus.values()[statusIdx+1]);
ormTestEntity.setValue(value);
ormTestEntityList.add(ormTestEntity);
}
}
}
}
return ormTestEntityList;
}
}
在这一节,我们介绍JDBC的实现。所谓JDBC,其实就是帮助程序员执行sql的一套ORM框架。
JDBC可以通过sql参数化,解决1.2.1中所讲的防注入问题。
这一部分没有太多好讲的,小编打算直接放出代码供大家参考比对。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
@RequiredArgsConstructor
@Service
public class OrmJDBCServiceImpl extends BaseOrmServiceImpl implements IOrmJDBCService {
private final JdbcTemplate mJdbcTemplate;
enum EParamType{
EQ,
LIKE,
BIG,
LESS,
BETWEEN;
}
@Transactional(rollbackFor = Exception.class)
@Override
public void initTestData() {
List<OrmTestEntity> ormTestEntityList = getInitOrmTestEntityList("JDBC");
String sql = "insert into c2_orm_test("+
"id,app_code,app_name,module_code,module_name,code,name,status,description,value,"+
"create_time,create_by,create_name,create_ip,modify_time,modify_by,modify_name,modify_ip,version,del) values(?,?,?,?,?,?,?,?,?,?," +
"?,?,?,?,?,?,?,?,?,?)";
List<Object[]> params = ormTestEntityList.stream().map(entity->entity.toParamList()).collect(Collectors.toList());
mJdbcTemplate.batchUpdate(sql,params);
}
@Override
public PageData<OrmTestEntity> page(int pCurrent, int pSize,
String pAppCode, String pModuleCode, String pCode, String pName,
EOrmEntityStatus pStatus, Integer pMin, Integer pMax) {
StringBuilder sb = new StringBuilder();
sb.append("select * from c2_orm_test");
StringBuilder whereSb = new StringBuilder().append(" ");
List<Object> params = new ArrayList<>();
Boolean hasParam = false;
addParam(whereSb,hasParam,params,"del",EParamType.EQ,0,null);
addParam(whereSb,hasParam,params,"app_code",EParamType.EQ,pAppCode,null);
addParam(whereSb,hasParam,params,"module_code",EParamType.EQ,pModuleCode,null);
addParam(whereSb,hasParam,params,"code",EParamType.EQ,pCode,null);
addParam(whereSb,hasParam,params,"name",EParamType.LIKE,pName,null);
addParam(whereSb,hasParam,params,"status",EParamType.EQ,null !=pStatus ? pStatus.getCode(): null,null);
if(null != pMin && null != pMax){
addParam(whereSb,hasParam,params,"value",EParamType.BETWEEN,pMin,pMax);
}else if(null != pMin){
addParam(whereSb,hasParam,params,"value",EParamType.BIG,pMin,null);
}else if(null != pMax){
addParam(whereSb,hasParam,params,"value",EParamType.LESS,pMax,null);
}
sb.append(whereSb);
PageData<OrmTestEntity> pageData = new PageData<OrmTestEntity>();
pageData.setCurrent(pCurrent);
pageData.setSize(pSize);
String cntSql = String.format("select count(*) as cnt from (%s) as tmp", sb);
Map<String,Object> cntRes = mJdbcTemplate.queryForMap(cntSql,params.toArray());
int cnt = ((Long)cntRes.get("cnt")).intValue();
if(cnt > 0){
if(pSize > 0){
sb.append(" LIMIT ").append((pCurrent-1) * pSize).append(", ").append(pSize);
}
List<Map<String, Object>> mapData = mJdbcTemplate.queryForList(sb.toString(),params.toArray());
List<OrmTestEntity> data = mapData.stream().map(theMapData->
BeanUtil.mapToBean(theMapData,OrmTestEntity.class,true, CopyOptions.create())
).collect(Collectors.toList());
pageData.setTotal(cnt);
int pages = (int)Math.ceil((double)cnt/pSize);
pageData.setPages(pages);
pageData.setPageData(data);
}else{
pageData.setTotal(0);
pageData.setPages(0);
}
return pageData;
}
private void addParam(StringBuilder pParamSb, Boolean pHasParam, List<Object> pParams, String pParamStr, EParamType pParamType, Object pParam1, Object pParam2){
if(null != pParam1){
if(pHasParam){
pParamSb.append(" and ");
}else{
pParamSb.append("where ");
}
pHasParam = true;
switch (pParamType){
case EQ:
pParamSb.append(pParamStr + " = ?");
pParams.add(pParam1);
break;
case LIKE:
pParamSb.append(pParamStr + " like ?");
pParams.add("%"+pParam1+"%");
break;
case BIG:
pParamSb.append(pParamStr + " > ?");
pParams.add(pParam1);
break;
case LESS:
pParamSb.append(pParamStr + " < ?");
pParams.add(pParam1);
break;
case BETWEEN:
pParamSb.append(pParamStr + " between ? and ?");
pParams.add(pParam1);
pParams.add(pParam2);
break;
}
}
}
}
我们从二可以看到,JDBC有个明显的弊端,就是sql的拼接都放到了代码里,这使得代码与数据库完全耦合。
另一方面,由于代码拼sql,使很多逻辑变得不易读,也就难以维护了。
所以,我们就会想,有没有办法可以动态生成sql呢,mybatis就呼之欲出了。
学习任何一个新的技术框架,我都推荐大家先去官网上看看。
本篇用到技术点有3个,Handling Enums;Result Maps;Dynamic SQL
由于小编的代码框架的ORM接入的mybatis-plus,所以引用mybatis-plus的就可以了
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
mybatis-plus:
typeEnumsPackage: indi.zhifa.recipe.bailan.busy.enums
mapper-locations: classpath*:mapping/**/*Mapper.xml
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="indi.zhifa.recipe.bailan.busy.dao.mapper.OrmTestMybatisMapper">
<resultMap id="OrmTestEntity" type="indi.zhifa.recipe.bailan.busy.entity.po.OrmTestEntity">
<id column="id" property="id"/>
<result property="appCode" column = "app_code"/>
<result property="appName" column = "app_name"/>
<result property="moduleCode" column = "module_code"/>
<result property="moduleName" column = "module_name"/>
<result property="code" column = "code"/>
<result property="name" column = "name"/>
<result property="status" column = "status" javaType="indi.zhifa.recipe.bailan.busy.enums.EOrmEntityStatus" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
<result property="value" column = "value"/>
<result property="description" column = "description"/>
<result property="createTime" column = "createTime"/>
<result property="createBy" column = "createBy"/>
<result property="createName" column = "createName"/>
<result property="createIp" column = "createIp"/>
<result property="modifyTime" column = "modifyTime"/>
<result property="modifyBy" column = "modifyBy"/>
<result property="modifyName" column = "modifyName"/>
<result property="modifyIp" column = "modifyIp"/>
<result property="version" column = "version"/>
<result property="del" column = "del"/>
resultMap>
<insert id="saveBatch" parameterType="java.util.List">
INSERT INTO c2_orm_test
(id,app_code,app_name,module_code,module_name,code,name,status,description,value,create_time,create_by,create_name,create_ip,modify_time,modify_by,modify_name,modify_ip,version,del)
VALUES
<foreach collection="pOrmTestEntityList" item="ormTestEntity" separator=",">
(#{ormTestEntity.id},#{ormTestEntity.appCode},#{ormTestEntity.appName},#{ormTestEntity.moduleCode},#{ormTestEntity.moduleName},#{ormTestEntity.code},#{ormTestEntity.name},#{ormTestEntity.status},#{ormTestEntity.description},#{ormTestEntity.value},#{ormTestEntity.createTime},#{ormTestEntity.createBy},#{ormTestEntity.createName},#{ormTestEntity.createIp},#{ormTestEntity.modifyTime},#{ormTestEntity.modifyBy},#{ormTestEntity.modifyName},#{ormTestEntity.modifyIp},#{ormTestEntity.version},#{ormTestEntity.del})
foreach>
insert>
<select id="page" resultMap="OrmTestEntity">
<bind name="name_like" value="'%' + _parameter.name + '%'" />
select * from c2_orm_test
<where>
del = 0
<if test="appCode != null">
and app_code = #{appCode}
if>
<if test="moduleCode != null">
and module_code = #{moduleCode}
if>
<if test="code != null">
and code = #{code}
if>
<if test="name != null">
and name like #{name_like}
if>
<if test="status != null">
and status = #{status}
if>
<if test="min != null and max != null">
and `value` between #{min} and #{max}
if>
<if test="min != null and max == null">
and `value` > #{min}
if>
<if test="min == null and max != null">
and `value` #{max}
if>
where>
select>
mapper>
看到mybatis的代码部分,是否有种豁然开朗的感觉,真的太干净了。
@Data
public class PageData<T> {
int total;
int pages;
int current;
int size;
List<T> pageData;
}
@RequiredArgsConstructor
@Service
public class OrmMybatisServiceImpl extends BaseOrmServiceImpl implements IOrmMybatisService {
private final OrmTestMybatisMapper mOrmTestMybatisMapper;
@Transactional(rollbackFor = Exception.class)
@Override
public void initTestData() {
List<OrmTestEntity> iniOrmTestEntityList = getInitOrmTestEntityList("mybatis");
mOrmTestMybatisMapper.saveBatch(iniOrmTestEntityList);
}
@Override
public PageData<OrmTestEntity> page(int pCurrent, int pSize,
String pAppCode, String pModuleCode, String pCode, String pName,
EOrmEntityStatus pStatus, Integer pMin, Integer pMax) {
List<OrmTestEntity> OrmTestEntityList = mOrmTestMybatisMapper.page(pCurrent,pSize,
pAppCode,pModuleCode,pCode,pName,
null !=pStatus ? pStatus.getCode():null,pMin,pMax);
PageData<OrmTestEntity> data = new PageData<>();
data.setCurrent(pCurrent);
data.setSize(pSize);
data.setPageData(OrmTestEntityList);
return data;
}
}
我们可以看到,mybatis虽然灵活,但对于一些简单的page查询,仍然需要配置大量的xml,拖慢了开发速度。
同时,使用mybatis就强制要求程序员数量掌握sql的语法,这样在招收新人时就增加了一些成本。
另外,手动配置xml的sql,也难免会发生错误,有时可能因为手滑写出一些难以发现调试的奇怪BUG,这也增加了开发成本。
于是,JPA就呼之欲出。试想,如果程序员只需要和产品对接,设计好数据库。用类似freemark的模板技术,把数据库对应的实体生成出来。实体字段和数据库字段以一定规则对应。一些简单的page,findByXxx,updateById,removeById等操作,不需要程序员做额外的配置,只简单的调用一下接口,岂不妙哉。
JPA正式这样一种ORM,程序员只需要在生成的Entity上加上一些注解,搞一个Reposity接口继承CrudRepository、QuerydslPredicateExecutor,QuerydslBinderCustomizer。在Service中注入一下,就可以简单使用了。
CrudRepository 是JPA的一个基础查询接口,他提供了诸如save、saveAll、findById、findAllById、deleteById等基础的接口实现。如果想要做其他查询,只需要在自己写的Repository中,按一定规则声明接口(不必自己写实现),就可以生效了。
举个例子,拿我们的1.2.8的样例Entity来说,如果想实现查找所有状态是EOrmEntityStatus.STATUS_2 并且value在10~20的所有实体,只需要这样就可以了:
public interface OrmTestReposity extends CrudRepository<OrmTestEntity,Long>, QuerydslPredicateExecutor<OrmTestEntity>, QuerydslBinderCustomizer<QOrmTestEntity> {
List<OrmTestEntity> findAllByStatusAndValueBetween(EOrmEntityStatus status, Integer min, Integer max);
}
}
具体语法规则,可以参见官网
有些时候,可能Repository的接口没什么共通性,在Service里只用1次,我们不想在Repository中定义,那该怎么做呢?
这时,QuerydslPredicateExecutor。在这里我不想做过多讲解,还是拿4.2的例子,在这里实现一遍。
public List<OrmTestEntity> listByStatusAndValueBetween(EOrmEntityStatus pOrmEntityStatus, Integer pMin, Integer pMax){
BooleanExpression b2 = QOrmTestEntity.ormTestEntity.value.between(pMin,pMax);
BooleanExpression b1 = QOrmTestEntity.ormTestEntity.status.eq(pOrmEntityStatus).and(b2);
List<OrmTestEntity> ormTestEntityList = Lists.newArrayList(mOrmTestReposity.findAll(b1));
return ormTestEntityList;
}
那么,对于page接口呢?理论上我们也可以用这种方式写。但又没有更省事的方法呢?答案是有的。
querydsl提供了一种自动绑定的功能,前端可以根据Entity的属性名自由的传递条件,Java部分代码如下:
Service:
@RequiredArgsConstructor
@Service
public class OrmJpaServiceImpl extends BaseOrmServiceImpl implements IOrmJpaService {
private final OrmTestReposity mOrmTestReposity;
@Override
public Page<OrmTestEntity> page(Pageable pPageable, Predicate predicate) {
Page<OrmTestEntity> ormTestEntityPage = mOrmTestReposity.findAll(predicate,pPageable);
return ormTestEntityPage;
}
}
controller:
@Operation(summary = “jpa分页查询”)
@GetMapping(“/jpa/page”)
public Page jpaPage(
@QuerydslPredicate(root = OrmTestEntity.class) Predicate predicate, Pageable page) {
Page pageData = mOrmJpaService.page(page,predicate);
return pageData;
}
repository:
public interface OrmTestReposity extends CrudRepository<OrmTestEntity,Long>, QuerydslPredicateExecutor<OrmTestEntity>, QuerydslBinderCustomizer<QOrmTestEntity> {
@Override
default void customize(QuerydslBindings bindings, QOrmTestEntity pQOrmTestEntity){
// name 使用like
bindings.bind(pQOrmTestEntity.name).first((path,value)->path.contains(value));
bindings.bind(pQOrmTestEntity.appName).first((path,value)->path.contains(value));
bindings.bind(pQOrmTestEntity.moduleName).first((path,value)->path.contains(value));
bindings.bind(pQOrmTestEntity.description).first((path,value)->path.contains(value));
bindings.bind(pQOrmTestEntity.value).all((path,value) -> {
Iterator<? extends Integer> it = value.iterator();
Integer from = it.next();
if (value.size() >= 2) {
Integer to = it.next();
return Optional.of(path.between(from, to)); // between
} else {
return Optional.of(path.goe(from)); // greater or equal
}
});
}
}
如果不重载customize,默认传入的所有参数都是以=的方式解析。
对于前端访问来说,可以这样:
http://localhost:8081/api/ormTest/jpa/page?page=1&size=10&name=JPA&value=10&value=30&status=STATUS_1
这段访问的意思是,查询name like “JPA”, 状态处于STATUS_1(数据库中存的是1),并且value在10~20的分页数据,分页大小是10,当前页0,
具体使用方法请参见官网文档
我们在前面看到,有一个类叫QOrmTestEntity,这种Q开头的是什么东东。
这是JPA-querydsl的生成类,通过这个类我们才能使用querydsl的相关功能。
这个类的生成是这样的:
@Generated("com.querydsl.codegen.EntitySerializer")
public class QOrmTestEntity extends EntityPathBase<OrmTestEntity> {
private static final long serialVersionUID = 863569505L;
public static final QOrmTestEntity ormTestEntity = new QOrmTestEntity("ormTestEntity");
public final StringPath appCode = createString("appCode");
public final StringPath appName = createString("appName");
public final StringPath code = createString("code");
public final BooleanPath del = createBoolean("del");
public final StringPath description = createString("description");
public final NumberPath<Long> id = createNumber("id", Long.class);
public final StringPath moduleCode = createString("moduleCode");
public final StringPath moduleName = createString("moduleName");
public final StringPath name = createString("name");
public final EnumPath<indi.zhifa.recipe.bailan.busy.enums.EOrmEntityStatus> status = createEnum("status", indi.zhifa.recipe.bailan.busy.enums.EOrmEntityStatus.class);
public final NumberPath<Integer> value = createNumber("value", Integer.class);
public final NumberPath<Integer> version = createNumber("version", Integer.class);
public QOrmTestEntity(String variable) {
super(OrmTestEntity.class, forVariable(variable));
}
public QOrmTestEntity(Path<? extends OrmTestEntity> path) {
super(path.getType(), path.getMetadata());
}
public QOrmTestEntity(PathMetadata metadata) {
super(OrmTestEntity.class, metadata);
}
}
我们只需要在项目的maven pom.xml中,添加如下配置。在工程编译的时候,就会自动生成对应的Q类
<build>
<plugins>
<plugin>
<groupId>com.mysema.mavengroupId>
<artifactId>apt-maven-pluginartifactId>
<version>1.1.3version>
<executions>
<execution>
<goals>
<goal>processgoal>
goals>
<configuration>
<outputDirectory>target/generated-sources/javaoutputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessorprocessor>
configuration>
execution>
executions>
plugin>
plugins>
build>
JPA的审计功能,可以让程序员在entity上添加响应的注解,如CreateBy,CreatedDate,LastModifiedBy,LastModifiedDate
并重载AuditorAware< Long > 接口,用于填充CreateBy和LastModifiedBy字段即可
在启动类上,也要加上@EnableJpaAuditing注解
JPA其实不支持假删除,如果一定要用假删除,那就不要调用deleteById接口,使用save接口更改del字段就可以了。
但我们在查询时,谁都不想在每个接口中处理del字段,那怎么办呢?
我可以使用Spring的AOP机制,拦截带有Predicate的Controller的接口,在其中拼上del相关的条件
乐观锁很简单,在entity的version字段加上@Version注解即可
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>com.mysema.mavengroupId>
<artifactId>apt-maven-pluginartifactId>
<version>1.1.3version>
<executions>
<execution>
<goals>
<goal>processgoal>
goals>
<configuration>
<outputDirectory>target/generated-sources/javaoutputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessorprocessor>
configuration>
execution>
executions>
plugin>
plugins>
build>
@Component
public class JPAAuditorAware implements AuditorAware<Long> {
@Override
public Optional<Long> getCurrentAuditor() {
Long id = SnowflakeIdWorker.generateId();
return Optional.of(id);
}
}
@Slf4j
public class SnowIdGenerator implements IdentifierGenerator {
/**
* 终端ID
*/
public static long WORKER_ID = 1;
/**
* 数据中心id
*/
public static long DATACENTER_ID = 1;
private Snowflake snowflake = IdUtil.createSnowflake(WORKER_ID, DATACENTER_ID);
@PostConstruct
public void init() {
WORKER_ID = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
log.info("当前机器的workId:{}", WORKER_ID);
}
public synchronized long snowflakeId() {
return snowflake.nextId();
}
public synchronized long snowflakeId(long workerId, long datacenterId) {
Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
return snowflake.nextId();
}
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object)
throws HibernateException {
return snowflakeId(WORKER_ID, DATACENTER_ID);
}
}
在entity的id上加如下注解
@GeneratedValue(generator = "orderIdGenerator", strategy = GenerationType.SEQUENCE)
@GenericGenerator(name = "orderIdGenerator", strategy = "indi.zhifa.recipe.bailan.busy.handler.SnowIdGenerator")
@Slf4j
@Aspect
@Component
@Order(1)
public class LogicDelApo {
private final EntityPathResolver entityPathResolver;
private final QuerydslBindingsFactory bindingsFactory;
private final QuerydslPredicateBuilder predicateBuilder;
public LogicDelApo(ObjectProvider<EntityPathResolver> resolver, Optional<ConversionService> conversionService){
QuerydslBindingsFactory factory = new QuerydslBindingsFactory((EntityPathResolver)resolver.getIfUnique(() -> {
return SimpleEntityPathResolver.INSTANCE;
}));
this.entityPathResolver = (EntityPathResolver)resolver.getIfUnique(() -> {
return SimpleEntityPathResolver.INSTANCE;
});
this.bindingsFactory = factory;
this.predicateBuilder = new QuerydslPredicateBuilder((ConversionService)conversionService.orElseGet(DefaultConversionService::new), factory.getEntityPathResolver());
}
@Around("execution(* indi.zhifa.recipe..*.*controller.api..*(..)) && @annotation(permission)")
public Object around(ProceedingJoinPoint joinPoint, JpaQsdl permission) throws Throwable {
Object[] args = joinPoint.getArgs();
for(int i = 0; i < args.length; ++i) {
if (args[i] instanceof Predicate) {
Predicate query = (Predicate)args[i];
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Parameter[] methodParameters = signature.getMethod().getParameters();
Parameter parameter = methodParameters[i];
QuerydslPredicate predicate = (QuerydslPredicate)parameter.getDeclaredAnnotation(QuerydslPredicate.class);
Assert.notNull(parameter, "Predicate参数的@QuerydslPredicate注解必须声明!");
MultiValueMap<String, String> parameters = new LinkedMultiValueMap();
parameters.add("del","0");
args[i] = this.createPredicate(parameters, query, predicate);
return joinPoint.proceed(args);
}
}
return joinPoint.proceed();
}
private Predicate createPredicate(MultiValueMap<String, String> parameters, Predicate rightQuery, QuerydslPredicate predicate) {
Optional<QuerydslPredicate> annotation = Optional.ofNullable(predicate);
TypeInformation<?> domainType = (TypeInformation)annotation.filter((it) -> {
return !Object.class.equals(it.root());
}).map((it) -> {
return ClassTypeInformation.from(it.root());
}).orElse(null);
Assert.notNull(domainType, "root不能为空");
Optional<Class<? extends QuerydslBinderCustomizer<EntityPath<?>>>> bindingsAnnotation = annotation.map(QuerydslPredicate::bindings).map(CastUtils::cast);
QuerydslBindings bindings = createBindings(bindingsAnnotation, domainType);
Predicate permissionPredicate = this.predicateBuilder.getPredicate(domainType, parameters, bindings);
BooleanBuilder builder = new BooleanBuilder(permissionPredicate);
return builder.and(rightQuery);
}
private QuerydslBindings createBindings(Optional<Class<? extends QuerydslBinderCustomizer<EntityPath<?>>>> bindingsAnnotation, TypeInformation<?> domainType) {
QuerydslBindings bindings = (QuerydslBindings)bindingsAnnotation.map((it) -> {
return this.bindingsFactory.createBindingsFor(domainType, it);
}).orElseGet(() -> {
return this.bindingsFactory.createBindingsFor(domainType);
});
EntityPath path = this.entityPathResolver.createPath(domainType.getType());
try {
// 取得SimplePath的构造函数(源代码是protect的)
Constructor<BooleanPath> appPathConstructor = BooleanPath.class.getDeclaredConstructor(Path.class, String.class);
appPathConstructor.setAccessible(true);
BooleanPath delPath = appPathConstructor.newInstance(path, "del");
bindings.bind(delPath).first(SimpleExpression::eq);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException var12) {
throw new ServiceException(var12.getMessage());
}
return bindings;
}
}
public interface OrmTestReposity extends CrudRepository<OrmTestEntity,Long>, QuerydslPredicateExecutor<OrmTestEntity>, QuerydslBinderCustomizer<QOrmTestEntity> {
@Override
default void customize(QuerydslBindings bindings, QOrmTestEntity pQOrmTestEntity){
// name 使用like
bindings.bind(pQOrmTestEntity.name).first((path,value)->path.contains(value));
bindings.bind(pQOrmTestEntity.appName).first((path,value)->path.contains(value));
bindings.bind(pQOrmTestEntity.moduleName).first((path,value)->path.contains(value));
bindings.bind(pQOrmTestEntity.description).first((path,value)->path.contains(value));
bindings.bind(pQOrmTestEntity.value).all((path,value) -> {
Iterator<? extends Integer> it = value.iterator();
Integer from = it.next();
if (value.size() >= 2) {
Integer to = it.next();
return Optional.of(path.between(from, to)); // between
} else {
return Optional.of(path.goe(from)); // greater or equal
}
});
}
}
@RequiredArgsConstructor
@Service
public class OrmJpaServiceImpl extends BaseOrmServiceImpl implements IOrmJpaService {
private final OrmTestReposity mOrmTestReposity;
@Transactional(rollbackFor = Exception.class)
@Override
public void initTestData() {
List<OrmTestEntity> initOrmTestEntityList = getInitOrmTestEntityList("JPA");
mOrmTestReposity.saveAll(initOrmTestEntityList);
}
@Override
public Page<OrmTestEntity> page(Pageable pPageable, Predicate predicate) {
Page<OrmTestEntity> ormTestEntityPage = mOrmTestReposity.findAll(predicate,pPageable);
return ormTestEntityPage;
}
}
@EnableJpaAuditing
@EnableJpaRepositories(basePackages = {"indi.zhifa.recipe.bailan.busy.**.repository"})
我们已经看到,JPA几乎满足了我们在1.2中讨论的所有问题,那为什么还要搞个mybatis-plus呢?
JPA是一个基于hibernate的一个框架,如果深入使用JPA的话,就会发现JPA的代码叠床架屋,十分难以驾驭。
而且JPA也无法像mybatis那样灵活操作sql。
mybatis在中国市场占有率如此之高,除了程序员时薪较发达国家偏低的原因外,肯定还是有其优势的。但在大多数情况下,mybatis的优势并无法体现出来,但却拖慢了开发的效率。
这时,mybatis-plus就呼之欲出。mybatis-plus基于mybatis编写,支持所有mybatis的功能。此外,mybatis-plus提供了类似JPA中CrudRepository+QuerydslPredicateExecutor的功能。同时对诸如审计字段、假删除、乐观锁等常见需求都有很好的支持。下面,就让我们来看看mybatis-plus吧。
mybatis根据数据库生成相应代码,可以自己写个main函数,引用mybatis-plus-generator来实现。
更多细节请参见官网
pom引用:
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
dependency>
代码:
public class MapperGenerator {
public static void main(String[] args) throws FileNotFoundException {
String outputPath = "E:\\DOCUMENT\\generator\\bailan2";
FastAutoGenerator.create("jdbc:mysql://localhost:3307/bailan?useSSL=false&useUnicode=true&characterEncoding=utf-8",
"root", "qqhilvMgAl@7")
.globalConfig(builder -> {
// 设置作者
builder.author("织法")
// 开启 swagger 模式
.enableSwagger()
// 覆盖已生成文件
.fileOverride()
// 指定输出目录
.outputDir(outputPath);
})
.packageConfig(builder -> {
// 设置父包名
builder.parent("indi.zhifa.recipe.bailan.busy")
// 指定模块名称
.moduleName("dbgen")
.entity("entity.po")
.service("dao.service")
.serviceImpl("dao.service.impl")
.mapper("dao.mapper");
})
.strategyConfig(builder -> {
// 设置过滤表前缀
builder.addTablePrefix("bl_","sys_")
.addExclude("gc_user")
.entityBuilder()
.enableLombok()
.enableRemoveIsPrefix()
.logicDeleteColumnName("del")
.logicDeletePropertyName("del")
.addTableFills(new Column("create_time", FieldFill.INSERT))
.addTableFills(new Property("createTime", FieldFill.INSERT))
.addTableFills(new Column("create_by",FieldFill.INSERT))
.addTableFills(new Property("createBy",FieldFill.INSERT))
.addTableFills(new Column("create_name",FieldFill.INSERT))
.addTableFills(new Property("createName",FieldFill.INSERT))
.addTableFills(new Column("create_ip",FieldFill.INSERT))
.addTableFills(new Property("createIp",FieldFill.INSERT))
.addTableFills(new Column("modify_time", FieldFill.UPDATE))
.addTableFills(new Property("modifyTime", FieldFill.UPDATE))
.addTableFills(new Column("modify_by",FieldFill.UPDATE))
.addTableFills(new Property("modifyBy",FieldFill.UPDATE))
.addTableFills(new Column("modify_name",FieldFill.UPDATE))
.addTableFills(new Property("modifyName",FieldFill.UPDATE))
.addTableFills(new Column("modify_ip",FieldFill.UPDATE))
.addTableFills(new Property("modifyIp",FieldFill.UPDATE))
.idType(IdType.ASSIGN_ID)
.formatFileName("%sEntity")
.entityBuilder()
.superClass(BaseEntity.class)
.disableSerialVersionUID()
.enableLombok()
.versionColumnName("version")
.versionPropertyName("version")
.addSuperEntityColumns("id","create_time","create_by","create_name","create_ip","modify_by","modify_time","modify_name","modify_ip","version","del")
.serviceBuilder()
.formatServiceFileName("I%sDbService")
.formatServiceImplFileName("%sDbServiceImpl")
.serviceBuilder()
.superServiceClass(IZfDbService.class)
.superServiceImplClass(ZfDbServiceImpl.class);
})
// 使用Freemarker引擎模板,默认的是Velocity引擎模板
.templateEngine(new FreemarkerTemplateEngine())
.execute();
}
}
mybatis-plus不用生成Q类,也可以实现类似QuerydslPredicateExecutor的功能。使用方法非常简单,小伙伴们可以去官网看一下。代码样例可以去5.6看,这里就不做过多赘述了。
类似JPA的CrudRepository,mybatis-plus提供了一个方便的service基类,其中包括常见的实体操作。
我们只需要在接口上继承IService< T >, 实现类上继承 ServiceImpl< T >即可。
mybatis中,可以写一个handler类继承自MetaObjectHandler,就可以实现字段填充。
@Component
@Slf4j
public class SimpleBaseMetaObjectHandler implements MetaObjectHandler {
protected static final Long SYS_ID = 0L;
protected static final String SYS_NAME = "sys";
protected static final String SYS_IP = "localhost";
protected String createTimeStr = "createTime";
protected String createByStr= "createBy";
protected String createNameStr = "createName";
protected String createIpStr = "createIp";
protected String modifyTimeStr = "modifyTime";
protected String modifyByStr= "modifyBy";
protected String modifyNameStr = "modifyName";
protected String modifyIpStr = "modifyIp";
@Override
public void insertFill(MetaObject pMetaObject) {
if(checkFieldNull(pMetaObject,createTimeStr)){
this.strictInsertFill(pMetaObject,createTimeStr,()-> LocalDateTime.now(),LocalDateTime.class);
}
if(checkFieldNull(pMetaObject,createByStr)){
this.strictInsertFill(pMetaObject,createByStr,()->SYS_ID,Long.class);
}
if(checkFieldNull(pMetaObject,createNameStr)){
this.strictInsertFill(pMetaObject,createNameStr,()->SYS_NAME,String.class);
}
if(checkFieldNull(pMetaObject,createIpStr)){
this.strictInsertFill(pMetaObject,createIpStr,()->SYS_IP,String.class);
}
}
@Override
public void updateFill(MetaObject pMetaObject) {
if(checkFieldNull(pMetaObject,modifyTimeStr)){
this.strictUpdateFill(pMetaObject,modifyTimeStr,()-> LocalDateTime.now(),LocalDateTime.class);
}
if(checkFieldNull(pMetaObject,modifyByStr)){
this.strictUpdateFill(pMetaObject,modifyByStr,()->SYS_ID,Long.class);
}
if(checkFieldNull(pMetaObject,modifyNameStr)){
this.strictUpdateFill(pMetaObject,modifyNameStr,()->SYS_NAME,String.class);
}
if(checkFieldNull(pMetaObject,modifyIpStr)){
this.strictUpdateFill(pMetaObject,modifyIpStr,()->SYS_IP,String.class);
}
}
protected boolean checkFieldNull(MetaObject pMetaObject, String pField){
if(pMetaObject.hasSetter(pField)){
Object orgModifyTime = pMetaObject.getValue(pField);
if(null == orgModifyTime){
return true;
}
}
return false;
}
}
假删除,只需要在entity相应字段上加上@TableLogic注解
乐观锁,只需要在entity相应字段上加上@Version注解
public interface OrmTestMybatisPlusMapper extends BaseMapper<OrmTestEntity> {
}
public interface IOrmTestMybatisPlusDbService extends IService<OrmTestEntity> {
}
@Component
public class OrmTestMybatisPlusDbServiceImpl extends ServiceImpl<OrmTestMybatisPlusMapper, OrmTestEntity> implements IOrmTestMybatisPlusDbService {
}
@Slf4j
@RequiredArgsConstructor
@Service
public class OrmMybatisPlusServiceImpl extends BaseOrmServiceImpl implements IOrmMybatisPlusService {
private final IOrmTestMybatisPlusDbService mOrmTestMybatisPlusDbService;
@Override
public void initTestData() {
List<OrmTestEntity> initOrmTestEntityList = getInitOrmTestEntityList("MyBatis-Plus");
mOrmTestMybatisPlusDbService.saveBatch(initOrmTestEntityList);
}
@Override
public Page<OrmTestEntity> page(int pCurrent, int pSize,
String pAppCode, String pModuleCode, String pCode, String pName, EOrmEntityStatus pStatus,
Integer pMin, Integer pMax) {
Page<OrmTestEntity> pageCfg = new Page<>(pCurrent,pSize);
LambdaQueryWrapper<OrmTestEntity> queryWrapper = Wrappers.<OrmTestEntity>lambdaQuery()
.eq(!StringUtils.isEmpty(pAppCode),OrmTestEntity::getAppCode,pAppCode)
.eq(!StringUtils.isEmpty(pAppCode),OrmTestEntity::getAppCode,pAppCode)
.eq(!StringUtils.isEmpty(pModuleCode),OrmTestEntity::getModuleCode,pModuleCode)
.eq(!StringUtils.isEmpty(pCode),OrmTestEntity::getCode,pCode)
.like(!StringUtils.isEmpty(pName),OrmTestEntity::getName,pName)
.eq(null != pStatus,OrmTestEntity::getStatus,pStatus);
if(null!= pMin && null != pMax){
queryWrapper = queryWrapper.between(OrmTestEntity::getValue,pMin,pMax);
}else if(null == pMin && null != pMax){
queryWrapper = queryWrapper.ge(OrmTestEntity::getValue,pMax);
}else if(null != pMax && null == pMax){
queryWrapper = queryWrapper.le(OrmTestEntity::getValue,pMin);
}
Page<OrmTestEntity> pageData = mOrmTestMybatisPlusDbService.page(pageCfg,queryWrapper);
return pageData;
}
}
通过前面的样例与讲解,结合小编的工作经验,给出如下总结:
JDBC虽然最麻烦,但也最灵活与轻量。在不确定数据源、不确定要执行具体什么逻辑的时候,比如写一些通用的框架,JDBC就是最适合的了。比如写一个报表系统,用户把要执行的sql存储到数据库中,调用相应接口执行sql。这个时候,无疑JDBC是最好的选择。
Mybatis的核心就是灵活生成sql。一些诸如大屏、报表的业务,其核心就是写复杂的聚合sql,并做一些逻辑运算。这种时候选用Mybatis就是最合适的了。
JPA更适合快速开发系统,把数据库设计好后,所有代码一并生成,快速部署交给前端。当产品和前端有一些新需求时,后端再介入开发。等前端开发告一段落,后端再做一次代码和安全性的优化。
也就是说,JPA更适合前端作为逻辑驱动,后端只是提供一个restful接口访问的数据仓库。
mybatis-plus更适合产品->后端->前端这种方式驱动的项目。产品先设计业务逻辑,后端设计数据库,生成初步的代码。而后根据一个一个的页面,认真编写接口。接口编写完成后,交给前端开发,然后后端继续编写接口的实现。
由于mybatis-plus兼容mybatis,一些大屏报表的业务,可以使用Mybatis的方式做开发。
ORM | 学习成本 | 开发效率 | 运行效率 | 深入学习 |
---|---|---|---|---|
JDBC | B | D | A | 容易 |
Mybatis | C | C | A | 较容易 |
JPA | B | A | C | 较难 |
Mybatis-plus | A | A | B | 较容易 |
ORM | 审计支持 | 乐观锁 | 假删除 | 易维护性 | 安全性 |
---|---|---|---|---|---|
JDBC | × | × | × | D | C |
Mybatis | × | × | × | C | A |
JPA | o | o | × | A | D |
Mybatis-plus | o | o | o | B | B |
注意,为什么小编认为JPA不安全呢,原因既是在于querydsl的通用分页接口的写法。
很多时候,程序员仅仅生成了代码,就把接口放了出去。entity中所有字段都可以被前端操作,想用什么字段查询就用什么字段查询,想修改哪个字段就修改哪个字段。
虽说JPA可以通过注解方式,使某些字段不参与条件。但在实际开发中,下面的程序员常常不去做这些,最后导致整个项目像是前端在写,后端只负责设计了个数据库和运维部署。