SpringBoot +JdbcTemplate+VUE 实现在线输入SQL语句返回数据库结果


在这里插入图片描述

前言

想起来要做这个功能是因为我们公司的预生产环境和生产环境如果想要连接数据库都需要登录堡垒机,然后再通过堡垒机进行跳转到对应定制的Navicat 连接工具进行查询。每次这个过程十分繁琐,所以就想直接在我们的系统上直接做个口子,登录以后,可以直接输入SQL查询,跟直接连接Navitcat效果是一样的

image-20231022175350150

我本地用的是DataGrip 直接运行结果如下:

image-20231022175441738

框架选型

那么想要实现一个这个功能需要准备什么呢?

首先我这里后端是采用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;

}

image-20231022183513609

先给出核心代码:主要就是上面说的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;
		});

	}

类似这样的结果:

image-20231022184052568

前端处理

前端我们这里有个需要特别处理的点就是需要根据后端每次返回的不同的列进行渲染不一样的表格

比如我这里select demo_id,demo_code,demo_name,status_desc,start_date5个字段,就展示5个列的表格,3个列就只展示3个了

image-20231022184305912

3个列展示对应的表格

image-20231022184419378

完整的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);
	}
实现多条件筛选功能的一般步骤如下: 1. 前端页面实现多个筛选条件的输入框及提交按钮。 2. 后端接收前端提交的筛选条件数据,根据条件拼接 SQL 语句。 3. 后端使用 SpringBoot 与 MySQL 进行交互,将拼接好的 SQL 语句传递给 MySQL 数据库进行查询。 4. 查询结果返回前端页面进行展示。 下面是一个示例代码: 前端页面代码: ```html <!-- 筛选条件输入框 --> <input v-model="filterData.name" placeholder="请输入名称"> <input v-model="filterData.age" placeholder="请输入年龄"> <!-- 提交按钮 --> <button @click="search">查询</button> ``` ```javascript // 前端页面对应的 Vue 实例 new Vue({ el: '#app', data: { filterData: { name: '', age: '' }, resultData: [] }, methods: { search() { // 前端页面提交筛选条件数据给后端 axios.post('/api/search', this.filterData) .then(response => { // 获取后端返回的查询结果 this.resultData = response.data; }) .catch(error => { console.log(error); }); } } }); ``` 后端代码: ```java @RestController @RequestMapping("/api") public class SearchController { @Autowired private JdbcTemplate jdbcTemplate; @PostMapping("/search") public List<Map<String, Object>> search(@RequestBody Map<String, Object> filterData) { String name = (String) filterData.get("name"); String age = (String) filterData.get("age"); // 拼接 SQL 语句 StringBuilder sql = new StringBuilder("SELECT * FROM users WHERE 1=1"); if (!StringUtils.isEmpty(name)) { sql.append(" AND name LIKE '%").append(name).append("%'"); } if (!StringUtils.isEmpty(age)) { sql.append(" AND age = ").append(age); } // 执行 SQL 语句 List<Map<String, Object>> result = jdbcTemplate.queryForList(sql.toString()); return result; } } ``` 上述代码中,前端页面输入筛选条件后,使用 axios 库将数据提交到后端的 `/api/search` 接口。后端接收到数据后,根据筛选条件拼接 SQL 语句,使用 JdbcTemplate 发送 SQL 语句到 MySQL 数据库进行查询,并将查询结果返回前端页面。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值