JPA 的封装和使用
一切一切为了简化操作,远离 CRUD,提升开发体验
核心操作
JPA 的封装在 developer-sql-jpa 中,基本上将数据库的核心关键词都已经封装
在 application.yml 中定义了 JPA 的分割符
# version: 2.0
baseSqlQuery:
opSeparator: _ #主要用于分割字段与操作符
例如:
- name_Like,转换为 SQL 为
name like '%?%'
- name_StartWith,转换为 SQL 为
name like '?%'
- createdAt_Gt,转换为 SQL 为
created_at > ?
...
操作符说明
如果一个字段不带关键词,那么就是 equal
操作符 | 说明 | 备注 |
---|---|---|
Is | equal | 一般不需要使用,默认就是 equal |
Eq | equal | 一般不需要使用,默认就是 equal |
Neq | notEqual | 不等于 |
StartWith | like(root.get(queryField), val + "%") | 首字符匹配 |
EndWith | like(root.get(queryField), "%" + val) | 尾字符匹配 |
Like | like(root.get(queryField), "%" + val + "%") | 相似匹配 |
Gt | greaterThan | 大于 |
Gte | greaterThanOrEqualTo | 大于等于 |
Lt | lessThan | 小于 |
Lte | lessThanOrEqualTo | 小于等于 |
NotNull | isNotNull | 非 null,3.0.4 版本进行了优化,value 可以输入 null |
Null | 为 null | isNull ,同样在 3.0.4 版本中进行了优化,value 可以输入 null |
Between | between | 在 x,x 之间 |
In | in | 在数组内 |
NotIn | not in | 不在数组内 |
FindInSet | MySql 的 find_in_set 方法 | 3.0.7 升级上线,特别适合存储 ids 的查找场景FindInSet |
如果你希望的操作符不在上面关键词中,那么可以向 TerryQi 提出改进
- 2022 年 10 月 22 日
在 orderBy 中提供获取随机数的方法,提供 rand()的关键词,可以乱序获取列表数据,具体用法如下:
BaseSqlQuery baseSqlQuery = (BaseSqlQuery) query;
baseSqlQuery.orderBy("rand()", null);
当然,这个能力已经在 Manager 层进行封装,具体方法为 randomOne 和 randomList
指定条件排序
在某些场景下,需要根据指定字段进行排序,即在 Qo 中,需要指定根据某些字段来升序或者降序,3.1.1 版本中,针对 BaseQuery 进行了升级,可以指定
LinkedHashMap<String, Sort.Direction> orderBy
字段,完成排序操作
基础用法
首先,我们扩展了 BaseQuery 能力,可以多传入一个参数LinkedHashMap<String, Sort.Direction> orderBy
,这个参数就代表排序规则,LinkedHashMap
代表一个有序的 Map(因为排序规则是有序的);String
代表字段;orderBy
代表升序或者降序
!!!请注意,在 Qo 中,排序字段值必须叫做orderByCustomSort
,否则就要在创建 BaseQuery 中,指定排除的字段
@Data
public class SysNationCodeQueryQo extends PageableQo {
@ApiModelProperty(value = "民族名称", example = "", required = false)
private String name_Like;
@ApiModelProperty(value = "民族编号", example = "", required = false)
private String code;
@ApiModelProperty(value = "根据指定条件排序", example = "", required = false)
private LinkedHashMap<String, Sort.Direction> orderByCustomSort;
}
目前,默认 qo 中定义排序的字段为orderByCustomSort
,当然你也可以自定义字段,那么就需要在创建 BaseQuery 时,指定这个字段不要进入 Where 的条件
// 这里第二个参数就是个性化排序的Map
Specification query = new BaseSqlQuery(qo, qo.getOrderByCustomSort());
nationCodeManager.findAll(query);
如果自己定义为 myOrderBy,那么要注意增加fieldExclude
字段,这个字段的含义是不加入 Qo 的 where 排序条件中
// 这里第二个参数就是个性化排序的Map,例如自己可以定义为myOrderBy
Specification query = new BaseSqlQuery(qo, qo.getMyOrderBy(),{"myOrderBy"});
nationCodeManager.findAll(query);
orderBy 的个性化处理
2024 年 5 月,为了解决根据检索结果排序的问题(例如 TopN 用户访问量等),增强了 orderBy 的属性,在 orderBy 中,可以根据字段结果进行排序
操作符 | 说明 | 备注 |
---|---|---|
Count | 进行统计 | 翻译出来的 Sql 就是 count(--) |
CountDistinct | 去重统计 | 翻译出来的 Sql 就是 count(distinct --) |
Sum | 求和 | 翻译出来的 Sql 就是 sum(--) |
Max | 求和 | 翻译出来的 Sql 就是 max(--) |
Min | 求和 | 翻译出来的 Sql 就是 min(--) |
Avg | 求和 | 翻译出来的 Sql 就是 avg(--) |
DateFormat | 转换为日期格式,即年-月-日 | 翻译出来的 Sql 就是 date_format(field,'%Y-%m-%d') |
MonthFormat | 转换为日期格式,即年-月 | 翻译出来的 Sql 就是 date_format(field,'%Y-%m') |
HourFormat | 转换为小时格式,即时(24 小时制) | 翻译出来的 Sql 就是 date_format(field,'%H') |
SmartSqlQuery query = (SmartSqlQuery) new SmartSqlQuery()
.multiSelect(Arrays.asList("provinceCityId_Count@num", "cityLevel"))
.groupBy("cityLevel")
.orderBy("provinceCityId_Count", Sort.Direction.ASC)
.setLimit(2).setOffset(1);
List<ProvinceDo> doList = provinceCityManager.smartFind(query, ProvinceDo.class);
上述例子中,就可以根据 provinceCityId_Count 的结果进行排序了,有问题请 call TerryQi
关于 limit 和 offset 关键词
通过 limit 和 offset,可以进行数据条目和偏移量的控制,和 MySQL 中的 limit、offset 的概念一样
在 BaseSqlQuery 和 SmartQuery 中,都提供了 limit、offset 属性,特别适合于获取 TopN 数据的需求
BaseSqlQuery query = (BaseSqlQuery) new BaseSqlQuery()
.setLimit(2).setOffset(1).setDataScope(DataScopeEnum.ALL);
List<SysProvinceCityEntity> doList = provinceCityManager.findAll(query);
Assert.assertTrue(true);
view 视图的使用(重要)
在实施 XX 项目时,发现我们的开发人员在 repository 中写了比较复杂的原始 sql 语句,究其原因,是因为项目中使用到了跨表的关联查询,由于 JPA 对跨表关联查询并不友好,导致我们的代码既复杂、又难懂,还有很多的语法级别的问题
JPA、Mybatis-plus 都不能很好地做跨表的复杂关联查询,但是我们 JPA 的单表查询能力很强,怎么办?
答案是 视图操作,使用视图会很好地解决复杂查询的问题,将跨表关联转查询换为单表查询,那么视图的缺点也很明显:
视图不好维护,在使用 navicat 进行数据迁移时,视图往往迁移不完整,所以要在代码中保存一下视图。位置在/doc/sql/view 目录中,一个视图一个 sql
表调整视图也需要同步调整,这里也不算是缺点,因为原始表调整的话,无论 Entity 还是我们的原始 sql 都需要调整
!!! 请注意,大家要理解视图的概念,视图仅仅用来查询,是无法用来增、删、改数据的。
视图的创建
正常的建设视图的语句,本质就是 sql 语句,那么请注意,视图一定要以 xxx_view 来命名,那么我们就知道 xxxViewEntity 是视图了
例如下面视图语句,就是将打卡记录补充执行区域信息
/**
主要补充打开信息的execute_area_id字段
*/
CREATE VIEW drug_clock_in_view AS
(
SELECT dci.*,
dp.execute_area_id AS execute_area_id
FROM drug_clock_in AS dci
LEFT JOIN drug_personnel AS dp ON dci.drug_personnel_id = dp.drug_personnel_id
)
视图的使用
与表一样生成 Entity 和 Manager 使用,要注意保存好视图的 sql(以便于在生产环境创建),接下来可以愉快地使用视图啦
对于一些联合查询、趋势统计很有效率,而且代码清晰、条件易于拼装,会很大的提升我们的开发体验
SmartQuery
历史上,数据趋势统计等能力都需要自定义 Sql 的方式来编写,不是很友好。其实 Jpa 给出的灵活 Sql 的能力与 laravel 的 ORM 几乎是一样的,那么通过 SmartQuery 的封装,对于单表可以进行比较灵活的统计类操作,也就是 count 和 groupBy
说明
SmartQuery 本质上是继承于 BaseSqlQuery,但是将 singleSelect, multiSelect 封装出来,在 multiSelect 中,可以加入所需要选择的列,并且附加操作符,可以加入的操作符如下
操作符 | 说明 | 备注 |
---|---|---|
Count | 进行统计 | 翻译出来的 Sql 就是 count(--) |
CountDistinct | 去重统计 | 翻译出来的 Sql 就是 count(distinct --) |
Sum | 求和 | 翻译出来的 Sql 就是 sum(--) |
Max | 求和 | 翻译出来的 Sql 就是 max(--) |
Min | 求和 | 翻译出来的 Sql 就是 min(--) |
Avg | 求和 | 翻译出来的 Sql 就是 avg(--) |
DateFormat | 转换为日期格式,即年-月-日 | 翻译出来的 Sql 就是 date_format(field,'%Y-%m-%d') |
YearFormat | 转换为日期格式,即年 | 翻译出来的 Sql 就是 date_format(field,'%Y') |
MonthFormat | 转换为日期格式,即年-月 | 翻译出来的 Sql 就是 date_format(field,'%Y-%m') |
HourFormat | 转换为日期格式,即小时 | 翻译出来的 Sql 就是 date_format(field,'%H') |
WeekFormat | 转换为日期格式,即周 | 翻译出来的 Sql 就是 date_format(field,'%u') |
IOSWeekFormat | 转换为日期格式,即周 | 翻译出来的 Sql 就是 yearweek(field,1) |
Select 字段设置别名
一般情况下,尤其是统计数据,select 的值我们需要换个名字,那么可以@
符号来设定别名,例如下面语句,就是count(code) as num
,统计 code 值作为 num
selectFieldList.add("code_Count@num");
Count & Find
在 Manager 中提供了 SmartCount 和 SmartFind 方法,其中 SmartCount 是用于统计数据的功能,输出是一个值,例如统计每日的 Pv 数;SmartFind 是输出趋势图的功能,结合 GroupBy 可以输出 List 的数据
Count 的例子
例如综合统计平台,我们需要统计每日的 Uv 数,那么 Uv 是需要根据用户 id 进行去重处理的,则在 userUuid 后加 _CountDistinct 即可
那么 _下划线 之后就是跟随的操作
SmartSqlQuery query = (SmartSqlQuery<SysNationCodeEntity>)
new SmartSqlQuery<SysNationCodeEntity>().singleSelect("value_Max")
.append("status", CommonConst.COMMON_VALID);
BigDecimal value = (BigDecimal) nationCodeManager.smartCount(query);
翻译的 Sql 为
SELECT
count( DISTINCT tjsviewrec0_.user_uuid ) AS col_0_0_
FROM
tjs_view_record tjsviewrec0_
WHERE
tjsviewrec0_.deleted_flag = '0'
- 请注意,smartCount 不算很优雅,因为统一返回的值是 Number 类型,那么要根据具体的情况强制转换下,报错的时候有提示。例如 count 返回 Long、Avg 返回 Double、Sum 就遵循字段类型,如果是 BigDecimal,就要用 BigDecimal 来接收,我没有找到更优雅的方案,目前是这样(主要原因在于 JPA 不同操作的返回值不同)
结合 GroupBy 进行趋势统计
结合 GroupBy 进行趋势统计可能是最关键的功能啦,例如你需要统计每天的 Pv 和 Uv 趋势,那么定义好 Dto 后,通过 SmartQuery 就可以获取数据了
我们的综合统计平台就是通过 GroupBy 来实施的,例如下面 :
- Pv 统计的样例
List<String> multiSelect = Arrays.asList("userUuid_Count", "dateAt");
List<TrendLongNumDo> doList = viewRecordManager
.smartFind((SmartSqlQuery<TjsViewRecordEntity>) new SmartSqlQuery<TjsViewRecordEntity>(request)
.multiSelect(multiSelect).groupBy("dateAt"), TrendLongNumDo.class);
doList = FillZeroUtils.fillUpZero(doList, request.getDateAt_Gte(), request.getDateAt_Lte());
- Uv 统计样例
List<String> multiSelect = Arrays.asList("userUuid_CountDistinct", "dateAt");
List<TrendLongNumDo> doList = viewRecordManager
.smartFind((SmartSqlQuery<TjsViewRecordEntity>) new SmartSqlQuery<TjsViewRecordEntity>(request)
.multiSelect(multiSelect).groupBy("dateAt"), TrendLongNumDo.class);
doList = FillZeroUtils.fillUpZero(doList, request.getDateAt_Gte(), request.getDateAt_Lte());
- 分时 Pv 统计
List<String> multiSelect = Arrays.asList("userUuid_Count", "hourAt");
List<TimeDivideTrendDto> doList = viewRecordManager
.smartFind((SmartSqlQuery<TjsViewRecordEntity>) new SmartSqlQuery<TjsViewRecordEntity>(request)
.multiSelect(multiSelect).groupBy("hourAt"), TimeDivideTrendDto.class);
doList = TrendUtils.fillUpZero(doList);
为了支持前端业务,也提供了统一的补零工具,大家可以使用
趋势统计 HourFormat 处理
HourFormat 就是将数据库中的年-月-日 时:分:秒
的数据转换为时(24小时制)
的格式,这个场景是比较通用的,例如要根据日期中的小时统计某个趋势
怎么用?详见 SmartQuery 中的 HourFormat 信息
具体用法为:
SmartSqlQuery query = new SmartSqlQuery();
List<String> selectFieldList = new ArrayList<>();
selectFieldList.add("code_Count@num");
selectFieldList.add("code");
selectFieldList.add("createdAt_HourFormat@hourAt");
query.multiSelect(selectFieldList);
query.groupBy("createdAt_HourFormat");
List<NationTrendWithDateTimeDto> result = nationCodeManager.smartFind(query, NationTrendWithDateTimeDto.class);
请注意这里的 createdAt_HourFormat,就是代表在 createdAt 字段上,套 date_format( created_at, '%H' )
函数
趋势统计 DateFormat 处理
顾明思议,DateFormat 就是将数据库中的年-月-日 时:分:秒
的数据转换为年-月-日
的格式,这个场景是比较通用的,例如要根据创建日期统计某个趋势,那么一般创建日期在数据库中是 createdAt 字段,是 datetime 类型的,所以要转换为 date 类型
怎么用?详见 SmartQuery 中的 DateFormat 信息
具体用法为:
SmartSqlQuery query = new SmartSqlQuery();
List<String> selectFieldList = new ArrayList<>();
selectFieldList.add("code_Count@num");
selectFieldList.add("code");
selectFieldList.add("createdAt_DateFormat@dateAt");
query.multiSelect(selectFieldList);
query.groupBy("createdAt_DateFormat");
List<NationTrendWithDateTimeDto> result = nationCodeManager.smartFind(query, NationTrendWithDateTimeDto.class);
请注意这里的 createdAt_DateFormat,就是代表在 createdAt 字段上,套 date_format( created_at, '%Y-%m-%d' )
函数
- 解析 selectFieldList
下面语句的含义是将 createdAt 进行 DateFormat 化,并且起别名为 dateAt,一般在 select 的时候用到
selectFieldList.add("createdAt_DateFormat@dateAt");
- 解析 groupBy
下面语句的含义是将 createdAt 进行 DateFormat 化,并进行分组条件,因为很多的时间字段是 datetime 类型的,但是我们统计要使用 date 来统计
query.groupBy("createdAt_DateFormat");
上面的代码翻译为 sql 为
SELECT
count( sysnationc0_.CODE ) AS col_0_0_,
sysnationc0_.CODE AS col_1_0_,
date_format( sysnationc0_.created_at, '%Y-%m-%d' ) AS col_2_0_
FROM
sys_nation_code sysnationc0_
WHERE
sysnationc0_.deleted_flag = '0'
GROUP BY
date_format( sysnationc0_.created_at, '%Y-%m-%d' )
此外,请注意 NationTrendWithDateTimeDto,一般是和返回结果对应的,如果用了 DateFormat 方法,那么就要用 String 来接了,具体为:
@Data
@AllArgsConstructor
public class NationTrendWithDateTimeDto {
/**
* 数量
*/
private Long num;
/**
* 名称
*/
private String code;
/**
* 日期,请注意,是String类型
*/
private String dateAt;
}
趋势统计 MonthFormat 处理
在 3.1.9 版本中,提供了 MonthFormat 方法,具体用法与 DateFormat 类似,适合于根据月份进行统计的场景
提供了 FillZreoUtils 工具,用于补零,可以参考下 TrendMonthDo 的写法,要继承一个 MonthBaseDo 的抽象类,传入参数 MonthDurationDo 配合进行范围控制
List<String> selectFieldList = new ArrayList<>();
selectFieldList.add("code_Count@num");
selectFieldList.add("createdAt_MonthFormat@monthAt");
SmartSqlQuery smartSqlQuery = (SmartSqlQuery) new SmartSqlQuery().multiSelect(selectFieldList)
.groupBy("createdAt_MonthFormat");
List<TrendMonthDo> trendList = nationCodeManager.smartFind(smartSqlQuery, TrendMonthDo.class);
MonthDurationDo startMonthAt = new MonthDurationDo(2023, 1);
MonthDurationDo endMonthAt = new MonthDurationDo(2024, 6);
trendList = FillZeroUtils.fillUpZeroImpl(trendList, TrendMonthDo.class, startMonthAt, endMonthAt);
log.info("trendList:{}", trendList);
Assert.assertTrue(true);
翻译的 SQL 为
SELECT
count( sysnationc0_.CODE ) AS col_0_0_,
date_format( sysnationc0_.created_at, '%Y-%m' ) AS col_1_0_
FROM
sys_nation_code sysnationc0_
WHERE
sysnationc0_.deleted_flag = '0'
GROUP BY
date_format( sysnationc0_.created_at, '%Y-%m' )
增强补零工具
趋势图往往都要结合补零工具使用,例如分组查询 Pv 的趋势,那么数据库中的数据根据 createdAt 进行分组,可能数据是不连续的,那么如何进行补零呢?
TrendBaseDo 抽象类
TrendBaseDo 是一个趋势图基础的抽象类,提供两个方法:
- convertToDate 方法
即将你的趋势数据中的日期转换为 LocalDate 类型,因为可能日期是 String 类型的(例如用了上面的 DateFormat),所以你要实现一下这个方法,将其转换为 LLocalDate 类型
- setZero 方法
setZero 约等于是数据初始化,即如果搜索的数据中没有当前的日期,那么就需要补零,补零数据的初始化应该是什么样子的
@Data
public abstract class TrendBaseDo {
/**
* 返回日期信息,用于补零操作
*
* @return
*/
public abstract LocalDate convertToDate();
/**
* 初始化数据
*
* @return
*/
public abstract void setZero(LocalDate dateAt);
}
怎么用?
- 首先,定义你的趋势 Dto,实现 convertToDate 方法和 setZero 方法,可以看到,日期已经不非得叫 dateAt 了
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NationTrendWithDateDto extends TrendBaseDo {
/**
* 数量
*/
private Long num;
/**
* 名称
*/
private String dateAt;
@Override
public LocalDate convertToDate() {
LocalDate date = LocalDate.parse(dateAt);
return date;
}
@Override
public void setZero(LocalDate dateAt) {
this.num = 0L;
this.dateAt = dateAt.toString();
}
}
- 使用增强补零工具进行补零操作
SmartSqlQuery query = new SmartSqlQuery();
List<String> selectFieldList = new ArrayList<>();
selectFieldList.add("code_Count@num");
selectFieldList.add("createdAt_DateFormat@dateAt");
query.multiSelect(selectFieldList);
query.groupBy("createdAt_DateFormat");
//用SmartFind获取的趋势数据
List<NationTrendWithDateDto> result = nationCodeManager
.smartFind(query, NationTrendWithDateDto.class);
log.info("result:{}", JSON.toJSONString(result));
//执行补零操作
List<NationTrendWithDateDto> fillUpDto = FillZeroUtils
.fillUpZeroImpl(result,NationTrendWithDateDto.class, LocalDate.now().minusDays(30),LocalDate.now());
log.info("fillUpDto:{}", JSON.toJSONString(fillUpDto));
以上可以实现各种 Dto 的补零,快、稳、便捷,也比较优雅
请注意,我们统一提供了 FillZerorUtils 的补零工具,在 3.1.2 版本中,进一步提供了 TrendNormalLongDo 的标准 Do 结构(包含 num 和 dateAt 字段),一般小型趋势图都可以使用,就不用自己定义 Do 了
关于 Entity
Entity 是通过 jDaoMySQL-v3.groovy 来生成的,具体详见 JDaoMySQL 的使用
请注意字段枚举值的设定,下面例子中 relTable 就是枚举值
PS:CommonEntity 默认的 Id 一定是 Integer 型的,3.0.5 版本进行了升级,提供了 Common2Entity,通过泛型管理 id、createdBy、updatedBy、deletedBy 等字段,具体详见JPA 审计
/**
* @author : jCoder
* @date : 2022-07-21 00:10
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "sys_account_auth")
@EntityListeners(AuditingEntityListener.class)
public class SysAccountAuthEntity extends CommonEntity implements Serializable{
private static final long serialVersionUID = 427821568216479352L;
/**
* PK
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "account_auth_id")
private Integer accountAuthId;
/**
* 关联表名,一般情况下为SYS_USER,即关联sys_user表的用户数据,部分业务中,可能鉴权信息要对应例如sys_user、sys_admin两个表,那么rel_table就是需要关联的表
*/
@Column(name = "rel_table")
@Enumerated(EnumType.STRING)
private RelTableEnum relTable;
/**
* 关联表的id,一般为用户表的id
*/
@Column(name = "rel_id")
private Integer relId;
/**
* TOKEN
*/
@Column(name = "token")
private String token;
/**
* TOKEN有效期
*/
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Column(name = "valid_at")
private LocalDateTime validAt;
/**
* key,加密用
*/
@Column(name = "secret_key")
private String secretKey;
/**
* sercret,加密用
*/
@Column(name = "secret_value")
private String secretValue;
/**
* 状态,是否生效,0:未生效;1:生效
*/
@Column(name = "status")
private String status;
}
关于 Manager 和 Repository
请注意,我们的代码分层结构为 Controller->Service->Manager->Repository,其中:
- Service:一般会封装一些业务,例如带事务
- Manager:单表的业务,如果判断用户名是否重复,给 Service 使用
- Repository:单表的 CRUD
Manager 封装方法
Manager 方法都继承于 BaseSqlManager
方法 | 说明 | 备注 |
---|---|---|
findOne | 找到 1 条数据 | 返回 option,一般需要跟.orElse(null) |
findById | 根据 id 获取数据 | 返回 option,一般需要跟.orElse(null) |
findAll | 获取列表数据 | |
count | 计数 | |
save | 保存 | |
saveAll | 批量保存 | |
delete | 删除 | |
deleteBatch | 批量删除 | |
setSort | 设置顺序 | |
setStatus | 设置状态 | |
query | 同 findAll | |
get | 根据 id 获取数据 | |
randomOne | 随机 1 条数据 | 3.0.7 版本提供 |
randomList | 随机列表数据 | 3.0.7 版本提供 |
findFirstOne | 获取第一条数据 | 3.1.4 版本提供 |
findTopNList | 获取头部几条数据 | 3.1.4 版本提供 |
batchSaveAll | 批量保存数据(多线程) | 3.1.4 版本提供,需要持续验证 |
SchemaManager | 执行原生 sql | 3.1.5 |
SchemaManager 的用法
执行项目的过程中,发现需要执行一些原生的 sql,以往都是在 repository 中实现,可能不够灵活,那么提供 SchemaManager 方法,完成 sql 的执行和对象映射
- 先定义映射的对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TableDo {
/**
* 名称
*/
@ApiModelProperty(value = "表名", example = "")
private String tableName;
/**
* 备注
*/
@ApiModelProperty(value = "备注", example = "")
private String tableComment;
/**
* 库
*/
@ApiModelProperty(value = "库", example = "")
private String tableSchema;
/**
* 类型
*/
@ApiModelProperty(value = "类型", example = "")
private String tableType;
}
- 执行 sql 文件
String sql = "SELECT\n" +
"\tTABLE_NAME as tableName,\n" +
"\tTABLE_COMMENT as tableComment,\n" +
"\tTABLE_SCHEMA as tableSchema,\n" +
"\tTABLE_TYPE as tableType\n" +
"FROM\n" +
"\tinformation_schema.TABLES \n" +
"WHERE\n" +
"\ttable_type = 'BASE TABLE' \n" +
"\tAND table_schema = DATABASE ( );";
List<TableDo> results = schemaManager.nativeQuery(sql, TableDo.class);
Respository 封装方法
Respository 方法都继承于 IBaseSqlRepository,其功能与 Manager 基本一致
实战一下
基础用法
一般情况下 Specification 是检索条件,通过 append 追加条件,追加条件一般为与关系
第一个参数是字段,第二个参数是具体的值,值要和 Entity 的属性保持一致,例如 createdAt 比对的值就应该是 LocalDateTime 类型
这个方案对枚举值很友好,原则上来将,我们全部的状态、类型类的字段都应该用枚举值来管理,枚举值的好处是在 Vo 中,可以自动翻译为 code、message 的形式
Specification query = new BaseSqlQuery()
.append("status", CommonConstant.COMMON_VALID)
.append("auditStatus", AuditStatusEnum.APPROVED)
.append("position_In", positionEnumList);
List<SysUserEntity> userEntityList = userManager.findAll(query);
Or 操作
举个典型的场景,要根据关键字搜索用户,与姓名和手机号都模糊匹配
下面是要根据内容或者标签来搜索题目,那么下面的 sql 为and (content like '%?%' or labels like '%')
Specification<QuestionEntity> contentQuery = new BaseSqlQuery<QuestionEntity>()
.append("content_Like", searchWord);
Specification<QuestionEntity> labelsQuery = new BaseSqlQuery<QuestionEntity>()
.append("labels_Like", searchWord);
Specification<QuestionEntity> allQuery = contentQuery.or(labelsQuery);
query = query.and(allQuery);
2023 年 11 月,其实宇鹏很早在做项目时候,遇到了复杂的查询,就咨询过 TerryQi,我们致力于找到一个优雅的方法,实现优雅的嵌套多条件查询,那么这个目标没有达成,哪位有时间可以解决该问题,可以 call TerryQi,有奖励哦~
课题描述:我们希望达到的效果是 希望通过优雅的.or
.and
操作,实现类似于下面的复杂条件
A and ( B or ( C and D or (E or F)))
客观来说,失败了,由于上述的 or 操作并不优雅,所以在 3.1.3 版本中,对于我们常用的 ( A or B or C) 类单层的 Or 操作,BaseSqlQuery 提供了 or 操作符,满足这个需求,请注意:目前只支持单层的 or 操作,也就是不能嵌套
例子
BaseSqlQuery query = new BaseSqlQuery().append("status", CommonConst.COMMON_VALID);
query.or(new BaseSqlQuery().append("name", "大连").append("cityCode", "106012"));
List<SysProvinceCityEntity> entityList = provinceCityManager.findAll(query);
其含义如下,请注意,or 的含义是 其内部的条件是 or 的
where status = '1' and ( name = '大连' or cityCode = '106012')
Find_In_Set
很多情况下,我们会将多个 id 用逗号分割存为 xxx_ids 字段(varchar 类型),此时如果查找包含 id 的记录,一般用 MySql 的 find_in_set 方法
在 3.0.7 版本中,提供了_FindInSet方法
,懂得自然懂
List<SysNationCodeEntity> entityList = nationCodeManager
.findAll(new BaseSqlQuery().append("code_FindInSet", "11"));
搜索数据范围
所谓 DataScope 是查询数据的范围,在 3.1.7 版本之前,无论 find、query、smartCount 等,默认都是查询没有被软删除的数据,即 deleted_flag='0'的数据,但是一些情况下,是需要查询被删除数据和全量数据的,这个情况下,就需要设置 dataScope,设置方法为
- 获取未软删除的数据
设置 dataScope==DataScopeEnum.NOT_DELETE,请注意不设置默认也是获取未软删除的数据,对应 sql 语句为deleted_flag=0
在 3.1.8 中,针对 findById 方法,也增加了 DataScope 字段,用于查询软删除字段
Specification query = new BaseSqlQuery().setDataScope(DataScopeEnum.NOT_DELETE);
- 获取被软删除的数据
设置 dataScope==DataScopeEnum.IS_DELETED,,对应 sql 语句为deleted_flag=1
Specification query = new BaseSqlQuery().setDataScope(DataScopeEnum.IS_DELETED);
- 获取全部的数据=未软删除数据+软删除数据
设置 dataScope==DataScopeEnum.ALL,,对应 sql 语句为deleted_flag between '0' and '1'
Specification query = new BaseSqlQuery().setDataScope(DataScopeEnum.ALL);
!!!请注意,dataScope 3.1.15 前只能用于 BaseSqlQuery,在 3.1.16 版本后, SmartQuery 也可以使用 dataScope。 但是请注意,默认是获取未软删除的数据,如果要获取软删除数据,则需要手动设置 dataScope 为 DataScopeEnum.IS_DELETED,或者需要获取全部数据,手动设置为 DataScopeEnum.ALL
Manager 用法
请注意,我们的建议是
Controller(->Service)->Manager->Repostory
具体详见JPA 通用,那么对于 Manager,通用方法如下:
方法名 | 说明 | 备注 |
---|---|---|
findOne | 获取一条数据 | 根据条件获取 1 条数据,要求只能映射到 1 条数据 |
findById | 获取一条数据 | 根据 id 获取数据 |
findAll | 获取列表/分页数据 | 送 Pageable 则获取分页数据 |
query | 获取列表 | 同 findAll |
get | 根据 id 获取数据 | |
count | 统计数据 | 获取 count 数据 |
save | 保存数据 | |
saveAll | 批量保存 | |
delete | 删除数据 | |
deleteAll | 批量删除 | 这个批量删除是伪批量删除,本质还是串行模式删除 |
setStatus | 设置状态 | |
setSort | 设置排序 | |
deleteBatch | 批量删除 | 3.0.6 版本新增批量删除,如果删除数据量特别大,可以使使用该方法,支持软删除和硬删除 |
findFirstOne | 获取一条数据 | 3.0.1 版本新增获取一条数据,本质还是分页 |
锁机制
JPA 已经提供了良好的锁机制,主要有乐观锁、悲观锁两种,当然我们也可以在 redis 中造锁,但是目前来看大部分业务通过框架和数据库即可解决锁问题
下面描述的乐观锁和悲观锁都是 JPA 的锁机制
乐观锁
我们的基础数据模型规范中,全部库表都要有 version 字段,就是用于支持 JPA 的锁机制的 数据库规范
/**
* 版本号
*/
@Version
@Column(name = "version")
protected Integer version;
如果你的库表需要加锁,那么直接在 Entity 上增加上面描述即可。
请注意,乐观锁的确会保障数据不会被污染,如果遇到冲突,则接口直接报错抛出异常,在我的理解里,这个方案是不好的,影响了业务。目前没有理解到哪些业务用乐观锁合适,一般的钱包、积分类业务都应该使用悲观锁。
悲观锁
在碧桂园系统中,乐观锁出现问题,场景是:
- 用户的积分和活跃时间都记录到了 user 表中
- 用户参与游戏扣减积分
- 写了个拦截器,如果用户调用接口,修改用户的活跃时间(异步方式)
此时特别容易造成冲突,即用户可能同时修改积分和活跃时间
那么原来就是在 UserEntity 上加了@Version 的乐观锁,目测不行,转而使用悲观锁
- repository,增加锁,当然也可以写 SQL 里面带 for update
@Lock(LockModeType.PESSIMISTIC_WRITE)
public SysUserEntity findByUserId(Integer userId);
- manager,最好再包一层,修改用户积分时,套一个事务,然后执行下面方法获取用户信息,进行积分扣减即可
/**
* 带锁查找用户
* @return
*/
public SysUserEntity findByUserIdWithLock(Integer userId){
SysUserEntity userEntity = repository.findByUserId(userId);
return userEntity;
}
请注意,使用悲观锁要慎重,原则上只允许根据主键查询上锁,否则锁的面积太大
分组查询
以往我们的 BaseSqlQuery 中有 append 增加条件和 order 设置排序,那么是没有分组能力的,在 3.0.6 版本中,增加了 groupBy 字句,用于进行分组查询。 那么能够设想到的场景是:如果一个商品有多个分类,分类是由字典值管理的,那么想要获取数据库中所有的分类有哪些,此时可以用到 groupBy 字句
Specification query = new BaseSqlQuery().groupBy("status");
List<SysNationCodeEntity> entityList = nationCodeManager.findAll(query);
log.info("entityList:{}", JSON.toJSONString(entityList));
Assert.assertTrue(true);
上面语句的场景是有一个民族表,想要看到民族表中的 status 有哪些值,翻译成的 sql 语句就是下面,当然你也可以子在 repository 中自定义查询,或者用 distinct 的语法来编写,也都是可以的
SELECT
sysnationc0_.nation_code_id AS nation_c1_0_,
sysnationc0_.created_at AS created_2_0_,
sysnationc0_.created_by AS created_3_0_,
sysnationc0_.deleted_at AS deleted_4_0_,
sysnationc0_.deleted_by AS deleted_5_0_,
sysnationc0_.deleted_flag AS deleted_6_0_,
sysnationc0_.note AS note7_0_,
sysnationc0_.sort AS sort8_0_,
sysnationc0_.STATUS AS status9_0_,
sysnationc0_.updated_at AS updated10_0_,
sysnationc0_.updated_by AS updated11_0_,
sysnationc0_.CODE AS code12_0_,
sysnationc0_.NAME AS name13_0_
FROM
sys_nation_code sysnationc0_
WHERE
1 = 1
GROUP BY
sysnationc0_.STATUS
批量删除
以往,我们的 Manager 和 Repository 中也有 deleteAll 的方法,但是 deleteAll 本质是循环 delete 方法,效率会低一些(当然,我们一般情况下也没有大批量的删除,所以在大多数场景下不存在效率问题)。
在研究 JPA 自定义语句能力时,搂草打兔子,实现了 deleteBatch 方法,主要也是完善框架,如果你的项目中有大批量删除的业务场景,可以体验一下
List<Integer> idList = new ArrayList<>();
idList.add(1);
idList.add(2);
idList.add(3);
nationCodeManager.deleteBatch(idList);
其中 deleteBatch 还给了一个 force 参数,Boolean 类型,代表是否应删除,默认是软删除,硬删除将从数据库中删除
public void deleteBatch(List idList, Boolean force) {
this.getRepository().deleteBatch(idList, force);
}
字段为 json
在实际项目中,库表中部分字段需要使用 json 来实施,例如在烟台城发项目中,需要存储需求方的 json,那么需要
- 引入 json 处理包
pom.xml 加入依赖,下个版本计划这个依赖加到框架中
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>2.4.3</version>
</dependency>
- 编写你映射 json 的数据结构
package com.qrqy.chengfa.data.demander.group;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
import java.io.Serializable;
/**
* @author TerryQi
* @since 2022/8/12 13:56
*/
@Data
public class DemandersJsonDo implements Serializable {
/**需求方id,即幼儿园id"**/
@ApiModelProperty(value = "需求方id ,即幼儿园id", example = "")
private Integer demanderId;
/**需求方名称,即幼儿园名称"**/
@ApiModelProperty(value = "需求方名称,即幼儿园名称", example = "")
private String name;
/**联系人姓名"**/
@ApiModelProperty(value = "联系人姓名", example = "")
private String contactName;
/**联系人电话"**/
@ApiModelProperty(value = "联系人电话", example = "")
private String contactPhoneNumber;
/**省"**/
@ApiModelProperty(value = "省", example = "")
private String province;
/**市"**/
@ApiModelProperty(value = "市", example = "")
private String city;
/**区"**/
@ApiModelProperty(value = "区", example = "")
private String area;
/**需求方地址"**/
@ApiModelProperty(value = "需求方地址", example = "")
private String address;
}
- Entity 中实施一下,demandersJson 这个字段就是 json 的字段
package com.qrqy.chengfa.dao.entity;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.qrqy.chengfa.data.demander.group.DemandersJsonDo;
import com.qrqy.chengfa.enums.purchase.task.PurchaseTaskStatusEnum;
import com.qrqy.developer.common.entity.CommonEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* @author : jCoder
* @date : 2022-08-12 11:16
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "cf_purchase_sub_task")
@EntityListeners(AuditingEntityListener.class)
public class CfPurchaseSubTaskEntity extends CommonEntity implements Serializable {
private static final long serialVersionUID = 7406912254492452506L;
/**
* PK
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "purchase_sub_task_id")
private Integer purchaseSubTaskId;
/**
* 采购任务id
*/
@Column(name = "purchase_task_id")
private Integer purchaseTaskId;
/**
* 子任务名称
*/
@Column(name = "sub_task_name")
private String subTaskName;
@Column(name = "batch_no")
private String batchNo;
/**
* 需求方分组id
*/
@Column(name = "demander_group_id")
private Integer demanderGroupId;
/**
* 需求方ids,多个id用逗号分割
*/
@Column(name = "demander_ids")
private String demanderIds;
@Column(name = "demander_group_name")
private String demanderGroupName;
@Type(type = "json")
@Column(name = "demanders_json",columnDefinition = "json")
private List<DemandersJsonDo> demandersJson;
/**
* 报名开始时间
*/
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Column(name = "sign_up_start_at")
private LocalDateTime signUpStartAt;
/**
* 报名结束时间
*/
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Column(name = "sign_up_end_at")
private LocalDateTime signUpEndAt;
/**
* 任务状态,NOT_PUBLISH:未发布;HAS_PUBLISHED:已发布;FINISHED:已结束
*/
@Column(name = "task_status")
@Enumerated(EnumType.STRING)
private PurchaseTaskStatusEnum taskStatus = PurchaseTaskStatusEnum.NOT_PUBLISH;
/**
* 需要商品总数量
*/
@Column(name = "total_product_num")
private Integer totalProductNum;
/**
* 需要商品总价格
*/
@Column(name = "total_price")
private BigDecimal totalPrice;
}
JPA 审计
JPA 审计是自动向库表中添加 createdBy、createdAt、updatedBy、updatedAt 等信息,目前 JPA 审计功能放在框架中。
背景
以往,根据我们的数据库模型定义,用户 id 都保存为 Integer 类型,Entity 直接集成 CommunitEntity 即可。在 2022 年 9 月实施阜新招商项目时,使用合作方团队的 OA 进行人员和组织架构的管理,合作方团队用户 id、组织架构 id 都设置为 String 类型,此时与我们的数据模型定义不一致,为了解决该问题,升级框架到 3.0.5 版本。
Community2Entity
Community2Entity 在原有 CommunityEntity 的基础上增加了泛型。
@Data
@MappedSuperclass
public abstract class Common2Entity<ID> {
/**
* 状态,0:失效,1:有效,主要考虑兼容性
*/
@Column(name = "status")
protected String status = "1";
/**
* 备注
*/
@Column(name = "note")
protected String note;
/**
* 排序
*/
@Column(name = "sort")
protected Integer sort = 99;
/**
* 删除标记,0:未删除;1:已删除
*/
@Column(name = "deleted_flag")
protected String deletedFlag = "0";
/**
* 创建用户
*/
@CreatedBy
@Column(name = "created_by")
protected ID createdBy;
/**
* 创建时间戳
*/
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
@CreatedDate
@Column(name = "created_at")
protected LocalDateTime createdAt;
/**
* 更新用户
*/
@LastModifiedBy
@Column(name = "updated_by")
protected ID updatedBy;
/**
* 更新时间戳
*/
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
@LastModifiedDate
@Column(name = "updated_at")
protected LocalDateTime updatedAt;
/**
* 删除用户
*/
@Column(name = "deleted_by")
protected ID deletedBy;
/**
* 删除时间戳
*/
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Column(name = "deleted_at")
protected LocalDateTime deletedAt;
}
那么,在使用时,Entity 可以定义主键的类型。
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "sys_nation_code")
@EntityListeners(AuditingEntityListener.class)
public class SysNationCodeEntity extends Common2Entity<Integer> implements Serializable {
private static final long serialVersionUID = 432550535284913460L;
/**
* PK
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "nation_code_id")
private Integer nationCodeId;
/**
* 民族名称
*/
@Column(name = "name")
private String name;
/**
* 民族编号
*/
@Column(name = "code")
private String code;
/**
* 状态,1:有效;0:无效
*/
@Column(name = "status")
private String status;
}
UserLoginDo 有需要调整
其中,在各个项目中的 UserLoginDo 也应该相应调整一下
@Data
public class UserLoginDo<ID> implements IBaseUserDetails<ID>, Serializable {
private static final long serialVersionUID = 5900016730059591925L;
/**
* 主键
*/
private ID userId;
/**
* 预留id,一般为hash值
*/
private String uuid;
/**
* 昵称
*/
private String nickName;
/**
* 真实名称
*/
private String realName;
/**
* 头像图片
*/
private String avatar;
/**
* 手机号
*/
private String phoneNumber;
/**
* 用户签名
*/
private String sign;
/**
* 用户性别;UNKNOWN:保密;MALE:男性;FEMALE:女性;
*/
private String gender;
/**
* 国家
*/
private String country;
/**
* 省份
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 地址
*/
private String address;
/**
* 区
*/
private String area;
/**
* 生日
*/
private LocalDate birthday;
/**
* 语言
*/
private String language;
/**
* 用户状态
*/
private String status;
/**
* token信息,带Bearer头
*/
private String token;
/**
* token信息,带Bearer头
*/
private List<RoleDo> roleList;
/**
* 授权信息
*/
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (roleList==null){
return new ArrayList<>();
}
return roleList.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleId())).collect(Collectors.toList());
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return this.realName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
日志审计代码
SpringSecurityAuditorAware.java
就是日志审计的代码,目前在框架中。
@Component
@Slf4j
public class SpringSecurityAuditorAware implements AuditorAware<Object> {
@Override
public Optional<Object> getCurrentAuditor() {
try {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Object userId;
if (principal instanceof IBaseUserDetails) {
userId = ((IBaseUserDetails<Object>) principal).getUserId();
} else {
userId = null;
}
return Optional.ofNullable(userId);
} catch (NullPointerException e) {
return Optional.empty();
}
}
}