dbmock:mock mysql数据的小工具介绍及重构

简介

dbmock是用来mock mysql数据的小工具。接口压测的时候,我们需要生成大量的数据,耗时耗力,dbmock可以通过简单的配置,批量生成表数据,包括关联表的数据。

代码仓库

gitee:https://gitee.com/mamba_tools/dbmock.git

github:https://github.com/qianguangtao/dbmock.git

代码说明

  • jdk 1.8+

  • hutool 5.8.10

  • fastjson 1.2.60

    在这里插入图片描述

配置说明

数据库连接配置

数据库操作使用hutool-db的,详情参看https://doc.hutool.cn/pages/db/index/

mock表信息配置

resources下新建table.json

配置名子配置配置类型配置说明示例
namestring表名
totalintmock表行数
columnList[]列配置
namestring列名
requiredbool是否必填;true|false
foreignKeystring表外键,格式:表名.列名“organ.id”
data多种类型,如下
string使用本表的另外一个字段“nickname”
[]数组中随机取值[0,1,2]
{}时间范围{“start”: “2024-01-01”, “end”: “2024-12-31”}

配置示例:

[
  {
    "name": "rdwh_work_project_info_2024",
    "total": 600000,
    "columnList": [
      {
        "name": "F_work_id",
        "foreignKey": "rdwh_work.F_Id"
      },
      {
        "name": "F_status",
        "data": [
          0,
          1
        ]
      },
      {
        "name": "F_Biz_type",
        "data": [
          "559006923546962181",
          "559006993503758597",
          "559007060453239045",
          "559007119194466565"
        ]
      },
      {
        "name": "F_hour_percentage",
        "required": true
      },
      {
        "name": "F_day",
        "data": {
          "start": "2024-01-01",
          "end": "2024-12-31"
        }
      },
      {
        "name": "F_work_day",
        "data": "F_day"
      } 
    ]
  }
]

应用配置

resources下新建application.properties

配置名配置类型配置说明示例
dbmock.idTypestringinsert主键生成类型String|int
dbmock.batchSizeint批量insert大小,每1000条insert一次1000
dbmock.tableFilestringmock表信息配置的json文件table.json

使用说明

调用 DBMock.mock() 即可

注意:数据库连接用户需要有 SHOW FULL COLUMNS FROM xxx 权限

代码重构

参见分支history

配置封装

优化前

App5.java

    static {
        // idPrefix = IdUtil.simpleUUID().substring(0, 5) + "-";
        idPrefix = "1";
    }

    static Random columnRandom = new Random();
    /** 主键前缀 */
    static String idPrefix;
    /** 批量insert的大小 */
    static Integer BATCH_SIZE = 1000;
    /** 配置文件 */
    static String TABLE_FILE = "table1.json";

优化后

MockConfigProperties.java

public interface MockConfigProperties {
    /**
     * 获取ID前缀
     * @return 返回ID前缀的字符串
     */
    String getIdPrefix();

    /**
     * 获取批量处理大小。
     * @return 批量处理大小的整数值
     */
    Integer getBatchSize();

    /**
     * 获取表格文件路径。
     * @return 表格文件路径的字符串表示
     */
    String getTableFile();
}

MockConfigPropertiesBean implements MockConfigProperties

public class MockConfigPropertiesBean implements MockConfigProperties {

    private final String idType;
    private final Integer batchSize;
    private final String tableFile;
    private final static String ID_TYPE_STRING = "String";
    private final static String ID_TYPE_INT = "int";
    private final static String ID_PREFIX_STRING = IdUtil.simpleUUID().substring(0, 5) + "-";
    private final static String ID_PREFIX_INT = Convert.toStr(new Random().nextInt(999));

    /**
     * MockConfigPropertiesBean的构造函数,用于初始化MockConfigPropertiesBean对象
     * 从application.properties文件中读取配置信息,并设置idType、batchSize和tableFile等属性
     * @throws RuntimeException 如果读取配置文件时发生IO异常,则抛出运行时异常
     */
    public MockConfigPropertiesBean() {
        ClassPathResource cpr = new ClassPathResource("application.properties");
        InputStream inputStream = cpr.getStream();
        Properties properties = new Properties();
        try {
            properties.load(inputStream);
            idType = StrUtil.isNotBlank(properties.getProperty("dbmock.idType")) ? properties.getProperty("dbmock.idType").trim() : "String";
            batchSize = StrUtil.isNotBlank(properties.getProperty("dbmock.batchSize")) ? Integer.parseInt(properties.getProperty("dbmock.batchSize").trim()) : 1000;
            tableFile = StrUtil.isNotBlank(properties.getProperty("dbmock.tableFile")) ? properties.getProperty("dbmock.tableFile").trim() : "table.json";
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String getIdPrefix() {
        if (ID_TYPE_STRING.equals(idType)) {
            return ID_PREFIX_STRING;
        } else {
            return ID_PREFIX_INT;
        }
    }

    @Override
    public Integer getBatchSize() {
        return batchSize;
    }

    @Override
    public String getTableFile() {
        return tableFile;
    }
}

单例模式

MockConfig.java

以单例模式封装所有配置(table.json+application.properties)

@Data
public class MockConfig {
    private List<Table> tablesList;
    private Map<String, Integer> tableTotalMap;
    private ConcurrentHashMap<String, String> dependKeyMap;
    private MockConfigProperties mockConfigProperties;
    private Random columnRandom = new Random();

    /**
     * 从文件中获取表格列表
     * @param fileClassPath 文件的类路径
     * @return 包含表格对象的列表
     * @throws IOException 如果文件读取失败
     */
    private List<Table> getTableListFromFile(String fileClassPath) {
        ClassPathResource cpr = new ClassPathResource(fileClassPath);
        String s = new String(cpr.readBytes());
        return JSON.parseObject(s, new TypeReference<List<Table>>() {
        });
    }

    /**
     * 获取MockConfig的实例对象。
     * @return 返回MockConfig的实例对象
     */
    public static MockConfig getInstance() {
        return InstanceIdGeneratorHolder.instance;
    }

    /**
     * 构造方法,私有构造函数,防止外部实例化。
     * 初始化mockConfigProperties对象,从文件中读取表格列表,并构建表格总数映射表和依赖关系映射表。
     */
    private MockConfig() {
        mockConfigProperties = new MockConfigPropertiesBean();
        tablesList = getTableListFromFile(mockConfigProperties.getTableFile());
        tableTotalMap = tablesList.stream().collect(Collectors.toMap(Table::getName, t -> t.getTotal()));
        dependKeyMap = new ConcurrentHashMap<>();
    }

    private static class InstanceIdGeneratorHolder {
        static MockConfig instance = new MockConfig();
    }

}

工厂模式

简单工厂

优化前

App5.java

   public static List<String> calculateColumnValue(Map<String, Integer> tableTotalMap, List<String> columnNameList, Map<String, Column> columnMap, int index, int total) {
        List<String> valueList = new ArrayList<>();
        Map<String, String> dependKeyMap = new HashMap<>();
        for (String columnName : columnNameList) {
            String columnValue = null;
            Column column = columnMap.get(columnName);
            if (column.getIsKey()) {
                columnValue = getColumnValue(idPrefix + index, column);
            } else if (ObjectUtil.isNotNull(column.getData())) {
                columnValue = calculateColumnValueByDataConfig(column, dependKeyMap);
            } else if (ObjectUtil.isNotNull(column.getForeignKey())) {
                // 处理字段是另一个表的主键,这里table和fkTable是n:1的关系,将fkTable的主键(total)平均分配到table
                String fkTable = column.getForeignKey().split("\\.")[0];
                Integer fkTableTotal = tableTotalMap.get(fkTable);
                int percent = total / fkTableTotal;
                int data = (index - 1) / percent + 1;
                columnValue = getColumnValue(idPrefix + data, column);
            } else {
                // 处理普通的非空字段
                columnValue = getDefaultColumnValue(column);
            }
            dependKeyMap.put(columnName, columnValue);
            valueList.add(columnValue);
        }
        return valueList;
    }

优化后

MockColumnFactory.java

public class MockColumnFactory {
    /**
     * 根据给定的列、总数和索引返回一个抽象的模拟列对象。
     * @param column 给定的列对象
     * @param total  给定的总数
     * @param index  给定的索引
     * @return 返回一个抽象的模拟列对象
     */
    public static AbstractMockColumn getMockColumn(Column column, int total, int index) {
        if (column.getIsKey()) {
            return new PrimaryKeyMockColumn(column, total, index);
        } else if (ObjectUtil.isNotNull(column.getData())) {
            return new DataMockColumn(column, total, index);
        } else if (ObjectUtil.isNotNull(column.getForeignKey())) {
            return new ForeignKeyMockColumn(column, total, index);
        } else {
            return new DefaultMockColumn(column, total, index);
        }
    }
}

AbstractMockColumn.java

@Data
public abstract class AbstractMockColumn {
    private Column column;
    private int total;
    private int index;

    /**
     * 获取列值
     * @return 返回列值的字符串形式
     */
    public abstract String getColumnValue();

    /**
     * 获取列值的insert values表示形式(字符串有'')
     * @param value 列值
     * @return 列值的字符串表示形式
     */
    public String getColumnValue(Object value) {
        if (this.getColumn().getJdbcType().clazz == String.class
                || (this.getColumn().getJdbcType().clazz == LocalDate.class)
                || (this.getColumn().getJdbcType().clazz == LocalDateTime.class)) {
            return "'" + Convert.toStr(value) + "'";
        } else {
            return Convert.toStr(value);
        }
    }

    /**
     * 获取列默认值的字符串表示形式
     * @return 列默认值的字符串表示形式
     */
    public String getDefaultColumnValue() {
        String columnValue;
        if (this.getColumn().getJdbcType().clazz == LocalDate.class) {
            return "'" + DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN) + "'";
        } else if (this.getColumn().getJdbcType().clazz == LocalDateTime.class) {
            return "'" + DateUtil.format(new Date(), DatePattern.NORM_DATETIME_PATTERN) + "'";
        } else if (this.getColumn().getJdbcType() == JdbcType.TEXT) {
            return "'" + this.getColumn().getName() + "'";
        } else {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < this.getColumn().getLength(); i++) {
                sb.append("1");
            }
            columnValue = sb.toString();
        }
        return getColumnValue(columnValue);
    }

    public AbstractMockColumn(Column column, int total, int index) {
        this.column = column;
        this.total = total;
        this.index = index;
    }
}

反射+工厂

优化前

App5.java

    public static String calculateColumnValueByDataConfig(Column column, Map<String, String> dependKeyMap) {
        if (column.getData() instanceof String) {
            // column.getData()是String,则是取该表的其他字段值
            String data = (String) column.getData();
            return dependKeyMap.get(data);
        } else if (column.getData() instanceof JSONArray) {
            // column.getData()是数组,则随机取值
            JSONArray jsonArray = (JSONArray) column.getData();
            Object[] random = jsonArray.stream().toArray();
            return getColumnValue(random[columnRandom.nextInt(random.length)], column);
        } else if (column.getData() instanceof JSONObject) {
            // column.getData()是JSONObject,取开始-结束的随机值
            JSONObject jsonObject = (JSONObject) column.getData();
            String start = jsonObject.getString("start");
            String end = jsonObject.getString("end");
            if (!StrUtil.hasBlank(start, end)) {
                String datePattern;
                if (column.getJdbcType() == JdbcType.DATE) {
                    // 处理日期,取当前日期
                    datePattern = DatePattern.NORM_DATE_PATTERN;
                } else {
                    datePattern = DatePattern.NORM_DATETIME_PATTERN;
                }
                DateTime startTime = DateUtil.parse(start, datePattern);
                DateTime endTime = DateUtil.parse(end, datePattern);
                long between = DateUtil.between(startTime, endTime, DateUnit.DAY);
                DateTime dateTime = startTime.offsetNew(DateField.DAY_OF_YEAR, columnRandom.nextInt(Convert.toInt(between + 1)));
                return getColumnValue(DateUtil.format(dateTime, datePattern), column);
            }
        } else {
            throw new IllegalArgumentException("Invalid column value");
        }
        return null;
    }

优化后

ColumnConfigDataFactory.java

public class ColumnConfigDataFactory {
    static {
        Map<String, AbstractColumnConfigData> map = new HashMap<>(16);
        // 取类包名.分割后的第一个作为待扫描包
        String packageName = ColumnConfigDataFactory.class.getName().split("\\.")[0];
        Set<Class<? extends AbstractColumnConfigData>> classSet = ReflectUtil.scanClassBySuper(packageName, AbstractColumnConfigData.class);
        classSet.forEach(clazz -> {
            AbstractColumnConfigData columnConfigData = null;
            try {
                columnConfigData = clazz.newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            if (ObjectUtil.isNotNull(columnConfigData)) {
                map.put(columnConfigData.getDataClazz(), columnConfigData);
            }
        });
        beanMap = map;
    }

    static Map<String, AbstractColumnConfigData> beanMap;

    public static AbstractColumnConfigData getColumnConfigData(AbstractMockColumn mockColumn) {
        String dataClazz = mockColumn.getColumn().getData().getClass().getName();
        AbstractColumnConfigData columnConfigData = beanMap.get(dataClazz);
        if (ObjectUtil.isNull(columnConfigData)) {
            throw new RuntimeException("未找到对应的列数据配置类");
        }
        columnConfigData.setMockColumn(mockColumn);
        return columnConfigData;
    }
}

AbstractColumnConfigData.java

public abstract class AbstractColumnConfigData {
    /**
     * 获取数据类型的Class全路径。
     * @return 返回数据类型的Class全路径
     */
    abstract public String getDataClazz();

    @Getter
    @Setter
    protected AbstractMockColumn mockColumn;

    /**
     * 获取列的原始值。
     * @return 返回列的原始值的字符串表示形式。
     */
    abstract public String getColumnValueByDataConfig();

    public AbstractColumnConfigData() {
        super();
    }
}
  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值