文章目录
- 前言
- 框架选型
- SpringBoot 结合JdbcTemplate简单示例
- SpringBoot 结合JdbcTemplate实现运行SQL
- 处理思路
- 后端处理
- 前端处理
前言
想起来要做这个功能是因为我们公司的预生产环境和生产环境如果想要连接数据库都需要登录堡垒机,然后再通过堡垒机进行跳转到对应定制的Navicat 连接工具进行查询。每次这个过程十分繁琐,所以就想直接在我们的系统上直接做个口子,登录以后,可以直接输入SQL查询,跟直接连接Navitcat效果是一样的
我本地用的是DataGrip 直接运行结果如下:
框架选型
那么想要实现一个这个功能需要准备什么呢?
首先我这里后端是采用SpringBoot 结合JdbcTemplate ,而对于前端展示的话,是采用了VUE+Element UI
Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
SpringBoot 结合JdbcTemplate简单示例
这里我们主要实现接口是通过JdbcTemplate的query(String sql, RowMapper rowMapper)方法,
该方法用于执行提供的SQL查询,并将查询结果通过RowMapper接口进行映射。
下面对该方法的参数和用法进行一些解析:
String sql
:要执行的SQL查询语句。RowMapper<T> rowMapper
:用于将结果集的每一行映射到具体对象的映射器接口。它定义了一个mapRow(ResultSet rs, int rowNum)
方法,用来映射结果集中的每一行数据。rs
参数代表结果集,rowNum
参数表示当前行号。该方法将会被JdbcTemplate在每一行数据上调用。
下面展示了一个示例,使用query(String sql, RowMapper<T> rowMapper)
方法查询数据库,将结果映射到Java对象中:
public class UserRepository {
private JdbcTemplate jdbcTemplate;
public List<User> getAllUsers() {
String sql = "SELECT * FROM users";
List<User> userList = jdbcTemplate.query(sql, new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
}
});
return userList;
}
}
在上面的示例中,我们使用了一个匿名内部类作为RowMapper,并实现了mapRow(ResultSet rs, int rowNum)
方法来完成结果集到User对象的映射,其中rs参数用于访问结果集的列,rowNum参数用来表示当前行的索引,我们根据列名获取对应字段的值并设置到User对象中。
当我们调用语句jdbcTemplate.query(sql, rowMapper)
时,JdbcTemplate会执行查询并将结果通过rowMapper进行映射,最终返回一个包含映射后的对象列表的List。
上述示例仅展示了基本用法,我们可以根据实际需要进行进一步的调整和自定义映射逻辑。
SpringBoot 结合JdbcTemplate实现运行SQL
如果对于上面的例子理解了,对于实现我们的功能就变得异常简单了。
处理思路
- SQL格式化: 我们前端传入的是一个SQL字符串,我们这里需要将对应的SQL先进行处理,去掉多余的一些空格,防止运行报错,
- 执行前先count一下总数进行限制(这个步骤程序里面的处理有点问题,先注释去掉了,只是提供了一个思路): 我们传入的SQL语句运行结果可能会有几万几十万甚至更大的数据,如果select了一张大表又不加以限制,那可能导致程序直接OOM,所以我们这里在运行SQL前,先count一下对应的sql数量如果不超过某个总数才可以直接进行查询。
- 处理查询结果 :由于我们的在线查询每次返回的都是不一样的结果,所以进行接收肯定是用map,而不能采用我们平常使用的实体entity接收
- 前端动态渲染列: 另外对于我们每次查询的sql,对应的列是不一样的,所以我们的前端vue需要根据返回的结果进行动态渲染列
后端处理
先看一下Postman返回结果展示: RunSqlVO主要就是两块内容,一个就是查询结果,一个就是对应的列名集合(给前端进行动态渲染表格使用)
@Data
public class RunSqlVO {
//数据库返回的map数据list
private List<Map<String, Object>> queryResult;
//对应的列名集合
private List <Map<String, Object>>columns;
}
先给出核心代码:主要就是上面说的4步骤:
@PostMapping("/queryData")
public Object queryData(@RequestBody RunSqlParam runSqlParam) {
JsonResponse jsonResponse = ResponseFactory.newJsonResponse(SystemCode.SUCCESS, "执行成功");
//1、格式化sql
String sql = formatSqlToOneLine(runSqlParam.getSql());
//2、执行前先count一下总数进行限制
// 这块处理有点问题,可以先去掉: 使用正则表达式匹配 SELECT 语句中的字段部分
/* String countSql = getCountSql(sql);
Integer count = jdbcTemplate.queryForObject(countSql, Integer.class);
if (count > MAX_COUNT) {
return jsonResponse;
}*/
3、处理查询结果
List<Map<String, Object>> queryResult = jdbcTemplate.query(sql
.replaceAll("[\\s]+", " "), (rs, rowNum) -> handleData(rs));
RunSqlVO runSqlVO = new RunSqlVO();
//4、根据queryResult数据库列名,进行前端动态渲染列
handleColumn(queryResult, runSqlVO);
runSqlVO.setQueryResult(queryResult);
return jsonResponse.setData(runSqlVO);
}
//处理数据库返回结果数据
private static Map<String, Object> handleData(ResultSet rs) throws SQLException {
Map<String, Object> row = new LinkedHashMap<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnName(i);
Object value = rs.getObject(i);
//如果是字符串太长了,只展示前200个
if (Objects.nonNull(value)) {
value = value.toString().length() > MAX_SHOW_CONTENT ?
value.toString().substring(0, MAX_SHOW_CONTENT) : value;
}
//针对日期进行格式化
if (value instanceof Date){
Date date = (Date) value;
value=DateUtil.formatDate(date);
}
row.put(columnName, value);
}
return row;
}
//根据queryResult 结果列里面返回对应前端的列名进行渲染
private static void handleColumn( List<Map<String, Object>> queryResult,
RunSqlVO runSqlVO) {
List<Map<String, Object>> columnList=new ArrayList<>();
Map<String, Object> map = queryResult.get(0);
for (String key : map.keySet()) {
Map<String, Object> column = Maps.newHashMap();
column.put("field", key);
column.put("label", key);
columnList.add(column);
}
runSqlVO.setColumns(columnList);
}
// 使用正则表达式匹配 SELECT 语句中的字段部分,并生成对应的 COUNT 语句
private static String getCountSql(String selectSql) {
// 定义正则表达式匹配模式
String regex = "^SELECT (.*) FROM";
String replacement = "SELECT COUNT(*) FROM";
// 使用正则表达式进行匹配和替换
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(selectSql);
String countSql = matcher.replaceFirst(replacement);
return countSql;
}
//格式化SQL
public static String formatSqlToOneLine(String sql) {
// 去除 SQL 语句中的换行符和多余的空格
sql = sql.replaceAll("\\s+", " ");
sql = sql.replaceAll("\\r?\\n", "");
return sql;
}
1、格式化sql
public static String formatSqlToOneLine(String sql) {
// 去除 SQL 语句中的换行符和多余的空格
sql = sql.replaceAll("\\s+", " ");
sql = sql.replaceAll("\\r?\\n", "");
return sql;
}
2、执行前先count一下总数进行限制
String countSql = getCountSql(sql);
Integer count = jdbcTemplate.queryForObject(countSql, Integer.class);
if (count > MAX_COUNT) {
return jsonResponse;
}
// 使用正则表达式匹配 SELECT 语句中的字段部分,并生成对应的 COUNT 语句
private static String getCountSql(String selectSql) {
// 定义正则表达式匹配模式
String regex = "^SELECT (.*) FROM";
String replacement = "SELECT COUNT(*) FROM";
// 使用正则表达式进行匹配和替换
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(selectSql);
String countSql = matcher.replaceFirst(replacement);
return countSql;
}
3、处理查询结果
由于我们的在线查询每次返回的都是不一样的结果,所以进行接受肯定是用map,而不能采用我们平常使用的实体entity接收
private static Map<String, Object> handleData(ResultSet rs) throws SQLException {
Map<String, Object> row = new LinkedHashMap<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnName(i);
Object value = rs.getObject(i);
//如果是字符串太长了,只展示前200个
if (Objects.nonNull(value)) {
value = value.toString().length() > MAX_SHOW_CONTENT ?
value.toString().substring(0, MAX_SHOW_CONTENT) : value;
}
//针对日期进行格式化
if (value instanceof Date){
Date date = (Date) value;
value=DateUtil.formatDate(date);
}
row.put(columnName, value);
}
return row;
}
4、queryResult里面包含了列名,根据这个返回对应的列名即可,进行前端动态渲染列
这里返回列字段是field和label主要是为了和前端处理对应上:
fileld,对应的英文名,label对应的实际展示名,我们这里前端就直接展示数据库对应的列了,当然也可以展示成对应数据库的comment注释。
private static void handleColumn( List<Map<String, Object>> queryResult,
RunSqlVO runSqlVO) {
List<Map<String, Object>> columnList=new ArrayList<>();
//默认取第一个查询结果即可,里面会有对应的列名
Map<String, Object> map = queryResult.get(0);
for (String key : map.keySet()) {
Map<String, Object> column = Maps.newHashMap();
column.put("field", key);
column.put("label", key);
columnList.add(column);
}
runSqlVO.setColumns(columnList);
}
拓展:如果想要实现展示列名是对应数据库里面的中文comment,可以参考这个方法可以返回对应的列名,这里暂未实现。
这里主要是采用了SHOW FULL COLUMNS FROM tableName
语句
public List<Map<String, Object>> getColumnComments(String tableName) {
// 执行查询语句获取列注释
String query = "SHOW FULL COLUMNS FROM " + tableName;
return jdbcTemplate.query(query, (rs, rowNum) -> {
Map<String, Object> row = new HashMap<>();
String columnName = rs.getString("Field");
String columnComment = rs.getString("Comment");
row.put("columnName", columnName);
row.put("columnComment", columnComment);
return row;
});
}
类似这样的结果:
前端处理
前端我们这里有个需要特别处理的点就是需要根据后端每次返回的不同的列进行渲染不一样的表格
比如我这里select demo_id,demo_code,demo_name,status_desc,start_date5个字段,就展示5个列的表格,3个列就只展示3个了
3个列展示对应的表格
完整的VUE页面代码:
<template>
<div class="grid-table-container">
<el-card class="box-card">
<el-input
type="textarea"
:autosize="{ minRows:5, maxRows: 10}"
placeholder="请输入内容"
v-model="sql"
>
</el-input>
<div style="margin-top: 20px;border-top: 1px solid #E8E8E8;">
<el-form :inline="true" :model="formInline" class="demo-form-inline"
style="margin-left: 10px; margin-top: 20px">
<el-form-item>
<el-button type="primary" @click="onSubmit" style="margin-left: 20px">查询</el-button>
</el-form-item>
<el-form-item>
<el-button type="info" plain @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="tableData" style="width: 100%" border>
<el-table-column align="center" v-for="column in columns" :key="column.field" :prop="column.field"
:label="column.label"
></el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import { runSql } from '@/api/sys/user'
export default {
methods: {
onSubmit() {
runSql({ 'sql': this.sql }).then(res => {
console.log(res)
this.columns = res.data.columns
this.tableData = res.data.queryResult
})
},
fetchColumnsFromBackend() {
// 发送请求到后端获取字段信息
// 假设后端返回的字段信息格式为:
// [
// { field: 'name', label: '姓名' },
// { field: 'age', label: '年龄' },
// ...
// ]
// 将后端返回的字段信息赋值给 this.columns
this.columns = [
{ field: 'name', label: '姓名' },
{ field: 'age', label: '年龄' }
// ...
]
// 获取表格数据
this.fetchTableDataFromBackend()
},
fetchTableDataFromBackend() {
// 发送请求到后端获取表格数据
// 假设后端返回的数据格式为:
// [
// { name: '张三', age: 20 },
// { name: '李四', age: 25 },
// ...
// ]
// 将后端返回的数据赋值给 this.tableData
this.tableData = [
{ name: '张三', age: 20 },
{ name: '李四', age: 25 }
// ...
]
}
},
mounted() {
// 从后端获取字段信息,并根据字段信息生成动态列
// this.fetchColumnsFromBackend();
},
data() {
return {
sql: 'select * from sys_role',
tableData: [], // 从后端获取的数据
columns: [] // 动态列数组
}
}
}
</script>
核心就是里面的:根据列循环遍历渲染
<el-table :data="tableData" style="width: 100%" border>
<el-table-column align="center" v-for="column in columns" :key="column.field" :prop="column.field"
:label="column.label">
</el-table-column>
而这里对应后端返回的结果也就是前面提到的这个方法进行处理的
private static void handleColumn( List<Map<String, Object>> queryResult,
RunSqlVO runSqlVO) {
List<Map<String, Object>> columnList=new ArrayList<>();
Map<String, Object> map = queryResult.get(0);
for (String key : map.keySet()) {
Map<String, Object> column = Maps.newHashMap();
column.put("field", key);
column.put("label", key);
columnList.add(column);
}
runSqlVO.setColumns(columnList);
}