日常数据维护中,在对数据表进行复制或者迁移后,通常需要进行比对,以确保复制或着迁移数据的正确性。本文展示两种数据对比方法:SQL比对以及JDBC编程比对。
适合同库不同的表,支持跨 Schema。假设有 a 和 b 两个表:
1、查询主键一致但其它字段不一致的记录(JOIN 或 INNER JOIN)
select a.*, b.* from a join b on a.id=b.id where a.col1 <> b.col1 or a.col2 <> b.col2 or ...
2、查询A表有B表没有的记录(LEFT JOIN)
select a.* from a left join b on a.id=b.id where b.id is null
3、查询B表有A表没有的记录(RIGHT JOIN)
select b.* from a right join b on a.id=b.id where a.id is null
可同库比对,也可以跨库比对。
1、实施步骤
Step0: 设置3个集合(A表独有主键集合a、B表独有主键集合b、AB表都有但非主键不一致的主键集合ab),一个计数器c(记录完全一致的记录数量),用于记录比对结果。
Step1: 使用流式查询获得2个表查询结果的 ResultSet,例如 rs1(a表) 和 rs2(b表)。查询须按主键正序排列。
Step2: 对比的前置处理:rs1、 rs2 指向第一条记录。
Step3:记录比对:
2、简单的示例代码如下
package org.opoo.demos.tablediff; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.ConnectionCallback; import org.springframework.jdbc.core.StatementCallback; import org.springframework.jdbc.datasource.DataSourceUtils; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.Objects; @Slf4j @SpringBootTest class TableDiffTests { @Autowired private DataSource dataSource; @Test void test() throws SQLException { final String sql1 = "select * from a order by id"; final String sql2 = "select * from b order by id"; final Result result = new Result(); // 不同数据源则支持跨库 try (final Connection conn1 = DataSourceUtils.getConnection(dataSource); final Connection conn2 = DataSourceUtils.getConnection(dataSource); final Statement stmt1 = conn1.createStatement(); final Statement stmt2 = conn2.createStatement()) { // MySQL 设置后底层为 ResultsetRowsStreaming stmt1.setFetchSize(Integer.MIN_VALUE); stmt2.setFetchSize(Integer.MIN_VALUE); try (final ResultSet rs1 = stmt1.executeQuery(sql1); final ResultSet rs2 = stmt2.executeQuery(sql2)) { final Context context = new Context(); boolean more; do { more = compare(context, result, rs1, rs2); } while (more); } } log.info("比对结果:{}", result); Assertions.assertTrue(result.getMatchCount() > 0); } private boolean compare(Context context, Result result, ResultSet rs1, ResultSet rs2) throws SQLException { final Long id1 = get(context.getId1(), () -> getNextId(rs1)); final Long id2 = get(context.getId2(), () -> getNextId(rs2)); if (id1 == null && id2 == null) { return false; } log.debug("id1 = {}, id2 = {}", id1, id2); if (id1 == null) { // 只有 id1 是 null,rs2 剩下的全是 b 独有的,包括 id2 addRemainIds(id2, rs2, result.getIdsOnlyIn2()); return false; } if (id2 == null) { // 只有 id2 是 null,rs1 剩下的全是 a 独有的,包括 id1 addRemainIds(id1, rs1, result.getIdsOnlyIn1()); return false; } // 联合主键可扩展这个方法 final int compareTo = id1.compareTo(id2); if (compareTo < 0) { // id1 还不够大,对不齐,记下来,并取下一个 id1 result.getIdsOnlyIn1().add(id1); context.set(null, id2); } else if (compareTo > 0) { // id2 还不够大,对不齐,记下来,并取下一个 id2 result.getIdsOnlyIn2().add(id2); context.set(id1, null); } else { // ID 是一样大的,则对比其它字段 if (equals(rs1, rs2, id1)) { result.setMatchCount(result.getMatchCount() + 1); } else { result.getIdsMismatch().add(id1); } context.set(null, null); } return true; } private Long getNextId(ResultSet resultSet) throws SQLException { // 联合主键可扩展这个方法 if (resultSet.next()) { return resultSet.getLong("id"); } return null; } private void addRemainIds(Long id, ResultSet rs, List<Long> idsOnlyIn) throws SQLException { idsOnlyIn.add(id); while (rs.next()) { idsOnlyIn.add(rs.getLong("id")); } } private boolean equals(ResultSet rs1, ResultSet rs2, final Long id) throws SQLException { final String[] names = {"col1", "col2", "col3"}; for (String name : names) { final String val1 = rs1.getString(name); final String val2 = rs2.getString(name); if (!Objects.equals(val1, val2)) { log.warn("[{}]的{}字段值不一致:{} <> {}", id, name, val1, val2); return false; } } return true; } private <T> T get(T t, SqlSupplier<T> supplier) throws SQLException { if (t != null) { return t; } return supplier.get(); } @FunctionalInterface public interface SqlSupplier<T> { T get() throws SQLException; } @Data public static class Result { final private List<Long> idsOnlyIn1 = new ArrayList<>(); final private List<Long> idsOnlyIn2 = new ArrayList<>(); final private List<Long> idsMismatch = new ArrayList<>(); private long matchCount = 0; } @Data public static class Context { private Long id1; private Long id2; public void set(Long id1, Long id2) { this.id1 = id1; this.id2 = id2; } } }
Source |
|