数据字典模块
需求
何为数据字典?数据字典就是管理系统常用的分类数据或者一些固定数据,例如:省市区三级联动数据、民族数据、行业数据、学历数据等,由于该系统大量使用这种数据,所以我们要做一个数据管理方便管理系统数据,一般系统基本都会做数据管理。
效果:
表结构
parent_id:上级id,通过id与parent_id构建上下级关系,例如:我们要获取所有行业数据,那么只需要查询parent_id=20000的数据
name:名称,例如:填写用户信息,我们要select标签选择民族,“汉族”就是数据字典的名称
value:值,例如:填写用户信息,我们要select标签选择民族,“1”(汉族的标识)就是数据字典的值
dict_code:编码,编码是我们自定义的,全局唯一,例如:我们要获取行业数据,我们可以通过parent_id获取,但是parent_id是不确定的,所以我们可以根据编码来获取行业数据
搭建环境
后端
配置文件
server:
port: 8202 # 服务端口
spring:
application:
name: service-cmn # 微服务名称
profiles:
active: dev # 设置为开发环境
datasource: # 配置数据源
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/yygh_cmn?characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
#返回json的全局时间格式
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
redis:
host: 192.168.241.130 # ip地址
port: 6379 # 端口号
database: 0
timeout: 1800000 # 超时时间
lettuce:
pool:
max-active: 20
max-wait: -1 # 最大阻塞等待时间(负数表示没限制)
max-idle: 5
min-idle: 0
mybatis-plus: # mybatis-plus日志
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:com/atguigu/hosp/yygh/mapper/xml/*.xml
配置类扫描mapper接口
@MapperScan(basePackages = "com.atguigu.yygh.cmn.mapper")
@Configuration
public class ServiceCmnConfig {
// 分页插件
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
根据id查询数据
由于前端组件element-ui需要以树形结构显示,而且每个需要使用一个字段hasChildren表示是否有孩子。
实体类
数据字典实体类,注意:一:id需要自动进行设置,所以不需要继承父类实体,二:hasChildren字段需要设置@TableField(exist = false),因为在表中没有此字段。
@Data
@ApiModel(description = "数据字典")
@TableName("dict")
public class Dict {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "id")
private Long id;
@ApiModelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField("create_time")
private Date createTime;
@ApiModelProperty(value = "更新时间")
@TableField("update_time")
private Date updateTime;
@ApiModelProperty(value = "逻辑删除(1:已删除,0:未删除)")
@TableLogic
@TableField("is_deleted")
private Integer isDeleted;
@ApiModelProperty(value = "其他参数")
@TableField(exist = false)
private Map<String,Object> param = new HashMap<>();
@ApiModelProperty(value = "上级id")
@TableField("parent_id")
private Long parentId;
@ApiModelProperty(value = "名称")
@TableField("name")
private String name;
@ApiModelProperty(value = "值")
@TableField("value")
private String value;
@ApiModelProperty(value = "编码")
@TableField("dict_code")
private String dictCode;
@ApiModelProperty(value = "是否包含子节点")
@TableField(exist = false)
private boolean hasChildren;
}
Controller层
@Api(tags = "数据字典管理")
@RestController
@RequestMapping("/admin/cmn/dict")
@CrossOrigin
public class CmnController {
@Autowired
private CmnSetService cmnSetService;
// 根据id查询数据列表
@GetMapping("findChildData/{id}")
public Result findChildData(@PathVariable("id") Long id) {
List<Dict> list = cmnSetService.findChildDataById(id);
return Result.ok(list);
}
}
Service层
接口
```java public interface CmnSetService extends IService{ List
findChildDataById(Long id);
}
- 实现类,**根据parent_id获取字段后,还需要判断该条信息是否有子孩子,从而来设置hasChildren字段**
```java
@Service
public class CmnServiceImpl extends ServiceImpl<CmnMapper, Dict> implements CmnSetService {
@Autowired
private CmnMapper cmnMapper;
@Override
@Cacheable(value = "dict", keyGenerator = "keyGenerator") // 缓存在redis中
public List<Dict> findChildDataById(Long id) {
// 封装条件
QueryWrapper<Dict> wrapper = new QueryWrapper<>();
wrapper.eq("parent_id", id);
List<Dict> list = cmnMapper.selectList(wrapper);
for (Dict dict : list) {
boolean hasChildren = hasChildren(dict.getId());
dict.setHasChildren(hasChildren);
}
return list;
}
// 根据id判断是否有子孩子
public boolean hasChildren(Long id) {
QueryWrapper<Dict> wrapper = new QueryWrapper<>();
wrapper.eq("parent_id", id);
int count = cmnMapper.selectCount(wrapper);
return count > 0;
}
}
导入导出字典信息文件
EasyExcel介绍
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,能够原本一个3M的excel用POI sax依然需要100M左右内存降低到几M,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式。在上层做了模型转换的封装,让使用者更加简单方便。
EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。
文档链接🔗
导出示例🔗
导入示例🔗
添加依赖
说明:我们已经在yygh-parent中的pom.xml中添加了所有依赖管理
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
导入导出需要定义对象,对象上需要引用easyexcel标签,所以model模块需要引入,scope:provided
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<scope>provided </scope>
</dependency>
导入导出我们会把它封装成工具类,放在common-util中,所有模块公用,所以该模块也得引入
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
导出实体
与Excel的列字段对应
@Data
public class DictEeVo {
@ExcelProperty(value = "id" ,index = 0) // 此时不设置自增,所以不用继承
private Long id;
@ExcelProperty(value = "上级id" ,index = 1)
private Long parentId;
@ExcelProperty(value = "名称" ,index = 2)
private String name;
@ExcelProperty(value = "值" ,index = 3)
private String value;
@ExcelProperty(value = "编码" ,index = 4)
private String dictCode;
}
Controller
导入需要接收MultipartFile类型文件,导出需要HttpServletResponse参数。
// 导出数据
@GetMapping("exportData")
public void exportData(HttpServletResponse response) {
cmnSetService.exportData(response);
}
// 导入数据
@PostMapping("importData")
public Result importData(MultipartFile file) {
cmnSetService.importData(file);
return Result.ok();
}
Service层实现
导出,思路为:查询数据库将数据赋值给VO对象,放入List中使用EasyExcel的write方法进行导出。
@Override
public void exportData(HttpServletResponse response) {
try {
// 设置参数
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("数据字典", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
List<DictEeVo> dictVOList = new ArrayList<>();
List<Dict> dictPOList = cmnMapper.selectList(null);
for (Dict dictPO : dictPOList) {
DictEeVo dictEeVO = new DictEeVo();
BeanUtils.copyProperties(dictPO, dictEeVO);
dictVOList.add(dictEeVO);
}
// 写入excel
EasyExcel.write(response.getOutputStream(), DictEeVo.class).sheet("数据字典").doWrite(dictVOList);
} catch (IOException e) {
e.printStackTrace();
}
导入
导入需要创建监听器,注意监听器不能被spring管理(官网示例说明),使用spring的对象时需要以构造的方式注入,而不是@Autowired。
EasyExcel使用的时一行一行的读取,所以将VO对象转化为PO对象插入操作数据库。
实现类
注意监听器需要传入baseMapper作为参数,来操作数据库。public class DictListener extends AnalysisEventListener<DictEeVo> {
private CmnMapper cmnMapper;
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param cmnMapper
*/
public DictListener(CmnMapper cmnMapper) {
this.cmnMapper = cmnMapper;
}
/**
* 这个每一条数据解析都会来调用
*
* @param dictEeVo
* @param analysisContext
*/
@Override
public void invoke(DictEeVo dictEeVo, AnalysisContext analysisContext) {
Dict dict = new Dict();
BeanUtils.copyProperties(dictEeVo, dict);
cmnMapper.insert(dict);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
@Override
public void importData(MultipartFile file) {
try {
EasyExcel.read(file.getInputStream(), DictEeVo.class, new DictListener(cmnMapper)).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
}
Spring Cache + Redis 缓存数据
Spring Cache 是一个非常优秀的缓存组件。自Spring 3.1起,提供了类似于@Transactional注解事务的注解Cache支持,且提供了Cache抽象,方便切换各种底层Cache(如:redis)
使用Spring Cache的好处:
1,提供基本的Cache抽象,方便切换各种底层Cache;
2,通过注解Cache可以实现类似于事务一样,缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码就可以完成;
3,提供事务回滚时也自动回滚缓存;
4,支持比较复杂的缓存逻辑;
在不经常修改的数据可以使用缓存缓解数据库压力。
导入依赖
因为缓存也是公共使用,所有的service模块都有可能使用缓存,所以我们把依赖与部分配置加在service-util模块,这样其他service模块都可以使用了
!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
配置类
此配置主要是定义缓存的规则。
注意:需要注入容器,和开启缓存。
@EnableCaching:标记注解 @EnableCaching,开启缓存,并配置Redis缓存管理器。@EnableCaching 注释触发后置处理器, 检查每一个Spring bean 的 public 方法是否存在缓存注解。如果找到这样的一个注释, 自动创建一个代理拦截方法调用和处理相应的缓存行为。
@Configuration
@EnableCaching
public class RedisConfig {
/**
* 自定义key规则
*
* @return
*/
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
/**
* 设置RedisTemplate规则
*
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 序列号key value
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 设置CacheManager缓存规则
*
* @param factory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
yml配置redis
此时在使用redis缓存的工程需要配置redis
spring:
redis:
host: 192.168.241.130 # ip地址
port: 6379 # 端口号
database: 0
timeout: 1800000 # 超时时间
lettuce:
pool:
max-active: 20
max-wait: -1 # 最大阻塞等待时间(负数表示没限制)
max-idle: 5
min-idle: 0
使用注解实现缓存
常用缓存注解
- @Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。 - @CachePut
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。 - @CacheEvict
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上。
实际项目使用:
查询中添加注解,此时查询第二次就在redis中查询
@Override
@Cacheable(value = "dict", keyGenerator = "keyGenerator") // 缓存在redis中
public List<Dict> findChildDataById(Long id) {
}
导入文件后需要清除缓存
@Override
@CacheEvict(value = "dict", allEntries = true) // 方法调用后清空所有缓存
public void importData(MultipartFile file) {
}
前端
定义api接口
导入导出,点击及发送请求
import request from "@/utils/request";
const api_name = '/admin/cmn/dict'
export default {
// 查询所有数据字典
getAllDict(id) {
return request({
url: `${api_name}/findChildData/${id}`,
method: 'get'
})
}
}
页面显示
<template>
<div class="app-container">
<div class="el-toolbar">
<div class="el-toolbar-body" style="justify-content: flex-start;">
<el-button type="text" @click="exportData"><i class="fa fa-plus"/> 导出</el-button>
<el-button type="text" @click="importData"><i class="fa fa-plus"/> 导入</el-button>
</div>
</div>
<el-dialog title="导入" :visible.sync="dialogImportVisible" width="480px">
<el-form label-position="right" label-width="170px">
<el-form-item label="文件">
<el-upload
:multiple="false"
:on-success="onUploadSuccess"
:action="'http://localhost:8202/admin/cmn/dict/importData'"
class="upload-demo">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传excel文件,且不超过500kb</div>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogImportVisible = false">
取消
</el-button>
</div>
</el-dialog>
<el-table
:data="list"
style="width: 100%"
row-key="id"
border
lazy
:load="getChildrens"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column label="名称" width="230" align="left">
<template slot-scope="scope">
<span>{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="编码" width="220">
<template slot-scope="{row}">
{{ row.dictCode }}
</template>
</el-table-column>
<el-table-column label="值" width="230" align="left">
<template slot-scope="scope">
<span>{{ scope.row.value }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center">
<template slot-scope="scope">
<span>{{ scope.row.createTime }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
初始化变量
页面渲染前查询id为1的数据,id为1时所有分类
data() {
return {
list: [], //数据字典列表数组
listLoading: true,
dialogImportVisible: false // 不显示弹框
}
},
created() {
this.getDictList(1);
},
调用方法
查询数据的方法
点击下翻箭头,此时element-ui可以自动调用getChildrens递归查询。需要修改element-ui版本为2.12.0或以上。// 查询所有数据字典分类
getDictList(id) {
dictApi.getAllDict(id)
.then((response) => {
this.list = response.data;
})
.catch((error) => {
this.$message({
type: "error",
message: "查询成功",
});
})
},
// 点击箭头后,递归查询孩子
getChildrens(tree, treeNode, resolve) {
dictApi.getAllDict(tree.id).then(response => {
resolve(response.data);
})
},
导入导出的方法
// 导出文档方法
exportData() {
this.$confirm('此操作将导出数据字典(Excel格式),是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { //确定执行then方法
window.location.href = 'http://localhost:8202/admin/cmn/dict/exportData';
})
},
// 显示上传框
importData() {
this.dialogImportVisible = true;
},
// 导入文档
onUploadSuccess(response, file) {
this.$message({
type: "success",
message: "上传成功",
});
this.dialogImportVisible = false;
this.fetchData();
}
}