简介
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
配置名 | 子配置 | 配置类型 | 配置说明 | 示例 |
---|---|---|---|---|
name | string | 表名 | ||
total | int | mock表行数 | ||
columnList | [] | 列配置 | ||
name | string | 列名 | ||
required | bool | 是否必填; | true|false | |
foreignKey | string | 表外键,格式:表名.列名 | “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.idType | string | insert主键生成类型 | String|int |
dbmock.batchSize | int | 批量insert大小,每1000条insert一次 | 1000 |
dbmock.tableFile | string | mock表信息配置的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();
}
}