数据字典模块

需求

何为数据字典?数据字典就是管理系统常用的分类数据或者一些固定数据,例如:省市区三级联动数据、民族数据、行业数据、学历数据等,由于该系统大量使用这种数据,所以我们要做一个数据管理方便管理系统数据,一般系统基本都会做数据管理。

效果:

数据字典模块 - 图1

表结构

数据字典模块 - 图2

parent_id:上级id,通过id与parent_id构建上下级关系,例如:我们要获取所有行业数据,那么只需要查询parent_id=20000的数据

name:名称,例如:填写用户信息,我们要select标签选择民族,“汉族”就是数据字典的名称

value:值,例如:填写用户信息,我们要select标签选择民族,“1”(汉族的标识)就是数据字典的值

dict_code:编码,编码是我们自定义的,全局唯一,例如:我们要获取行业数据,我们可以通过parent_id获取,但是parent_id是不确定的,所以我们可以根据编码来获取行业数据

搭建环境

数据字典模块 - 图3

后端

配置文件

  1. server:
  2. port: 8202 # 服务端口
  3. spring:
  4. application:
  5. name: service-cmn # 微服务名称
  6. profiles:
  7. active: dev # 设置为开发环境
  8. datasource: # 配置数据源
  9. driver-class-name: com.mysql.cj.jdbc.Driver
  10. url: jdbc:mysql://localhost:3306/yygh_cmn?characterEncoding=utf-8&serverTimezone=GMT%2B8
  11. username: root
  12. password: root
  13. #返回json的全局时间格式
  14. jackson:
  15. date-format: yyyy-MM-dd HH:mm:ss
  16. time-zone: GMT+8
  17. redis:
  18. host: 192.168.241.130 # ip地址
  19. port: 6379 # 端口号
  20. database: 0
  21. timeout: 1800000 # 超时时间
  22. lettuce:
  23. pool:
  24. max-active: 20
  25. max-wait: -1 # 最大阻塞等待时间(负数表示没限制)
  26. max-idle: 5
  27. min-idle: 0
  28. mybatis-plus: # mybatis-plus日志
  29. configuration:
  30. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  31. mapper-locations: classpath:com/atguigu/hosp/yygh/mapper/xml/*.xml

配置类扫描mapper接口

  1. @MapperScan(basePackages = "com.atguigu.yygh.cmn.mapper")
  2. @Configuration
  3. public class ServiceCmnConfig {
  4. // 分页插件
  5. @Bean
  6. public PaginationInterceptor paginationInterceptor() {
  7. return new PaginationInterceptor();
  8. }
  9. }

根据id查询数据

由于前端组件element-ui需要以树形结构显示,而且每个需要使用一个字段hasChildren表示是否有孩子。

数据字典模块 - 图4

实体类

数据字典实体类,注意:一:id需要自动进行设置,所以不需要继承父类实体,二:hasChildren字段需要设置@TableField(exist = false),因为在表中没有此字段。

  1. @Data
  2. @ApiModel(description = "数据字典")
  3. @TableName("dict")
  4. public class Dict {
  5. private static final long serialVersionUID = 1L;
  6. @ApiModelProperty(value = "id")
  7. private Long id;
  8. @ApiModelProperty(value = "创建时间")
  9. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
  10. @TableField("create_time")
  11. private Date createTime;
  12. @ApiModelProperty(value = "更新时间")
  13. @TableField("update_time")
  14. private Date updateTime;
  15. @ApiModelProperty(value = "逻辑删除(1:已删除,0:未删除)")
  16. @TableLogic
  17. @TableField("is_deleted")
  18. private Integer isDeleted;
  19. @ApiModelProperty(value = "其他参数")
  20. @TableField(exist = false)
  21. private Map<String,Object> param = new HashMap<>();
  22. @ApiModelProperty(value = "上级id")
  23. @TableField("parent_id")
  24. private Long parentId;
  25. @ApiModelProperty(value = "名称")
  26. @TableField("name")
  27. private String name;
  28. @ApiModelProperty(value = "值")
  29. @TableField("value")
  30. private String value;
  31. @ApiModelProperty(value = "编码")
  32. @TableField("dict_code")
  33. private String dictCode;
  34. @ApiModelProperty(value = "是否包含子节点")
  35. @TableField(exist = false)
  36. private boolean hasChildren;
  37. }

Controller层

  1. @Api(tags = "数据字典管理")
  2. @RestController
  3. @RequestMapping("/admin/cmn/dict")
  4. @CrossOrigin
  5. public class CmnController {
  6. @Autowired
  7. private CmnSetService cmnSetService;
  8. // 根据id查询数据列表
  9. @GetMapping("findChildData/{id}")
  10. public Result findChildData(@PathVariable("id") Long id) {
  11. List<Dict> list = cmnSetService.findChildDataById(id);
  12. return Result.ok(list);
  13. }
  14. }

Service层

  • 接口
    ```java public interface CmnSetService extends IService {

    List findChildDataById(Long id);

}

  1. - 实现类,**根据parent_id获取字段后,还需要判断该条信息是否有子孩子,从而来设置hasChildren字段**
  2. ```java
  3. @Service
  4. public class CmnServiceImpl extends ServiceImpl<CmnMapper, Dict> implements CmnSetService {
  5. @Autowired
  6. private CmnMapper cmnMapper;
  7. @Override
  8. @Cacheable(value = "dict", keyGenerator = "keyGenerator") // 缓存在redis中
  9. public List<Dict> findChildDataById(Long id) {
  10. // 封装条件
  11. QueryWrapper<Dict> wrapper = new QueryWrapper<>();
  12. wrapper.eq("parent_id", id);
  13. List<Dict> list = cmnMapper.selectList(wrapper);
  14. for (Dict dict : list) {
  15. boolean hasChildren = hasChildren(dict.getId());
  16. dict.setHasChildren(hasChildren);
  17. }
  18. return list;
  19. }
  20. // 根据id判断是否有子孩子
  21. public boolean hasChildren(Long id) {
  22. QueryWrapper<Dict> wrapper = new QueryWrapper<>();
  23. wrapper.eq("parent_id", id);
  24. int count = cmnMapper.selectCount(wrapper);
  25. return count > 0;
  26. }
  27. }

导入导出字典信息文件

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中添加了所有依赖管理

  1. <dependencies>
  2. <!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
  3. <dependency>
  4. <groupId>com.alibaba</groupId>
  5. <artifactId>easyexcel</artifactId>
  6. <version>2.1.1</version>
  7. </dependency>
  8. </dependencies>

导入导出需要定义对象,对象上需要引用easyexcel标签,所以model模块需要引入,scope:provided

  1. <dependency>
  2. <groupId>com.alibaba</groupId>
  3. <artifactId>easyexcel</artifactId>
  4. <scope>provided </scope>
  5. </dependency>

导入导出我们会把它封装成工具类,放在common-util中,所有模块公用,所以该模块也得引入

  1. <dependency>
  2. <groupId>com.alibaba</groupId>
  3. <artifactId>easyexcel</artifactId>
  4. </dependency>

导出实体

与Excel的列字段对应

  1. @Data
  2. public class DictEeVo {
  3. @ExcelProperty(value = "id" ,index = 0) // 此时不设置自增,所以不用继承
  4. private Long id;
  5. @ExcelProperty(value = "上级id" ,index = 1)
  6. private Long parentId;
  7. @ExcelProperty(value = "名称" ,index = 2)
  8. private String name;
  9. @ExcelProperty(value = "值" ,index = 3)
  10. private String value;
  11. @ExcelProperty(value = "编码" ,index = 4)
  12. private String dictCode;
  13. }

Controller

导入需要接收MultipartFile类型文件,导出需要HttpServletResponse参数。

  1. // 导出数据
  2. @GetMapping("exportData")
  3. public void exportData(HttpServletResponse response) {
  4. cmnSetService.exportData(response);
  5. }
  6. // 导入数据
  7. @PostMapping("importData")
  8. public Result importData(MultipartFile file) {
  9. cmnSetService.importData(file);
  10. return Result.ok();
  11. }

Service层实现

  • 导出,思路为:查询数据库将数据赋值给VO对象,放入List中使用EasyExcel的write方法进行导出。

    1. @Override
    2. public void exportData(HttpServletResponse response) {
    3. try {
    4. // 设置参数
    5. response.setContentType("application/vnd.ms-excel");
    6. response.setCharacterEncoding("utf-8");
    7. // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
    8. String fileName = URLEncoder.encode("数据字典", "UTF-8");
    9. response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
    10. List<DictEeVo> dictVOList = new ArrayList<>();
    11. List<Dict> dictPOList = cmnMapper.selectList(null);
    12. for (Dict dictPO : dictPOList) {
    13. DictEeVo dictEeVO = new DictEeVo();
    14. BeanUtils.copyProperties(dictPO, dictEeVO);
    15. dictVOList.add(dictEeVO);
    16. }
    17. // 写入excel
    18. EasyExcel.write(response.getOutputStream(), DictEeVo.class).sheet("数据字典").doWrite(dictVOList);
    19. } catch (IOException e) {
    20. e.printStackTrace();
    21. }
  • 导入
    导入需要创建监听器,注意监听器不能被spring管理(官网示例说明),使用spring的对象时需要以构造的方式注入,而不是@Autowired。
    EasyExcel使用的时一行一行的读取,所以将VO对象转化为PO对象插入操作数据库。
    实现类
    注意监听器需要传入baseMapper作为参数,来操作数据库。

    1. public class DictListener extends AnalysisEventListener<DictEeVo> {
    2. private CmnMapper cmnMapper;
    3. /**
    4. * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
    5. *
    6. * @param cmnMapper
    7. */
    8. public DictListener(CmnMapper cmnMapper) {
    9. this.cmnMapper = cmnMapper;
    10. }
    11. /**
    12. * 这个每一条数据解析都会来调用
    13. *
    14. * @param dictEeVo
    15. * @param analysisContext
    16. */
    17. @Override
    18. public void invoke(DictEeVo dictEeVo, AnalysisContext analysisContext) {
    19. Dict dict = new Dict();
    20. BeanUtils.copyProperties(dictEeVo, dict);
    21. cmnMapper.insert(dict);
    22. }
    23. @Override
    24. public void doAfterAllAnalysed(AnalysisContext analysisContext) {
    25. }
    26. }
    1. @Override
    2. public void importData(MultipartFile file) {
    3. try {
    4. EasyExcel.read(file.getInputStream(), DictEeVo.class, new DictListener(cmnMapper)).sheet().doRead();
    5. } catch (IOException e) {
    6. e.printStackTrace();
    7. }
    8. }

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模块都可以使用了

  1. !-- redis -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-redis</artifactId>
  5. </dependency>
  6. <!-- spring2.X集成redis所需common-pool2-->
  7. <dependency>
  8. <groupId>org.apache.commons</groupId>
  9. <artifactId>commons-pool2</artifactId>
  10. <version>2.6.0</version>
  11. </dependency>

配置类

此配置主要是定义缓存的规则。

注意:需要注入容器,和开启缓存

@EnableCaching:标记注解 @EnableCaching,开启缓存,并配置Redis缓存管理器。@EnableCaching 注释触发后置处理器, 检查每一个Spring bean 的 public 方法是否存在缓存注解。如果找到这样的一个注释, 自动创建一个代理拦截方法调用和处理相应的缓存行为。

  1. @Configuration
  2. @EnableCaching
  3. public class RedisConfig {
  4. /**
  5. * 自定义key规则
  6. *
  7. * @return
  8. */
  9. @Bean
  10. public KeyGenerator keyGenerator() {
  11. return new KeyGenerator() {
  12. @Override
  13. public Object generate(Object target, Method method, Object... params) {
  14. StringBuilder sb = new StringBuilder();
  15. sb.append(target.getClass().getName());
  16. sb.append(method.getName());
  17. for (Object obj : params) {
  18. sb.append(obj.toString());
  19. }
  20. return sb.toString();
  21. }
  22. };
  23. }
  24. /**
  25. * 设置RedisTemplate规则
  26. *
  27. * @param redisConnectionFactory
  28. * @return
  29. */
  30. @Bean
  31. public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  32. RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
  33. redisTemplate.setConnectionFactory(redisConnectionFactory);
  34. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  35. // 解决查询缓存转换异常的问题
  36. ObjectMapper om = new ObjectMapper();
  37. // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
  38. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  39. // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
  40. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  41. jackson2JsonRedisSerializer.setObjectMapper(om);
  42. // 序列号key value
  43. redisTemplate.setKeySerializer(new StringRedisSerializer());
  44. redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
  45. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
  46. redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
  47. redisTemplate.afterPropertiesSet();
  48. return redisTemplate;
  49. }
  50. /**
  51. * 设置CacheManager缓存规则
  52. *
  53. * @param factory
  54. * @return
  55. */
  56. @Bean
  57. public CacheManager cacheManager(RedisConnectionFactory factory) {
  58. RedisSerializer<String> redisSerializer = new StringRedisSerializer();
  59. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  60. // 解决查询缓存转换异常的问题
  61. ObjectMapper om = new ObjectMapper();
  62. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  63. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  64. jackson2JsonRedisSerializer.setObjectMapper(om);
  65. // 配置序列化(解决乱码的问题),过期时间600秒
  66. RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
  67. .entryTtl(Duration.ofSeconds(600))
  68. .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
  69. .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
  70. .disableCachingNullValues();
  71. RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
  72. .cacheDefaults(config)
  73. .build();
  74. return cacheManager;
  75. }
  76. }

yml配置redis

此时在使用redis缓存的工程需要配置redis

  1. spring:
  2. redis:
  3. host: 192.168.241.130 # ip地址
  4. port: 6379 # 端口号
  5. database: 0
  6. timeout: 1800000 # 超时时间
  7. lettuce:
  8. pool:
  9. max-active: 20
  10. max-wait: -1 # 最大阻塞等待时间(负数表示没限制)
  11. max-idle: 5
  12. min-idle: 0

使用注解实现缓存

常用缓存注解

  • @Cacheable
    根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。
    数据字典模块 - 图5
  • @CachePut
    使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。
    数据字典模块 - 图6
  • @CacheEvict
    使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上。
    数据字典模块 - 图7

实际项目使用:

查询中添加注解,此时查询第二次就在redis中查询

  1. @Override
  2. @Cacheable(value = "dict", keyGenerator = "keyGenerator") // 缓存在redis中
  3. public List<Dict> findChildDataById(Long id) {
  4. }

导入文件后需要清除缓存

  1. @Override
  2. @CacheEvict(value = "dict", allEntries = true) // 方法调用后清空所有缓存
  3. public void importData(MultipartFile file) {
  4. }

前端

定义api接口

导入导出,点击及发送请求

  1. import request from "@/utils/request";
  2. const api_name = '/admin/cmn/dict'
  3. export default {
  4. // 查询所有数据字典
  5. getAllDict(id) {
  6. return request({
  7. url: `${api_name}/findChildData/${id}`,
  8. method: 'get'
  9. })
  10. }
  11. }

页面显示

  1. <template>
  2. <div class="app-container">
  3. <div class="el-toolbar">
  4. <div class="el-toolbar-body" style="justify-content: flex-start;">
  5. <el-button type="text" @click="exportData"><i class="fa fa-plus"/> 导出</el-button>
  6. <el-button type="text" @click="importData"><i class="fa fa-plus"/> 导入</el-button>
  7. </div>
  8. </div>
  9. <el-dialog title="导入" :visible.sync="dialogImportVisible" width="480px">
  10. <el-form label-position="right" label-width="170px">
  11. <el-form-item label="文件">
  12. <el-upload
  13. :multiple="false"
  14. :on-success="onUploadSuccess"
  15. :action="'http://localhost:8202/admin/cmn/dict/importData'"
  16. class="upload-demo">
  17. <el-button size="small" type="primary">点击上传</el-button>
  18. <div slot="tip" class="el-upload__tip">只能上传excel文件,且不超过500kb</div>
  19. </el-upload>
  20. </el-form-item>
  21. </el-form>
  22. <div slot="footer" class="dialog-footer">
  23. <el-button @click="dialogImportVisible = false">
  24. 取消
  25. </el-button>
  26. </div>
  27. </el-dialog>
  28. <el-table
  29. :data="list"
  30. style="width: 100%"
  31. row-key="id"
  32. border
  33. lazy
  34. :load="getChildrens"
  35. :tree-props="{children: 'children', hasChildren: 'hasChildren'}">
  36. <el-table-column label="名称" width="230" align="left">
  37. <template slot-scope="scope">
  38. <span>{{ scope.row.name }}</span>
  39. </template>
  40. </el-table-column>
  41. <el-table-column label="编码" width="220">
  42. <template slot-scope="{row}">
  43. {{ row.dictCode }}
  44. </template>
  45. </el-table-column>
  46. <el-table-column label="值" width="230" align="left">
  47. <template slot-scope="scope">
  48. <span>{{ scope.row.value }}</span>
  49. </template>
  50. </el-table-column>
  51. <el-table-column label="创建时间" align="center">
  52. <template slot-scope="scope">
  53. <span>{{ scope.row.createTime }}</span>
  54. </template>
  55. </el-table-column>
  56. </el-table>
  57. </div>
  58. </template>

初始化变量

页面渲染前查询id为1的数据,id为1时所有分类

  1. data() {
  2. return {
  3. list: [], //数据字典列表数组
  4. listLoading: true,
  5. dialogImportVisible: false // 不显示弹框
  6. }
  7. },
  8. created() {
  9. this.getDictList(1);
  10. },

调用方法

  • 查询数据的方法
    点击下翻箭头,此时element-ui可以自动调用getChildrens递归查询。需要修改element-ui版本为2.12.0或以上。

    1. // 查询所有数据字典分类
    2. getDictList(id) {
    3. dictApi.getAllDict(id)
    4. .then((response) => {
    5. this.list = response.data;
    6. })
    7. .catch((error) => {
    8. this.$message({
    9. type: "error",
    10. message: "查询成功",
    11. });
    12. })
    13. },
    14. // 点击箭头后,递归查询孩子
    15. getChildrens(tree, treeNode, resolve) {
    16. dictApi.getAllDict(tree.id).then(response => {
    17. resolve(response.data);
    18. })
    19. },
  • 导入导出的方法

    1. // 导出文档方法
    2. exportData() {
    3. this.$confirm('此操作将导出数据字典(Excel格式),是否继续?', '提示', {
    4. confirmButtonText: '确定',
    5. cancelButtonText: '取消',
    6. type: 'warning'
    7. }).then(() => { //确定执行then方法
    8. window.location.href = 'http://localhost:8202/admin/cmn/dict/exportData';
    9. })
    10. },
    11. // 显示上传框
    12. importData() {
    13. this.dialogImportVisible = true;
    14. },
    15. // 导入文档
    16. onUploadSuccess(response, file) {
    17. this.$message({
    18. type: "success",
    19. message: "上传成功",
    20. });
    21. this.dialogImportVisible = false;
    22. this.fetchData();
    23. }
    24. }