暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

基于Web的数据库查询工具:多引擎支持与动态输入处理

一安未来 2024-01-24
50

大家好,我是一安~

前言

需求详细说明:

一、项目启动与页面展示

  1. 项目启动后,用户应能看到一个基础页面。
  2. 在页面中,用户应能选择引擎类型,包括:mysql、oracle、mongo、shell

二、引擎选择与展示

  1. 当用户选择mysql
    oracle
    作为引擎时:
    • 页面应展示以下输入项:数据库地址、数据库账号、数据库密码、命令行和操作类型。
  2. 当用户选择mongo
    作为引擎时:
    • 页面应只展示以下输入项:数据库地址、命令行和操作类型。
  3. 当用户选择shell
    作为引擎时:
    • 页面应展示以下输入项:脚本路径、命令行。

三、输入项处理与默认值

  1. 如果用户未填写数据库地址、数据库账号、数据库密码,则应使用默认数据库信息。
  2. 如果用户填写了数据库地址、数据库账号、数据库密码,则应动态切换至用户输入的数据库信息。

四、查询功能

  1. 用户在填写完所有必要信息后,可以点击“查询”按钮。
  2. 点击“查询”后,系统应根据用户选择的引擎类型、数据库信息、命令行和操作类型进行相应的查询。

以上主要功能点是如何实现可以动态的注入新的数据源

正文

测试页面设计比较清晰简洁,易于理解和使用:

实际开发可以优化页面,比如:
又或者这样:

原理

Mysql
Oracle
数据源切换

AbstractRoutingDataSource
是多数据源切换中常用的一种实现方式,主要关注以下几个参数:

  1. defaultTargetDataSource
    :默认的数据源,当需要切换的数据源不存在或发生异常时,会使用该默认数据源。这个参数在初始化AbstractRoutingDataSource
    时需要提供。
  2. targetDataSources
    :这是一个存储多个数据源实例的Map
    ,设置该参数后,AbstractRoutingDataSource
    会将这些数据源实例存储在内部,以便后续的路由切换。
  1. resolvedDataSources
    :这是一个同步到 targetDataSources
    的数据源 Map
    ,在初始化后,AbstractRoutingDataSource
    会将 targetDataSources
    中的数据源实例同步到 resolvedDataSources
    中。
  2. afterPropertiesSet
    :这是生命周期回调方法,当所有属性都设置好之后会自动执行该方法。
  1. determineCurrentLookupKey
    :这是一个抽象方法,子类需要实现该方法以自定义数据源的切换逻辑。在该方法中,会根据当前线程的上下文信息返回一个用于获取对应数据源的 key
    ,这个 key
    是从 resolvedDataSources
    中获取数据源实例的依据。

Mongo
数据源切换

DefaultSingletonBeanRegistry
Spring
框架中一个重要的类,用于管理单例 Bean 的生命周期。

  1. registerSingleton
    :用于向 DefaultSingletonBeanRegistry
    注册一个新的单例 Bean。这个方法接收一个 Bean 名称和一个 Object
    对象作为参数,将它们关联起来,以便后续可以通过名称获取到对应的 Bean 实例。
  1. destroySingleton
    :用于销毁一个已经注册的单例 Bean。这个方法接收一个 Bean 名称作为参数,通过名称找到对应的 Bean 实例,然后调用其 destroy
    方法来销毁它。

具体实现

按照上面原理介绍去实现:

  1. 定义DynamicDataSource
    类继承AbstractRoutingDataSource
    ,重写determineCurrentLookupKey()
    方法。
  2. 配置多个数据源启动时注入targetDataSources
    defaultTargetDataSource
  3. 通过Druid
    初始化新增数据源,通过afterPropertiesSet()
    方法将重新解析数据源。
  4. 对于mongo
    的采用DefaultSingletonBeanRegistry
    重新注入新的数据源。
  5. 采用策略模式将每个算法封装成一个对象

依赖引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.48</version>
</dependency>

<dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>11.2.0</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/lib/ojdbc6.jar</systemPath>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

配置默认数据源:

server:
  port: 8080

spring:
  datasource:
    mysql:
      url: jdbc:mysql://xxx.xxx.xxx.xxx:3306/scp?characterEncoding=utf8&useSSL=false
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password: root
      connection-error-retry-attempts: 3
    oracle:
      url: jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521:ora11g
      driver-class-name: oracle.jdbc.driver.OracleDriver
      username: vpdn
      password: vpdn
      connection-error-retry-attempts: 3
    mongo:
      uri: mongodb://admin:admin@xxx.xxx.xxx.xxx:27017/test?authSource=admin&authMechanism=SCRAM-SHA-1

定义DynamicDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class);

    // 通过ThreadLocal线程隔离的优势线程存储线程,当前线程只能操作当前线程的局部变量
    private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();
    // 把已有的数据源封装在Map里
    private Map<Object, Object> dynamicTargetDataSources = new HashMap<>();

    @Override
    protected Object determineCurrentLookupKey() {
        String key = DATA_SOURCE_KEY.get();
        LOGGER.info("current data-source is {}", key);
        return key;
    }


    /**
     * 创建一个新的数据源连接,并且设置此数据源为我们要用的数据源
     **/
    public  void changeDataSource(CommandDto commandDto) throws Exception {
        String type = commandDto.getType();
        String jdbcUrl = commandDto.getJdbcUrl();
        String username = commandDto.getUsername();
        String password = commandDto.getPassword();
        if("mysql".equalsIgnoreCase(type)){
            Class.forName("com.mysql.jdbc.Driver");
        }else{
            Class.forName("oracle.jdbc.driver.OracleDriver");
        }

        // 通过Druid数据库连接池连接数据库
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(jdbcUrl);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setConnectionErrorRetryAttempts(3);
        // 数据源初始化
        dataSource.init();
        //获取已解析的数据源
        Map<Object, DataSource> resolvedDataSources = super.getResolvedDataSources();
        //数据源的键值对存入Map
        this.dynamicTargetDataSources.putAll(resolvedDataSources);
        this.dynamicTargetDataSources.put(type, dataSource);
        // 设置数据源
        this.setTargetDataSources(this.dynamicTargetDataSources);
        // 解析数据源
        this.afterPropertiesSet();
        // 切换数据源
        DATA_SOURCE_KEY.set(type);
    }

    public static void changeDataSource(String dataSourceKey) {
        DATA_SOURCE_KEY.set(dataSourceKey);
    }

    public static void clearDataSource() {
        DATA_SOURCE_KEY.remove();
    }
}

配置数据源:

@Configuration
public class DynamicDataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.mysql")
    public DataSource mysqlDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.oracle")
    public DataSource oracleDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(mysqlDataSource());
        Map<Object, Object> targetDataSources = new HashMap<>(5);
        targetDataSources.put("mysql", mysqlDataSource());
        targetDataSources.put("oracle", oracleDataSource());
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }


    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactory.setMapperLocations(resolver.getResources("classpath*:/mapper/**/*.xml"));

        //map接收返回值值为null的问题,默认是当值为null,将key返回
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setCallSettersOnNulls(true);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        sessionFactory.setConfiguration(configuration);
        //设置全局GlobaleConfig用于解决Oracle主键自增
        if (dataSource.getConnection().getMetaData().getDriverName().contains("Oracle")){
            //设置全局GlobaleConfig用于解决Oracle主键自增
            com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig config = new com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig();
            config.setKeyGenerator(keyGenerator());
            com.baomidou.mybatisplus.core.config.GlobalConfig globalConfig = new GlobalConfig();
            globalConfig.setDbConfig(config);
            sessionFactory.setGlobalConfig(globalConfig);
        }
        return sessionFactory.getObject();
    }

    @Bean(name = "keyGenerator")
    public IKeyGenerator keyGenerator(){
        return new OracleKeyGenerator();
    }

    @Bean(name = "jdbcTemplate")
    public JdbcTemplate primaryJdbcTemplate(
            @Qualifier("dynamicDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

对于mysql
oracle
,采用JdbcTemplate
方式执行命令,这里不再多介绍,主要介绍一下mongo
shell

mongo
一些常用的查询运算符:

  • $eq
    :等于
  • $ne
    :不等于
  • $gt
    :大于
  • $gte
    :大于等于
  • $lt
    :小于
  • $lte
    :小于等于
  • $in
    :在指定的值数组中
  • $nin
    :不在指定的值数组中
  • $and
    :多个条件同时满足
  • $or
    :满足任意一个条件

使用样例:

  1. 查询年龄大于等于20的文档:"{\"find\": \"your_collection\", \"filter\": {\"age\": {\"$gte\": 20}}}"
  2. 插入:"{\"insert\":\"your_collection\",\"documents\":[{\"sessionid\":\"sessionid-2310291708500037511\",\"user_id\":\"userid-23102917085000371407\"}]}"
  3. 更新:"{\"update\": \"your_collection\", \"updates\": [{\"q\": {\"full_name\": \"full_name-231029170850003713\"}, \"u\": {\"$set\": {\"full_name\": \"full_name-yian\"}}}]}"
  4. 删除:"{\"delete\": \"your_collection\", \"deletes\": [{\"q\": {\"full_name\": \"full_name-yian\"}, \"limit\": 1}]}"

最后采用mongoTemplate.executeCommand(String jsonCommand)
执行命令。

shell
采用ProcessBuilder
Java
中用于创建和管理操作系统进程的类,与之前的Process
类相比,ProcessBuilder
提供了更强大和灵活的API
,可以更好地控制进程的输入、输出和错误流

演示效果

页面html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单</title>
    <script src="https://unpkg.com/vue@2.7.14/dist/vue.min.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <script src="https://unpkg.com/axios@1.6.3/dist/axios.min.js"></script>
    <script src="https://unpkg.com/vue-json-viewer@2.2.22/vue-json-viewer.js"></script>
    <style>
        .content {
            margin: 50px auto;
        }

        .json-content {
            height: 650px;
            overflow-y: auto;
        }
    </style>
</head>

<body>
    <div class="content">
        <div id="app">
            <el-card>
                <el-form ref="ruleForm" label-width="100px" inline>
                    <el-form-item label="引擎类型">
                        <el-select v-model="formData.type" placeholder="请选择类型" style="width: 240px;">
                            <el-option label="全部" :value="null"></el-option>
                            <el-option v-for="(typeDes,type) in typeOptions" :key="type" :label="typeDes"
                                :value="type"></el-option>
                        </el-select>
                    </el-form-item>
                    <el-form-item label="数据库URL" prop="jdbcUrl" v-if="formData.type !== 'shell'">
                        <el-input v-model="formData.jdbcUrl" style="width: 240px;"></el-input>
                    </el-form-item>
                    <template v-if="formData.type === 'mysql' || formData.type === 'oracle'">
                        <el-form-item label="数据库账号" prop="username">
                            <el-input v-model="formData.username" style="width: 240px;"></el-input>
                        </el-form-item>
                        <el-form-item label="数据库密码" prop="password">
                            <el-input type="password" show-password v-model="formData.password"
                                style="width: 240px;"></el-input>
                        </el-form-item>
                    </template>
                    <el-form-item label="脚本路径" prop="desc" v-if="formData.type === 'shell'">
                        <el-input v-model="formData.desc" style="width: 240px;"></el-input>
                    </el-form-item>
                    <el-form-item label="命令行" prop="command">
                        <el-input v-model="formData.command" style="width: 240px;"></el-input>
                    </el-form-item>
                    <el-form-item label="操作类型" prop="desc" v-if="formData.type !== 'shell' && formData.type">
                        <el-select v-model="formData.desc" placeholder="请选择操作类型" style="width: 240px;">
                            <el-option label="全部" :value="null"></el-option>
                            <el-option v-for="(desc,key) in descOptions" :key="key" :label="desc"
                                :value="desc"></el-option>
                        </el-select>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
                    </el-form-item>
                </el-form>
            </el-card>
            <div style="height:20px;"></div>
            <el-card header="查询结果">
                <div class="json-content">
                    <json-viewer :value="searchResult" :expand-depth=5 copyable sort></json-viewer>
                </div>
            </el-card>
        </div>
    </div>
    <script>
        Vue.use(JsonView.default)
        new Vue({
            el: '#app',
            data() {
                return {
                    formData: {
                        type: null,
                        jdbcUrl: '',
                        username: '',
                        password: '',
                        command'',
                        desc: ''
                    },
                    typeOptions: {
                        'mysql''MySql数据库',
                        'oracle''Oracle数据库',
                        'mongo''MongoDB数据库',
                        'shell''shell脚本'
                    },
                    descOptions: {
                        'insert''insert',
                        'delete''delete',
                        'update''update',
                        'select''select'
                    },
                    searchResult: '暂无数据'
                }
            },
            watch: {
                "formData.type"(val, old) {
                    if (old === 'shell' || val === 'shell') {
                        this.formData.desc = '';
                    }
                    this.formData.jdbcUrl = '';
                    this.formData.username = '';
                    this.formData.password = '';
                    this.formData.command = '';
                }
            },
            methods: {
                async search() {
                    const result = await axios.post('http://localhost:8080/execute', this.formData).then(res => res.data);
                    this.searchResult = result;
                }
            }
        })
    </script>
</body>

</html>

控制层:

@RestController
@RequiredArgsConstructor(onConstructor_ = {@Lazy, @Autowired})
public class CommandController {

    private final DynamicDataSource dynamicDataSource;
    private final JdbcTemplate jdbcTemplate;
    private final MongoTemplate mongoTemplate;
    private final CommandContext commandContext;

    private final ApplicationContext applicationContext;



    @PostMapping("/execute")
    public HashMap<String, Object> executeCommand(@RequestBody CommandDto commandDto) throws Exception {
        String type = commandDto.getType();
        String jdbcUrl = commandDto.getJdbcUrl();
        String username = commandDto.getUsername();
        String password = commandDto.getPassword();
        if(StringUtils.hasText(jdbcUrl)&&StringUtils.hasText(username)&&StringUtils.hasText(password)){
            //切换数据源之前先清空
            DynamicDataSource.clearDataSource();
            //切换数据源
            dynamicDataSource.changeDataSource(commandDto);
        }else if("mysql".equalsIgnoreCase(type)){
            DynamicDataSource.clearDataSource();
            DynamicDataSource.changeDataSource("mysql");
        }else if("oracle".equalsIgnoreCase(type)){
            DynamicDataSource.clearDataSource();
            DynamicDataSource.changeDataSource("oracle");
        }else if(StringUtils.hasText(jdbcUrl)&&"mongo".equalsIgnoreCase(type)){
            DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry) applicationContext.getAutowireCapableBeanFactory();
            registry.destroySingleton("mongoDbFactory");
            registry.registerSingleton("mongoDbFactory", new SimpleMongoClientDatabaseFactory(jdbcUrl));
        }
        if ("mysql".equalsIgnoreCase(type)){
            CommandStrategy commandStrategy = new MysqlCommandStrategy(jdbcTemplate);
            commandContext.setCommandStrategy(commandStrategy);
        }else if ("oracle".equalsIgnoreCase(type)){
            CommandStrategy commandStrategy = new OracleCommandStrategy(jdbcTemplate);
            commandContext.setCommandStrategy(commandStrategy);
        }else if ("mongo".equalsIgnoreCase(type)){
            CommandStrategy commandStrategy = new MongoCommandStrategy(mongoTemplate);
            commandContext.setCommandStrategy(commandStrategy);
        }else if ("shell".equalsIgnoreCase(type)){
            CommandStrategy commandStrategy = new ShellCommandStrategy();
            commandContext.setCommandStrategy(commandStrategy);
        }
        return commandContext.executeCommand(commandDto);
    }
}


如果这篇文章对你有所帮助,或者有所启发的话,帮忙 分享、收藏、点赞、在看,你的支持就是我坚持下去的最大动力!

SpringCloud Alibaba微服务实战之集成网关


聊一聊海量请求下的接口并发解决方案


阿里终面:每天100w次登陆请求, 8G 内存该如何设置JVM参数?

文章转载自一安未来,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论