Java系列文章
前言
本文将简单介绍如何使用springboot和mybatis快速增删改查,实现通用的数据字典业务流程,项目通用配置另写文章介绍。
一、环境准备
1.1 开发工具清单
- JDK 17+、Maven 3.8+、IntelliJ IDEA 2024.3
1.2 初始化SpringBoot项目
1.2.1项目创建
- Spring Initializr或者IDEA快速创建(勾选Web/MyBatis/MySQL)
1.2.2 目录结构
1.3数据库准备
1.3.1 表结构设计
1.3.2 添加数据
1.3.3 sql执行代码
CREATE TABLE `tb_dictionary` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`parent_id` int DEFAULT NULL COMMENT '父级id',
`key` varchar(255) DEFAULT NULL COMMENT '字典key',
`value` varchar(255) DEFAULT NULL COMMENT '字典value',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`sort` int DEFAULT NULL COMMENT '排序',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (1, 0, 'combo_type', NULL, '套餐类型', 1, '2025-05-14 22:27:12');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (2, 1, '1', '不限', '', 2, '2025-05-14 22:27:28');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (3, 1, '2', '父母体检', '', 2, '2025-05-14 22:33:08');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (4, 1, '3', '入职体检', '', 2, '2025-05-14 22:33:23');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (5, 1, '4', '职场白领', '', 2, '2025-05-14 22:34:12');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (6, 1, '5', '个人高端', '', 2, '2025-05-14 22:35:23');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (7, 1, '6', '中青年体检', '', 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (8, 0, 'combo_tag', NULL, '套餐标签', 1, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (9, 8, '1', '男性', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (10, 8, '2', '女性', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (11, 8, '3', '中青年', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (12, 8, '4', '中老年', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (13, 8, '5', '白领精英', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (14, 0, 'combo_module', NULL, '套餐模块', 1, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (15, 0, 'shelf', NULL, '上下架', 1, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (16, 14, '1', '活动专区', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (17, 14, '2', '热卖套餐', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (18, 14, '3', '新品推荐', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (19, 14, '4', '孝敬父母', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (20, 14, '5', '白领精英', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (21, 15, '1', '上架', NULL, 2, '2025-05-14 22:35:37');
INSERT INTO `tb_dictionary` (`id`, `parent_id`, `key`, `value`, `description`, `sort`, `update_time`) VALUES (22, 15, '2', '下架', NULL, 2, '2025-05-14 22:35:37');
二、极速整合
2.1 依赖配置解析
2.1.1 pom.xml关键依赖
提示:这里使用到mybatis、mysql和driud连接池
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!--MySQL-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.32</version>
</dependency>
<!--MySQL连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.15</version>
</dependency>
2.2 数据源配置
2.2.1 application.yml配置MySQL连接池(Druid)
server:
port: 5577
servlet:
context-path: /mis-api
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: "数据库连接地址"
username: "账号"
password: "密码"
initial-size: 8
max-active: 16
min-idle: 8
max-wait: 60000
test-while-idle: true
test-on-borrow: true
test-on-return: false
validation-query: SELECT 1
mybatis:
mapper-locations: "classpath*:mapper/*.xml"
type-aliases-package: com.example.db.pojo
configuration:
2.3 MyBatis核心配置
2.3.1 Mapper接口扫描路径
2.3.2 驼峰命名自动转换
提示:yml文件配置开启驼峰转换
mybatis:
mapper-locations: "classpath*:mapper/*.xml"
type-aliases-package: com.example.db.pojo
configuration:
# Mybatis开启驼峰映射
map-underscore-to-camel-case: true
2.3.3 MyBatis-X自动生成Mapper/Entity
快速生成
生成目录
三、CRUD接口编写
3.1 实体类与Mapper接口
3.1.1 DictionaryEntity实体类
package com.example.db.pojo;
import java.util.Date;
import lombok.Data;
/**
*
* @TableName tb_dictionary
*/
@Data
public class DictionaryEntity {
/**
* id
*/
private Integer id;
/**
* 父级id
*/
private Integer parentId;
/**
* 字典key
*/
private String key;
/**
* 字典value
*/
private String value;
/**
* 描述
*/
private String description;
/**
* 排序
*/
private Integer sort;
/**
* 更新时间
*/
private Date updateTime;
}
3.1.2 DictionaryDao接口
package com.example.db.dao;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* @author lenovo
* @description 针对表【tb_dictionary】的数据库操作Mapper
* @createDate 2025-05-14 23:04:21
* @Entity com.example.db.pojo.DictionaryEntity
*/
public interface DictionaryDao {
// 数据字典-查询id
public ArrayList<HashMap> searchDictionaryById(Map param);
// 数据字典-新增
public Integer insertDictionary(Map param);
// 数据字典-更新
public Integer updateDictionary(Map param);
// 数据字典-删除
public Integer deleteDictionary(Map param);
}
3.2 xml映射文件编写
3.2.1 DictionaryDao.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.db.dao.DictionaryDao">
<resultMap id="BaseResultMap" type="com.example.db.pojo.DictionaryEntity">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="parentId" column="parent_id" jdbcType="INTEGER"/>
<result property="key" column="key" jdbcType="VARCHAR"/>
<result property="value" column="value" jdbcType="VARCHAR"/>
<result property="description" column="description" jdbcType="VARCHAR"/>
<result property="sort" column="sort" jdbcType="INTEGER"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 数据字典-查询id -->
<select id="searchDictionaryById" parameterType="map" resultType="hashmap">
select d.id, d.parent_id as parentId, d.key, d.value, d.description, d.sort, DATE_FORMAT(d.update_time,"%Y-%m-%d %H:%i:%s") as updateTimne
from tb_dictionary d
where d.parent_id = #{parentId}
</select>
<!-- 数据字典-新增 -->
<insert id="insertDictionary">
insert into tb_dictionary
set parent_id=#{parentId}
<if test="description!=null">
,`description`=#{description}
</if>
<if test="sort!=null">
,`sort`=#{sort}
</if>
<if test="key!=null">
,`key`=#{key}
</if>
<if test="value!=null">
,`value`=#{value}
</if>
</insert>
<!-- 数据字典-更新 -->
<update id="updateDictionary">
update tb_dictionary
set parent_id=#{parentId}
<if test="description!=null">
,`description`=#{description}
</if>
<if test="sort!=null">
,`sort`=#{sort}
</if>
<if test="key!=null">
,`key`=#{key}
</if>
<if test="value!=null">
,`value`=#{value}
</if>
where id = #{id}
</update>
<!-- 数据字典-删除 -->
<delete id="deleteDictionary">
delete from tb_dictionary where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
3.3 Service层封装
package com.example.mis.service;
import com.example.db.dao.DictionaryDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Service
public class DictionaryService {
@Resource
private DictionaryDao dictionaryDao;
// 数据字典-查询id
public ArrayList<HashMap> searchDictionaryById(Map param){
ArrayList<HashMap> list = dictionaryDao.searchDictionaryById(param);
return list;
}
// 数据字典-新增
public Integer insertDictionary(Map param) {
Integer rows = dictionaryDao.insertDictionary(param);
return rows;
}
// 数据字典-更新
public Integer updateDictionary(Map param) {
Integer rows = dictionaryDao.updateDictionary(param);
return rows;
}
// 数据字典-删除
public Integer deleteDictionary(Map param) {
Integer rows = dictionaryDao.deleteDictionary(param);
return rows;
}
}
3.4 Controller层CRUD
3.4.1 接口参数校验
表单验证目录
校验代码编写
package com.example.req.Dictionary;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class DictionaryEditForm {
private Integer id;
@NotNull(message = "parentId不能为空")
private Integer parentId;
// @NotBlank(message = "key不能为空")
private String key;
// @NotBlank(message = "value不能为空")
private String value;
// @NotBlank(message = "description不能为空")
private String description;
// @NotNull(message = "sort不能为空")
private Integer sort;
}
3.4.1 增删改查接口开发
package com.example.mis.controller;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.example.common.result.R;
import com.example.mis.service.DictionaryService;
import com.example.req.Dictionary.DictionaryDeleteForm;
import com.example.req.Dictionary.DictionaryEditForm;
import com.example.req.Dictionary.DictionarySearchForm;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/admin")
public class DictionaryController {
@Resource
private DictionaryService dictionaryService;
/**
* 数据字典-查询id
* @param form
* @return
*/
@PostMapping("/dictionary/searchDictionaryById")
public R searchDictionaryById(@RequestBody @Valid DictionarySearchForm form) {
Map param = BeanUtil.beanToMap(form);
ArrayList<HashMap> list = dictionaryService.searchDictionaryById(param);
return R.success(list);
}
/**
* 数据字典-编辑
* @param form
* @return
*/
@PostMapping("/dictionary/editDictionary")
public R editDictionary(@RequestBody @Valid DictionaryEditForm form) {
Map param = BeanUtil.beanToMap(form);
Integer rows;
if(ObjectUtil.isEmpty(form.getId())) {
# 数据字典-新增
rows = dictionaryService.insertDictionary(param);
}else {
# 数据字典-更新
rows = dictionaryService.updateDictionary(param);
}
return R.success(rows);
}
/**
* 数据字典-删除
* @param form
* @return
*/
@PostMapping("/dictionary/deleteDictionary")
public R deleteDictionary(@RequestBody @Valid DictionaryDeleteForm form) {
Map param = BeanUtil.beanToMap(form);
Integer rows = dictionaryService.deleteDictionary(param);
return R.success(rows);
}
}
3.5 Apifox接口测试
四、前端实现
4.1 界面展示
4.2 完整代码
<template>
<a-card shadow="never" :style="{ height: cardHeight + 'px' }">
<splitpanes class="split-theme">
<pane min-size="10" size="15">
<div class="dictionary-category">
<div
:class="['dictionary-category-item', activeKey === index ? 'active' : '']"
v-for="(item, index) in categoryList"
:key="index"
@click="onCategoryItem(item, index)"
>
{{ item.key }}({{ item.description }})
</div>
</div>
</pane>
<pane min-size="70" size="85">
<a-spin :spinning="loading" size="large" tip="Loading...">
<div class="split-header">
<div class="nav">
<a-button type="primary" @click="onAddDictionary">新增</a-button>
</div>
<div class="action">
<a-button type="primary">导出</a-button>
</div>
</div>
<div class="split-list">
<a-table :columns="dictionaryColumn" :data-source="dictionaryData" :pagination="false" :scroll="{ y: scrollHeight }" size="small">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button type="text" @click="onEditDictionary(record)">编辑</a-button>
<a-button type="text" @click="onDeleteDictionary(record)">删除</a-button>
</template>
</template>
</a-table>
</div>
</a-spin>
</pane>
</splitpanes>
<diaLogModal v-model="diaLogData.show" :diaLogData="diaLogData" @submit="onDialogConfirm">
<a-form ref="userFormRef" :model="dataForm" :label-col="{ style: { width: '150px' } }" :wrapper-col="{ span: 14 }">
<a-form-item label="key">
<a-input v-model:value="dataForm.key" placeholder="请输入key" allow-clear autoComplete="off" />
</a-form-item>
<a-form-item label="value">
<a-input v-model:value="dataForm.value" placeholder="请输入value" allow-clear autoComplete="off" />
</a-form-item>
<a-form-item label="描述">
<a-input v-model:value="dataForm.description" placeholder="请输入字典描述" allow-clear autoComplete="off" />
</a-form-item>
<a-form-item label="排序">
<a-input v-model:value="dataForm.sort" placeholder="请输入排序" allow-clear autoComplete="off" />
</a-form-item>
</a-form>
</diaLogModal>
</a-card>
</template>
<script setup>
import { searchDictionaryById, editDictionary, deleteDictionary } from '@/api/modules/system.js'
import { ref, reactive, onMounted, h } from 'vue'
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
import { Modal, message } from 'ant-design-vue'
// 页面窗口尺寸
function useWindowRise() {
const scrollHeight = ref(window.innerHeight - 122 - 140)
const cardHeight = ref(window.innerHeight - 122)
const height = ref(window.innerHeight - 122 + 'px')
window.onresize = e => {
scrollHeight.value = e.target.innerHeight - 122 - 140
cardHeight.value = window.innerHeight - 122
height.value = window.innerHeight - 122 + 'px'
}
return {
scrollHeight,
cardHeight,
height
}
}
// 初始化数据
function useInitData() {
const loading = ref(false)
const activeKey = ref(0)
const categoryList = ref([])
const dictionaryColumn = ref([
{
title: 'Key',
dataIndex: 'key',
key: 'key',
width: 180
},
{
title: 'Value',
dataIndex: 'value',
key: 'value',
width: 160
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
width: 300
},
{
title: '更新时间',
dataIndex: 'updateTimne',
key: 'updateTimne',
width: 120
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 120
}
])
const dictionaryData = ref([])
const getDictionaryData = async parentId => {
loading.value = true
const data = await searchDictionaryById({ parentId: parentId })
setTimeout(() => {
loading.value = false
}, 200)
return data
}
onMounted(async () => {
loading.value = true
categoryList.value = await getDictionaryData(0)
dictionaryData.value = await getDictionaryData(categoryList.value[0]?.id)
})
return { loading, activeKey, categoryList, dictionaryColumn, dictionaryData, getDictionaryData }
}
// 操作事件
function useAction() {
// 弹窗表单
const diaLogData = reactive({
show: false,
title: '字典新增',
width: '40%',
buttons: ['取消', '确定']
})
const dataForm = reactive({
id: '',
parentId: '',
key: '',
value: '',
description: '',
sort: ''
})
const originForm = reactive({
id: '',
parentId: '',
key: '',
value: '',
description: '',
sort: ''
})
// 按钮事件
const onCategoryItem = async (item, key) => {
activeKey.value = key
loading.value = true
dictionaryData.value = await getDictionaryData(item.id)
}
const onAddDictionary = () => {
Object.assign(dataForm, originForm)
dataForm.parentId = categoryList.value[activeKey.value]?.id
diaLogData.show = true
}
const onEditDictionary = rows => {
diaLogData.show = true
dataForm.id = rows.id
dataForm.parentId = rows.parentId
dataForm.key = rows.key
dataForm.value = rows.value
dataForm.description = rows.description
dataForm.sort = rows.sort
}
const onDeleteDictionary = rows => {
Modal.confirm({
title: '警告',
icon: h(ExclamationCircleOutlined),
content: '是否确认删除?',
okText: '确认',
cancelText: '取消',
onOk() {
deleteDictionary({ ids: [rows.id] })
.then(async res => {
message.success('删除成功')
dictionaryData.value = await getDictionaryData(categoryList.value[activeKey.value]?.id)
})
.catch(err => {})
}
})
}
const onDialogConfirm = async () => {
const rows = await editDictionary(dataForm)
diaLogData.show = false
dictionaryData.value = await getDictionaryData(categoryList.value[activeKey.value]?.id)
message.success(rows > 0 ? '更新成功' : '更新失败')
}
return {
onCategoryItem,
diaLogData,
dataForm,
onAddDictionary,
onEditDictionary,
onDeleteDictionary,
onDialogConfirm
}
}
const { scrollHeight, cardHeight, height } = useWindowRise()
const { loading, activeKey, categoryList, dictionaryColumn, dictionaryData, getDictionaryData } = useInitData()
const { onCategoryItem, diaLogData, dataForm, onAddDictionary, onEditDictionary, onDeleteDictionary, onDialogConfirm } = useAction()
</script>
<style lang="scss" scoped>
.dictionary {
width: 100%;
height: 100%;
background: #fff;
box-sizing: border-box;
}
.split-theme {
height: v-bind(height);
}
.dictionary-category {
padding: 4px 0;
}
.dictionary-category-item {
padding: 8px;
margin: 4px;
color: #515a6e;
font-size: 14px;
font-weight: 400;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
user-select: none;
transition: all 0.5s;
&.active {
background-color: #3963bc33 !important;
color: #3963bc;
font-weight: 600;
}
&:hover {
background-color: #f3f3f3;
}
}
.split-header {
height: 60px;
padding: 16px;
/* box-sizing: border-box; */
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
gap: 16px;
justify-content: space-between;
.action {
display: flex;
align-items: center;
gap: 16px;
}
}
.split-list {
padding: 16px;
user-select: none;
overflow: hidden;
}
:deep(.splitpanes--vertical > .splitpanes__splitter) {
background: #f0f0f0a8 !important;
width: 1px;
margin: 8px 0;
&::after {
content: '';
width: 200px;
height: 200px;
background: url(../../../assets/images/split.png);
position: absolute;
top: 50%;
margin: -100px 0 0 -100px;
border-radius: 8px;
transform: scale(0.2);
}
}
:deep(.ant-card-body) {
padding: 0px !important;
}
:deep(.ant-spin-nested-loading) {
position: initial !important;
}
:deep(.ant-spin-nested-loading .ant-spin-container) {
position: initial !important;
}
:deep(.splitpanes__pane) {
position: relative !important;
min-width: 150px;
}
</style>