java mysql差异导出_java 如何从零实现一个数据库差异对比工具?

2023-11-08

对比数据的痛苦

不知道你是否也像我一样,在快乐编写代码的时候,必须进行一些数据库的数据对比工作。

诚然,一般的数据差异,比如是每一行的内容不同,市场上有比较成熟的 compare2 等对比工具。

但是如果是对比数据的每一列是否相同,这个就会变得比较麻烦。

c8b1bfb8d01de060b3bf8ebd06a82a7c.png

v1.0 纯人工对比

我们在做一些数据迁移等功能时,经常需要对比数据是否正确,最常见的方法就是人工一列一列的对比。

一开始老马也是这么和同事对的,对了几天之后感觉效率实在是低,而且还容易看花眼。

于是我就是琢磨,这个东西用程序对比应该会简单很多。

v2.0 半人工对比

说干就干,我花半天时间实现了一个基于 jsqlparser 可以解析类似于 insert into xxx (xx, xx, xx) values (xx, xx, xx); 的工具类。

然后对比 2 边的数据,这下对于一张表上百个字段的对比,一些变得快了许多,准确率也高了很多。

不要问我为什么会有上百个字段,这都是历史沉淀下来的瑰宝。。。

ps: insert into 语句是否通过数据库连接工具手工导出的。

后来又发现另一个问题:表太多,如果想换一个数据对比,我手工导出一遍又要花费数十分钟的时间,关键是重复且枯燥。

e07fcc66f3eb955fcf2955e8c2c9a111.png

既然重复,那么可以使用程序实现吗?

v3.0 对比基本自动化

于是我下班后熬夜实现了这个版本: java 程序实现了数据的导出持久化,然后进行修改前后的差异对比。

下面我分享一下自己的思路,以及核心源码,文末有下载福利。

希望对你工作和学习提供帮助。

整体理念

我希望这个工具是 MVP 的理念,由简单到复杂,后期逐渐丰富特性。

要有可拓展性,目前支持 mysql/oracle/sql server 等主流数据库,用户可以定制化开发。

尽可能少的依赖,使用原生的 jdbc,不需要引入 mybatis 等框架。

核心依赖

下面列举一下我用到的核心依赖:

fastjson 用于数据持久化为 json

mysql-connector-java 数据库连接驱动

jsqlparser 辅助工具,解析 sql 使用,非必须

实现思路根据指定的 jdbc 连接信息,自动选择对应的 jdbc 实现。

执行对应的 sql,将结果解析为 map,进行 JSON 持久化

对持久化的 json 进行差异对比,展现出差异结果

有了这个思路,一切就会变得朴实无华。

当然在此之前,需要我们把代码实现出来,下面进入写BUG环节:

af35aeb73a8d88ce289bf682e744905b.png

jdbc 实现

核心接口

考虑到后期不同数据库实现,我们统一定义一个查询接口/**

* JDBC 访问层

* @author 老马啸西风

* @date 2017/8/1

*/

public interface JdbcMapper {

/**

* 执行查询语句

* @param querySql

* @return

*/

ResultSet query(String querySql);

}

抽象实现

这里提供了基本的抽象实现。

子类只需要实现对应的连接获取信息即可。public abstract class AbstractJdbcMapper implements JdbcMapper {

protected JdbcVo jdbcVo;

public AbstractJdbcMapper(JdbcVo jdbcVo) {

this.jdbcVo = jdbcVo;

}

/**

* 获取数据库连接

* @return

*/

protected abstract Connection getConnection();

@Override

public ResultSet query(String querySql) {

ResultSet rs = null;

Connection connection = getConnection();

try {

Statement stmt = null;

stmt = connection.createStatement();

rs = stmt.executeQuery(querySql);

} catch (Exception e) {

System.out.println("SQL: " + querySql);

throw new ExportdbException(e);

}

return rs;

}

}

JdbcVo 连接信息

这个对象主要是数据库连接信息对象:public class JdbcVo {

/**

* 驱动类名称

*/

private String driverClassName;

/**

* 数据库链接

*/

private String url;

/**

* 用户名称

*/

private String username;

/**

* 密码

*/

private String password;

//getter & setter

}

mysql 实现

此处以 mysql 为例:import com.github.houbb.exportdb.dto.JdbcVo;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.SQLException;

/**

* mysql 实现

* @author 老马啸西风

* @date 2017/8/1

*/

public class MySqlJdbcMapper extends AbstractJdbcMapper {

public MySqlJdbcMapper(JdbcVo jdbcVo) {

super(jdbcVo);

}

@Override

protected Connection getConnection() {

try {

Class.forName(jdbcVo.getDriverClassName());

return DriverManager.getConnection(jdbcVo.getUrl(),

jdbcVo.getUsername(),

jdbcVo.getPassword());

} catch (ClassNotFoundException | SQLException e) {

e.printStackTrace();

}

return null;

}

}

这里主要是对连接的初始化,连接不同的数据库,都需要引入对应的数据源。

行数据导出实现

下面是导出的核心实现:

接口定义public interface IExportdb {

/**

* 查询

* @param context 上下文

* @param sql sql

* @return 结果

* @since 0.0.1

*/

QueryResultVo query(final ExportdbContext context, final String sql);

}

这里指定了需要执行的 sql。

context 中为了便于后期拓展,目前只有 JdbcMapper。

返回的就是 QueryResultVo,就是查询结果,定义如下:public class QueryResultVo {

/**

* 表名称

*/

private String tableName;

/**

* 数据库名称

*

* @since 0.0.2

*/

private String databaseName;

/**

* 结果集合

*/

private List> resultMaps;

/**

* 执行的 sql

*/

private String sql;

//getter & setter

}

默认实现

默认的导出实现如下:import com.github.houbb.exportdb.core.ExportdbContext;

import com.github.houbb.exportdb.core.IExportdb;

import com.github.houbb.exportdb.dal.JdbcMapper;

import com.github.houbb.exportdb.dto.QueryResultVo;

import com.github.houbb.exportdb.exception.ExportdbException;

import com.github.houbb.heaven.util.lang.StringUtil;

import net.sf.jsqlparser.JSQLParserException;

import net.sf.jsqlparser.parser.CCJSqlParserUtil;

import net.sf.jsqlparser.statement.Statement;

import net.sf.jsqlparser.statement.insert.Insert;

import net.sf.jsqlparser.statement.select.PlainSelect;

import net.sf.jsqlparser.statement.select.Select;

import java.sql.ResultSet;

import java.sql.ResultSetMetaData;

import java.sql.SQLException;

import java.util.ArrayList;

import java.util.LinkedHashMap;

import java.util.List;

import java.util.Map;

/**

* @author binbin.hou

* @since 0.0.1

*/

public class Exportdb implements IExportdb {

@Override

public QueryResultVo query(ExportdbContext context, String sql) {

try {

final JdbcMapper jdbcMapper = context.jdbcMapper();

ResultSet resultSet = jdbcMapper.query(sql);

List> maps = new ArrayList<>();

String tableName = null;

while (resultSet.next()) {

final ResultSetMetaData metaData = resultSet.getMetaData();

// 设置表名称

if(tableName == null) {

tableName = metaData.getTableName(1);

}

Map map = new LinkedHashMap<>();

// 为空直接返回,大于1则报错

// 列数的总数

int columnCount = metaData.getColumnCount();

for (int i = 1; i <= columnCount; i++) {

String columnName = metaData.getColumnName(i);

Object value = resultSet.getObject(columnName);

map.put(columnName, value);

}

maps.add(map);

}

if(StringUtil.isEmptyTrim(tableName)) {

Statement statement = CCJSqlParserUtil.parse(sql);

Select select = (Select)statement;

PlainSelect plainSelect = (PlainSelect) select.getSelectBody();

tableName = plainSelect.getFromItem().toString();

}

return QueryResultVo.newInstance().tableName(tableName)

.databaseName("")

.sql(sql)

.resultMaps(maps);

} catch (SQLException | JSQLParserException throwables) {

throw new ExportdbException(throwables);

}

}

}

其实实现非常简单,我们主要讲一下两点:

(1)表名称

mysql 经测试可以通过如下方式获取:resultSet.getMetaData();

tableName = metaData.getTableName(1);

oracle 我在测试的时候,发现无法获取。所以是借助 sqlparser 解析我们的查询语句得到的。

暂时主要是支持查询,所以这里写的有些固定了,后续可以优化一下。if(StringUtil.isEmptyTrim(tableName)) {

Statement statement = CCJSqlParserUtil.parse(sql);

Select select = (Select)statement;

PlainSelect plainSelect = (PlainSelect) select.getSelectBody();

tableName = plainSelect.getFromItem().toString();

}

(2)列信息

每一个查询,可能都对应多条记录。

我们看一下每一条记录的构建:while (resultSet.next()) {

final ResultSetMetaData metaData = resultSet.getMetaData();

Map map = new LinkedHashMap<>();

// 为空直接返回,大于1则报错

// 列数的总数

int columnCount = metaData.getColumnCount();

for (int i = 1; i <= columnCount; i++) {

String columnName = metaData.getColumnName(i);

Object value = resultSet.getObject(columnName);

map.put(columnName, value);

}

maps.add(map);

}

这个经常写 jdbc 的小伙伴也一定不陌生。

你说现在都用 mybatis 了,谁还写 jdbc 啊,这么 low。

那么,你自己手写一个 mybatis,这些也是必会的。

差异对比

导出的使用

我们可以把一行数据导出,可以在修改前后分别导出。

如果是导出到不同的库,不同的表,那么就进行不同库表之间的导出。

导出结果之后,就需要进行对比了。

对比实现

接口定义

对于导出结果的处理,你可以根据自己的实际情况自行选择。

比如导出为 csv/json/insert 等,对比差异也可以按照自己的需求定制。public interface IQueryResultHandler {

/**

* 结果处理类

* @param queryResultVo 查询结果

*/

void handler(final QueryResultVo queryResultVo);

}

持久化

此处介绍一种比较简单实用的方式:json 持久化。import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.serializer.SerializerFeature;

import com.github.houbb.exportdb.dto.QueryResultVo;

import com.github.houbb.exportdb.support.result.IQueryResultHandler;

import com.github.houbb.heaven.util.io.FileUtil;

import java.util.ArrayList;

import java.util.List;

import java.util.Map;

/**

* @author 老马啸西风

* @since 0.0.1

*/

public class FileJsonQueryResultHandler implements IQueryResultHandler {

/**

* 默认的文件输出路径

*

* 根据操作系统,自动设置

* @since 0.0.1

*/

private final String dir;

public FileJsonQueryResultHandler(String dir) {

this.dir = dir;

}

public FileJsonQueryResultHandler() {

this("D:\\exportdb\\");

}

/**

* 结果处理类

*

* @param queryResultVo 查询结果

*/

@Override

public void handler(final QueryResultVo queryResultVo) {

String path = dir+queryResultVo.tableName()+".edb";

System.out.println("文件路径: " + path);

List> list = queryResultVo.resultMaps();

List lines = new ArrayList<>(list.size()+1);

lines.add("-- "+queryResultVo.sql());

for(Map map : list) {

lines.add(JSON.toJSONString(map, SerializerFeature.WriteMapNullValue));

}

FileUtil.write(path, lines);

}

}

我们将行数据持久化到文件中,注意这里指定了 JSON.toJSONString(map, SerializerFeature.WriteMapNullValue);

这样可以让 null 字段也输出,更加方便对比。

文件差异对比实现

上面我们假设将文件输出到 2 个文件,下面指定文件路径就可以进行对比了:/**

* 差异对比

* @param oldPath 原始路径

* @param newPath 新的路径

*/

public static void differ(final String oldPath, final String newPath) {

List oldLines = FileUtil.readAllLines(oldPath);

List newLines = FileUtil.readAllLines(newPath);

System.out.println(FileUtil.getFileName(oldPath)+" 对比开始---------------");

for(int i = 0; i < oldLines.size(); i++) {

String oldL = oldLines.get(i);

String newL = newLines.get(i);

if(oldL.startsWith("--")) {

continue;

}

System.out.println("第 " + (i+1) +" 行对比: ");

differMaps(oldL, newL);

}

System.out.println(FileUtil.getFileName(oldPath)+" 对比结束---------------");

System.out.println();

}

private static void differMaps(final String oldMap, final String newMap) {

Map om = JSON.parseObject(oldMap);

Map nm = JSON.parseObject(newMap);

for(Map.Entry entry : om.entrySet()) {

String key = entry.getKey();

Object oldV = om.get(key);

Object newV = nm.get(key);

// 跳过 null 的对比

if(oldV == null && newV == null) {

continue;

}

if(!ObjectUtil.isEquals(oldV, newV)) {

System.out.println("差异列:" + key +", 旧值:" + oldV + ", 新值:" + newV);

}

}

}

这里将差异内容,直接 console 控台输出。

文件夹

当然,我们也可以对比两个文件夹下的内容。

实现如下:public static void differDir(final String oldDir, final String newDir) {

File[] oldFiles = new File(oldDir).listFiles();

for(File file : oldFiles) {

String fileName = file.getName();

String aop = file.getAbsolutePath();

String anp = newDir+fileName;

differ(aop, anp);

}

}

引导类

便利性

上面我们把核心实现都搞定了,但是用户使用起来还是不够方便。因为配置等不够优雅。

所以我们引入引导类,帮助用户快速使用:/**

* @author 老马啸西风

* @since 0.0.1

*/

public class ExportdbBs {

private ExportdbBs(){}

/**

* 导出实现

* @since 0.0.1

*/

private final IExportdb exportdb = new Exportdb();

/**

* 驱动类名称

*/

private String driverName = DriverNameConstant.MYSQL;

/**

* 数据库链接

*/

private String url = "jdbc:mysql://localhost:3306/test";

/**

* 用户名称

*/

private String username = "root";

/**

* 密码

*/

private String password = "123456";

public static ExportdbBs newInstance() {

return new ExportdbBs();

}

public ExportdbBs driverName(String driverName) {

this.driverName = driverName;

return this;

}

public ExportdbBs url(String url) {

this.url = url;

return this;

}

public ExportdbBs username(String username) {

this.username = username;

return this;

}

public ExportdbBs password(String password) {

this.password = password;

return this;

}

/**

* 查询

* @param sql sql

* @return 结果

* @since 0.0.1

*/

public QueryResultVo query(final String sql) {

//1. 构建 vo

JdbcVo jdbcVo = new JdbcVo(driverName, url, username, password);

//2. 获取 mapper

final JdbcMapper jdbcMapper = getJdbcMapper(jdbcVo);

//3. 构建上下文

final ExportdbContext context = ExportdbContext.newInstance().jdbcMapper(jdbcMapper);

return this.exportdb.query(context, sql);

}

/**

* 查询并且处理

* @param queryResultHandler 查询结果处理器

* @param sql sql

* @since 0.0.1

*/

public void queryAndHandle(final IQueryResultHandler queryResultHandler,

final String sql, final String... otherSqls) {

QueryResultVo queryResultVo = this.query(sql);

queryResultHandler.handler(queryResultVo);

// 同理处理其他的 sql

for(String os : otherSqls) {

QueryResultVo vo = this.query(os);

queryResultHandler.handler(vo);

}

}

/**

* 查询并且处理

* @param queryResultHandler 查询结果处理器

* @param sqlList sql 列表

* @since 0.0.2

*/

public void queryAndHandle(final IQueryResultHandler queryResultHandler,

List sqlList) {

// 同理处理其他的 sql

for(String sql : sqlList) {

System.out.println("开始执行:" + sql);

QueryResultVo vo = this.query(sql);

queryResultHandler.handler(vo);

}

}

private JdbcMapper getJdbcMapper(JdbcVo jdbcVo) {

if(DriverNameConstant.MYSQL.equalsIgnoreCase(driverName)) {

return new MySqlJdbcMapper(jdbcVo);

}

if(DriverNameConstant.ORACLE.equalsIgnoreCase(driverName)) {

return new OracleJdbcMapper(jdbcVo);

}

if(DriverNameConstant.SQL_SERVER.equalsIgnoreCase(driverName)) {

return new SqlServerJdbcMapper(jdbcVo);

}

throw new UnsupportedOperationException();

}

}

这里为用户提供了 mysql 最基本的配置,以及常用的查询处理方法。

测试

下面我们来看一下测试的效果:

直接查询QueryResultVo resultVo = ExportdbBs.newInstance().query("select * from user;");

System.out.println(resultVo);

查询并处理final String sql = "select * from user;";

final IQueryResultHandler handler = new FileJsonQueryResultHandler();

ExportdbBs.newInstance().queryAndHandle(handler, sql);

两次导出可以指定文件路径,比如分别是:

D:\exportdb\old\ 和 D:\exportdb\new\

针对两次结果对比final String oldP = "D:\\exportdb\\old\\";

final String newP = "D:\\exportdb\\new\\";

CompareUtil.differDir(oldP, newP);

差异结果就会被输出到控台。

4853fbbc223c5c883fb735ca480bf7b3.png

一切顺利,不过革命尚未成功,同学仍需加班呀~~~

不足之处

这是一个 v0.0.1 版本,还有很多不足。

比如:导出为 csv

导出为 insert/update 语句

导出的文件名称自定义策略

可以指定多个 sql 是否生成在同一个文件中

导出路径根据操作系统,自动变更

更加便于使用,比如页面指定数据源+sql,页面显示对应差异结果。

不过也基本可用,符合我们最初的设想。

小结

不知道你平时又是如何对比数据的呢?

如果你需要这个工具,可以关注【老马啸西风】,后台回复【对比】即可。

希望本文对你有帮助,如果有其他想法的话,也可以评论区和大家分享哦。

各位极客的点赞收藏转发,是老马持续写作的最大动力!

80d4941b3e7050e22b4b03b5def69ac1.png

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

java mysql差异导出_java 如何从零实现一个数据库差异对比工具? 的相关文章

随机推荐

  • sql server 查询表是否被哪些存储过程和视图引用

    select name from sysobjects as s inner join syscomments as o on s id o id where text like N 表名
  • GFS 阅读笔记

    这篇博客是我阅读著名的 GFS 论文 The Google File System 所总结的笔记以及自己一些的思考 这篇论文是一篇非常经典的论文 尤其对于想要了解分布式或者刚刚开始研究分布式的人来说 是一篇非常好的读物 它里面提到了许多分布
  • 使用python代码绘制一个会动的爱心

    你可以使用 matplotlib 库来绘制一个会动的爱心 下面是一个简单的代码示例 import matplotlib pyplot as plt import numpy asnp x np linspace np pi np pi 25
  • Apache Beam中的有状态计算

    Beam帮助我们处理流式 乱序 大规模的数据 并且提供了高度的抽象机制Pipeline 统一了流式和批量数据处理 从功能上流处理可以分为无状态 stateless 的和有状态 stateful 两种 在流处理的框架里引入状态管理大大提升了系
  • mysql怎么打开表ddl_MySQL数据表操作(DDL)

    一 创建数据表 语法 create table 表名称 字段 字段类型 约束 字段 字段类型 约束 表选项 约束 primary key not null auto increment等 表选项 数据表的属性 一般包括engine char
  • linux常用命令及解释大全(三)

    目录 前言 一 字符设置和文件格式转换 二 文件系统分析 三 初始化一个文件系统 四 备份 五 光盘 六 网络 总结 前言 本篇文章继续介绍了一部分linux常用命令 包括字符设置和文件格式转换 文件系统分析 初始化一个文件系统 备份 光盘
  • 模板类,模板类函数特例化,模板类特例化

    今天在看某c 框架源码时 发现模板类里的部分函数可以特例化 感觉很神奇便尝试了下 发现很多平时不会注意的c 细节及知识 写下来分享给大家 以便大家遇到相似问题时可以少踩点坑 模板类会出现链接问题 编译不通过 如果模板类 h文件和 cpp文件
  • matlab_一组数据元素随机排列

    如何生成一组数据元素随机排列 随机打乱顺序 产生新的排列 1 使用的matlab函数 randperm 1 语法格式 R randperm n 2 函数功能 将数字1 n进行随机排列 排列结果存储在行向量内 3 EX 输入 randperm
  • 网页端扫码通过公众号实现微信授权登录

    1 参考开发文档 https developers weixin qq com doc offiaccount OA Web Apps Wechat webpage authorization html 0 2 先调起微信授权页面 获取co
  • Java学习心得2——构造函数

    1 什么是构造函数 构造函数顾名思义 就是对象被构造的时候就会被调用的函数 例如当你new一个Cat对象的时候 Cat对象的构造函数就会被调用 public static void main String args Cat c new Ca
  • (如何读写文件)流-------输入流

    流 流是一组有序的数据序列 通过流来读写文件 流从方向分输入 输出流 流从单位份字节 字符流 字节输入流 InputStream 抽象父类 基类 常用方法 read 几个重载方法作用不同 int read 输入流一个字节一个字节的读 返回的
  • U3D中对象的自动销毁

    很多游戏中都会重用同一个游戏对象 比如说Dota 一波兵一波兵的被对方消灭 如果尸体不会自动销毁 恐怕地图中的尸体堆积如山不说 配置再厉害的电脑也架不住 这样游戏的性能太低了 需要自动销毁的情形有 游戏对象已经并且永远消失在视野中了 天空中
  • Java基础篇——集合

    活动地址 CSDN21天学习挑战赛 1 为什么使用集合 集合与数组类似 都是容器 我们为什么不用数组而还要使用集合呢 数组的特点 数组定义完成并启动后 类型确定 长度固定 不适合元素的个数和类型不确定的业务场景 更不适合做需要增删的操作 数
  • 如何在手机上运行c语言,各位前辈这两个程序怎么在手机上运行

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 made by ebhrz include include include include include include include global define int cfd 文件描述符
  • Python mailmerge库

    Python mailmerge库 本文使用环境 win10 python3 7 office2016 第0步 按装mailmerge pip install mailmerge 第1步 引用库 from mailmerge import
  • 测试用例设计(增量测试)

    增量测试 在执行模块测试过程中 我们主要有两点考虑 第一 如何设计一个有效的测试用例集 第二 将模块组装成工作程序的方式 第二点考虑很重要 因为它涉及模块测试用例编写的形式 可能用到的测试工具类型 模块编码和测试的顺序 生成测试用例的成本以
  • Markdown的十个常用标志符号

    Markdown 是一种轻量级的 标记语言 它的优点很多 目前也被越来越多的写作爱好者 撰稿者广泛使用 看到这里请不要被 标记 语言 所迷惑 Markdown 的语法十分简单 常用的标记符号也不超过十个 这种相对于更为复杂的HTML 标记语
  • Flutter开发之数据存储-2-文件存储(33)

    数据存储部分在移动开发中是非常重要的部分 无论是一些轻量级的数据 如用户信息 APP配置信息等 还是把需要长期存储的数据写入本地文件或者Sqlite3 都离不开数据存储 上一篇SharedPreferences的使用 今天就练习一下文件存储
  • python求平均数、方差、中位数

    CalStatistics py def getNum 获取用户不定长度的输入 nums iNumStr input 请输入数字 回车退出 while iNumStr nums append eval iNumStr iNumStr inp
  • java mysql差异导出_java 如何从零实现一个数据库差异对比工具?

    对比数据的痛苦 不知道你是否也像我一样 在快乐编写代码的时候 必须进行一些数据库的数据对比工作 诚然 一般的数据差异 比如是每一行的内容不同 市场上有比较成熟的 compare2 等对比工具 但是如果是对比数据的每一列是否相同 这个就会变得