老家通常指资本不发达的2、3线城市,在这样的地方,几乎所有的和软件相关的企业都是国企的附庸。而国企的业务,更多是数据的采集、监控、统计以及流程管理,这与北上广深杭这种一线的互联网环境迥然不同。
国企的业务,通常分2类,一类是在原有系统上迭代开发,这种项目数据和表是既有的,并且可能表设计没有统一的规范,时间、枚举等字段常常放飞自我;一类是做新项目,包装的十分高大上,但其实就是表很多的增删改查,既对效率没有很高的要求,也不牵扯复杂的并发与算法逻辑,稍微费点功夫的可能是节点间的流程控制与数据的导入导出。本篇的设计聚焦第二种。
在这种大背景下,我们更应该关注的是如何快速开发系统;如何让新来的员工更快的融入团队;如何更快的去定位BUG;让领导放心,告(hu)诉(you)他们,说我们有较强的数据安全措施。而对于我们自己而言,更快的做完开发,一来降低项目的风险,二来可以腾出时间摸(xue)鱼(xi),何乐而不为呢。
在老家的环境中,业务往往面向领导开发。领导可能常常有新鲜的想法,而老家的产品经理往往机不强势,几乎没什么自己的主见。而老家的开发,往往是瀑布式的。产品先用axture画一份没有任何说明文字的文档,而后程序员稀里糊涂的设计好数据库、而后生成代码、之后开发业务。这样一来,在开发过程中,发现数据库表或字段缺失或设计的不合理,那是十分普遍的。
如果使用传统3范式设计数据库,使用mybatis的xml处理表连接join。如果这个时候某些字段发生改变,或者某个实体加点字段,那和他连接的实体的xml中的sql全得改一次,这可真令人头疼。有的时候,领导心血来潮把mysql换postgre,那就更崩溃了。
所以不需要写sql的开发方式就呼之欲出,如果更改了字段,那就直接在实体里改就好了。
对于类型字段的存储,可以用整型、也可以用字符串、也可以直接使用枚举存储。但数据库里又没有枚举,那怎么办呢。
嗯,在全局的配置中处理一下,前端与服务端交流用字符串,后端与数据库交流使用整型,在代码中直接使用枚举就行了。
为什么我强调使用枚举呢,试想下,如果有的程序员在枚举的存储和判断时,放飞自我,直接在代码里写魔法值。当出现BUG时,大家就会非常的开心。当然也有人说,定义一个枚举的全局变量文件不就行了么。但当别人接手代码时,看到实体中的类型字段使用的是Integer,有可能还没有注释,那也会心中万马奔腾吧~~~
总会有人担心,我把数据删掉找不回来怎么办?既然这样,我们干脆不去删数据,在数据库表中定义一个象征是否被删除的字段,比如叫is_del。如果该条数据被删了,就置为1,否则为0。
但这样做,有一个大坑大家一定要注意,那就是伪删除和数据库表的唯一索引是不兼容的。如果表中设置了唯一索引,某个数据删掉了,再想加入同样关键字的数据就很麻烦了……。
所以,干脆不设计唯一索引。把发现问题的人解决掉,往往就没有问题了~~~
我们希望ORM框架,可以直接调用delete接口,就帮我们解决这些问题,幸运的是,mybatis-plus是支持的。
国企的数据中,通常希望记录这条数据什么时候创建修改的,什么时候创建修改的,由谁创建修改的,由哪个Ip创建修改的。而我们开发时,不希望在代码中关心这些内容。
虽说面向企业的开发中,很少遇到并发问题,因为同时在线用户量非常小。但总有人会担心这种问题,害怕两个人同时修改一份数据,导致相互覆盖,产生不好定位的BUG。
这种业务,正常来做需要用redis对数据上锁,当用户打开修改页面后存锁信息,用户完成编辑后释放锁信息。如果有人试图打开特定数据的编辑页面,先查看该数据是否被上锁。如果被上锁则直接抛错。
但实际情况,并不会出现这种两个人修改同一数据的情况,或者极少出现。我们不如简单的做,在实体上加一个版本号,每次修改时验证新的版本号是否比上一次多1,如果不是就报错。所幸,这个功能mybatis-plus中也是有的。
类别 | 类名 | 注释 |
---|---|---|
数据库表 | 模块前缀_实体名 | 下划线式 |
数据库表映射实体po | 实体名Entity | 驼峰命名 |
数据定义实体dto | 实体名DTO | 用于接受前端实体,通常不放id |
返回值视图实体vo | 实体名VO | 返回前端的实体 |
mapper | 实体名Mapper | mybatis-plus生成的mapper |
daoService接口 | I实体名DbService | mybatis-plus生成的service接口 |
daoService实现 | 实体名DbServiceImpl | mybatis-plus生成的service实现 |
service接口 | I实体名Service | 真实的service,对应业务模块 |
service实现 | 实体名ServiceImpl | 真实的service实现,可以注入多个daoService |
我个人观点,mybatis-plus生成的service,其实就是对应于JAP的JpaRepository,故放在dao层,命名加Dao前缀。
对于方法命名,我更倾向于使用JPA的风格,比如findByXXXAndXXX()。对于多条件的分页,直接使用page()。
正常的service对应从业务角度的模块划分,原则上service不允许相互注入,但service可以注入多个daoService
通常,在controller中做前端数据的校验,并调用service层,执行特定的逻辑。
使用lombok的@Data注解,不必在实体中写get,set方法
在controller或service上加上@RequiredArgsConstructor,把需要注入的组件定义成final的
这样lombok就会自动生成带有所有final字段的构造函数。
十分不推荐使用@Autowired,因为@Autowired不保证一定注入成功,可能造成莫名其妙的bug。
使用构造函数注入,还能间接指定各组建初始化的顺序。
使用@Slf4j注解,便于打log
对于系统生成的实体,不需要记录是谁创建修改的,其基类代码如下。
@Data
public class SysBaseEntity {
@Schema(title = "主键")
@TableId(type = IdType.ASSIGN_ID)
protected Long id;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.INSERT)
@Schema(title = "创建时间")
protected LocalDateTime createTime;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.UPDATE)
@Schema(title = "修改时间")
protected LocalDateTime modifyTime;
@Schema(title = "版本")
@Version
protected Integer version;
public void createInit() {
id = IdWorker.getId();
// 靠全局配置填充
createTime = null;
modifyTime = null;
version = 0;
}
public void updateInit() {
modifyTime = null;
}
}
对于用户创建的实体,需要记录操作者的信息,其代码如下:
@Data
public class BaseEntity extends SysBaseEntity{
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.INSERT)
@Schema(title = "创建用户Id")
protected Long createBy;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.INSERT)
@Schema(title = "创建用户昵称")
protected String createName;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.INSERT)
@Schema(title = "创建者Ip")
protected String createIp;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.UPDATE)
@Schema(title = "修改用户Id")
protected Long modifyBy;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.UPDATE)
@Schema(title = "修改用户昵称")
protected String modifyName;
@JSONField(serialize = false, deserialize = false)
@TableField(fill = FieldFill.UPDATE)
@Schema(title = "创建者Ip")
protected String modifyIp;
@JSONField(serialize = false, deserialize = false)
@TableLogic
@Schema(title = "是否删除")
protected boolean del;
@Override
public void createInit() {
super.createInit();
// 靠全局配置填充
createBy = null;
createName = null;
createIp = null;
modifyBy = null;
modifyName = null;
modifyIp = null;
del = false;
}
@Override
public void updateInit() {
super.updateInit();
modifyBy = null;
modifyName = null;
modifyIp = null;
}
}
在实际开发中,我们通常不会直接使用mybatis-plus提供的通用DbService。我们会自己写一个类继承mybatis-plus的类,在里面增加一些共通的接口和实现。本章添加一些方便返回Vo的分页和列表接口。
IZfDbService:
public interface IZfDbService<T> extends IService<T> {
<VO>Page<VO> page(IPage page, Wrapper<T> queryWrapper, Class<VO> cls);
<VO>Page<VO> page(IPage page, Wrapper<T> queryWrapper, Class<VO> cls, Function<T,VO> convertor);
<VO> List<VO> list(Wrapper<T> queryWrapper, Class<VO> cls);
<VO> List<VO> list(Wrapper<T> queryWrapper, Class<VO> cls, Function<T,VO> convertor);
T findOne(Wrapper<T> queryWrapper);
boolean existBy(Wrapper<T> queryWrapper);
}
ZfDbServiceImpl:
public class ZfDbServiceImpl<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> implements IZfDbService<T> {
@Override
public <VO> Page<VO> page(IPage page, Wrapper<T> queryWrapper, Class<VO> cls) {
IPage<T> pageData = super.page(page,queryWrapper);
Page<VO> pageVoData = new Page<VO>(pageData.getCurrent(),pageData.getSize());
pageVoData.setTotal(pageData.getTotal());
pageVoData.setPages(pageData.getPages());
List<VO> voList = DtoEntityUtil.trans(pageData.getRecords(),cls);
pageVoData.setRecords(voList);
return pageVoData;
}
@Override
public <VO> Page<VO> page(IPage page, Wrapper<T> queryWrapper, Class<VO> cls, Function<T, VO> convertor) {
IPage<T> pageData = super.page(page,queryWrapper);
Page<VO> pageVoData = new Page<VO>(pageData.getCurrent(),pageData.getSize());
pageVoData.setTotal(pageData.getTotal());
pageVoData.setPages(pageData.getPages());
List<VO> voList = new ArrayList<>(pageData.getRecords().size());
for(T data : pageData.getRecords()){
VO voData = convertor.apply(data);
voList.add(voData);
}
pageVoData.setRecords(voList);
return pageVoData;
}
@Override
public <VO> List<VO> list(Wrapper<T> queryWrapper, Class<VO> cls) {
List<T> listData = super.list(queryWrapper);
List<VO> listVoData = DtoEntityUtil.trans(listData,cls);
return listVoData;
}
@Override
public <VO> List<VO> list(Wrapper<T> queryWrapper, Class<VO> cls, Function<T, VO> convertor) {
List<T> listData = super.list(queryWrapper);
List<VO> listVoData = new ArrayList<>();
for(T data : listData){
VO voData = convertor.apply(data);
listVoData.add(voData);
}
return listVoData;
}
@Override
public T findOne(Wrapper<T> queryWrapper) {
List<T> listData = super.list(queryWrapper);
if(CollectionUtils.isEmpty(listData)){
return null;
}
return listData.get(0);
}
@Override
public boolean existBy(Wrapper<T> queryWrapper) {
long cnt = baseMapper.selectCount(queryWrapper);
return cnt > 0;
}
}
在实际项目中,有些数据创建好后几乎不可能改变,比如用户名。但对于这种不会改变的信息,没必要每次再查一遍,这样不但执行效率低,些起来还很麻烦。
对于一些大屏需求,常常会设计极其复杂的表连接和聚合查询。这个时候定义实体已经完全没有意义。
这时候我们不妨封装JDBC,建一个查询的表,存储查询的大sql。可以结合freemark,使用xml使sql动态化。服务只需调用特定的键,就会执行对应的sql。sql里写的什么样的值,就直接返回前端就好。
这个配置类负责mybatis-plus的分页和乐观锁
@AllArgsConstructor
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页
PaginationInnerInterceptor mysqlInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
interceptor.addInnerInterceptor(mysqlInnerInterceptor);
// 乐观锁
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
这个基类负责mybatis-plus的自动填充功能,在具体的业务服务器写一个类继承他便可使用
@Slf4j
public abstract class BaseMetaObjectHandler 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 createByNameStr = "createByName";
protected String createIpStr = "createIp";
protected String modifyTimeStr = "modifyTime";
protected String modifyByStr= "modifyBy";
protected String modifyByNameStr = "modifyByName";
protected String modifyIpStr = "modifyIp";
protected void insertCreateTime(MetaObject pMetaObject){
if(pMetaObject.hasSetter(createTimeStr)){
Object orgCreateTime = pMetaObject.getValue(createTimeStr);
if(null == orgCreateTime){
this.strictInsertFill(pMetaObject,createTimeStr,()-> LocalDateTime.now(),LocalDateTime.class);
}
}
}
protected abstract void insertCreateBy(MetaObject pMetaObject);
protected abstract void insertCreateByName(MetaObject pMetaObject);
protected abstract void insertCreateByIp(MetaObject pMetaObject);
abstract protected void otherInsertFill(MetaObject pMetaObject);
abstract protected boolean hasTokenObject();
@Override
public void insertFill(MetaObject pMetaObject) {
insertCreateTime(pMetaObject);
if(hasTokenObject()){
try{
insertCreateBy(pMetaObject);
insertCreateByName(pMetaObject);
insertCreateByIp(pMetaObject);
}catch (Exception ex){
log.error("insertFill 出现异常",ex);
}
}else{
this.strictInsertFill(pMetaObject,createByStr,()->SYS_ID,Long.class);
this.strictInsertFill(pMetaObject,createByNameStr,()->SYS_NAME,String.class);
this.strictInsertFill(pMetaObject,createIpStr,()->SYS_IP,String.class);
}
otherInsertFill(pMetaObject);
}
protected void updateModifyTime(MetaObject pMetaObject){
if(pMetaObject.hasSetter(modifyTimeStr)){
Object orgModifyTime = pMetaObject.getValue(modifyTimeStr);
if(null == orgModifyTime){
this.strictUpdateFill(pMetaObject,modifyTimeStr,()-> LocalDateTime.now(),LocalDateTime.class);
}
}
}
protected abstract void updateModifyBy(MetaObject pMetaObject);
protected abstract void updateModifyByName(MetaObject pMetaObject);
protected abstract void updateModifyByIp(MetaObject pMetaObject);
abstract protected void otherUpdateFill(MetaObject pMetaObject);
@Override
public void updateFill(MetaObject pMetaObject) {
updateModifyTime(pMetaObject);
if(hasTokenObject()){
try{
updateModifyBy(pMetaObject);
updateModifyByName(pMetaObject);
updateModifyTime(pMetaObject);
updateModifyByIp(pMetaObject);
}catch (Exception ex){
log.error("updateFill 出现异常",ex);
}
}else{
this.strictUpdateFill(pMetaObject,modifyByStr,()->SYS_ID,Long.class);
this.strictUpdateFill(pMetaObject,modifyByNameStr,()->SYS_NAME,String.class);
this.strictUpdateFill(pMetaObject,modifyIpStr,()->SYS_IP,String.class);
}
otherUpdateFill(pMetaObject);
}
}
由于本期没有接鉴权,直接把hasTokenObject返回false,其他函数空白即可
@Component
public class MetaObjectHandler extends BaseMetaObjectHandler {
@Override
protected boolean hasTokenObject() {
return false;
}
}
在第一讲中的DtoUtil下,添加如下代码
public class DbDtoEntityUtil extends DtoEntityUtil {
/**
* 用于新增
*
* @param pDto dto实体
* @param clazz po的class
* @return 初始化好的po
*/
public static <D, E> E dtoToPo(D pDto, Class<E> clazz) {
if (pDto == null) {
return null;
}
E result = mapper.map(pDto, clazz);
if(result instanceof BaseEntity){
((BaseEntity) result).createInit();
}
return result;
}
/**
* 用于更新
*
* @param pDto dto实体
* @param pPo 待更新的po实体
* @param pPoClass po的类
* @return 更新后的po
*/
public static <D, E> E dtoToPo(D pDto, E pPo, Class<E> pPoClass) {
if (pDto == null) {
return null;
}
E result = mapper.map(pPo, pPoClass);
copy(result,pDto);
if(result instanceof BaseEntity){
((BaseEntity) result).updateInit();
}
return result;
}
}
我们可以模拟一个PCR公会战报刀系统。
实体部分:允许创建角色,创建公会,加入公会,公会内统计,公会排名。
游戏部分:允许举办公会战,报刀,尾刀,查看会战状态等。
本节着重讨论数据库相关操作,故业务上只讨论表的创建
在空白处单击右键,选择display preference>>table>>advanced
记得把这个页面的Indexes 和 Comment勾上
而后点击colomn,设置为如图
而后,点击菜单的database,选择generate database,生成数据库文件。
再然后把数据库文件在数据库中执行,就得到我们业务所需的表。
上一集我们已经建好了mysql模块的pom库,我们需要在上期的pom中添加生成代码相关的库
为方便起见,展示所有的pom代码:
<dependencies>
<dependency>
<groupId>indi.zhifa.recipegroupId>
<artifactId>framework-commonartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-extensionartifactId>
dependency>
<dependency>
<groupId>p6spygroupId>
<artifactId>p6spyartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
dependency>
<dependency>
<groupId>org.freemarkergroupId>
<artifactId>freemarkerartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
dependencies>
outputPath中我设置的是我电脑的路径,大家可以自行替换
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_")
.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();
}
}
执行程序,打开dbgen文件夹,把我们不需要的代码删掉,如controller以及生成的xml文件。
然后把该文件夹拖进idea,以便之后组织代码
通常,业务量比较小的时候,把po放一个目录,mapper放一个目录,dbService放一个目录,service放一个目录,这样更体现一种分层开发的思想。理论上上层组件只能调用下层的组件,不可以平级调用或者调用更上层的。在同一层也方便编写拦截器做一些统一处理。
但真是的开发中,业务量往往很大,同一模块还有很多辅助表,如果用这种方式组织代码,就会导致代码的查找极其麻烦,写着写着就写晕的情况时有发生。
故我更推荐把相同模块放入同一个文件夹中。
首先在一个包下建好目录,而后再把这个目录复制到各种包下面:
手动拖改后变成这个样子:
spring:
application:
name: bailan2
pathmatch:
matching-strategy: ANT_PATH_MATCHER
cloud:
nacos:
server-addr: localhost:8848
discovery:
register-enabled: true
datasource:
#数据库配置
#driver-class-name: com.mysql.cj.jdbc.Driver
#平时开发用这个,便于看sql的log
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
#url: jdbc:mysql://localhost:3307/bailan?useSSL=false&useUnicode=true&characterEncoding=utf-8
url: jdbc:p6spy:mysql://localhost:3307/bailan?useSSL=false&useUnicode=true&characterEncoding=utf-8
username: 芝法酱
password: 我才不告诉你
hikari:
# 连接池最大连接数,默认是10
maximum-pool-size: 100
# 最小空闲链接
minimum-idle: 5
# 空闲连接存活最大时间,默认 600000(10分钟)
idle-timeout: 600000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 30000
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟;正在使用的连接永远不会退休,只有在关闭后才会被删除。
max-lifetime: 1800000
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
pool-name: Hikari
swagger:
enable: true
group-name: "业务接口"
api-package: indi.zhifa.recipe.bailan.busy.controller.api
api-regex: "/api/**"
title: "母猪焊接会战管理接口"
description: "母猪焊接会战管理接口"
version: "1.0.0"
name: "芝法酱"
email: "[email protected]"
url: "https://github.com/hataksumo"
mybatis-plus:
typeEnumsPackage: indi.zhifa.recipe.bailan.busy.enums
app:
battle-days: 6
day-hits: 3
page-size: 100
max-union-player-num: 30
logging:
level:
indi.zhifa.recipe.bailan: info
在业务服务的resource下建1个spy.properties
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
deregisterdrivers=true
useprefix=true
excludecategories=info,debug,result,commit,resultset
dateformat=yyyy-MM-dd HH:mm:ss
outagedetection=true
outagedetectioninterval=2