检查实体类字段或类型是否与数据库匹配
需求:要求在项目启动时,对系统中存在的实体类做数据库表结构检查,缺少字段或类型不匹配的要日志打印(单独日志文件)。
思路:通过获取所有配置的路径扫描所有的贴了@TableName(“xxxdemo”)实体类,然后通过类字节码获取该类的所有字段,将实体类字段和类型关联起来,然后查出对应的表所有的字段和类型,进行一一比较,用日志记录起来缺少的和不匹配的。
1.在logback-spring或日志配置中加入
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<property name="PATTERN" value="%-12(%d{yyyy-MM-dd HH:mm:ss.SSS}) |-%-5level %c [%L] -| %msg%n" />
<!-- 单独记录某个类 -->
<appender name="TableStructCheck" class="ch.qos.logback.core.FileAppender">
<file>TableStructCheck.log</file> <!-- 日志文件路径 -->
<encoder>
<pattern>${PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 单独记录某个类 -->
<logger name="com.xxx.xxx.task.TableStructureChecker" level="INFO" additivity="false">
<appender-ref ref="TableStructCheck" />
</logger>
2.在resources下建一个目录,目录中放config-dev.properties
#需要实体类与数据库字段对比的包路径,以“,”号分隔开
include.path=com.leatop.ops.model
3.业务代码
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xxx.xxx.dao.TableStructureDAO;
import lombok.SneakyThrows;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.*;
@Component
public class TableStructureChecker implements ApplicationListener<ApplicationReadyEvent> {
private static final Logger logger = LoggerFactory.getLogger(TableStructureChecker.class);
String[] StringArray = {
"char", // 固定长度字符串
"varchar", // 可变长度字符串
"text", // 大文本字符串
"json", // 大文本字符串
"tinytext", // 小文本字符串
"mediumtext", // 中等大小文本字符串
"longtext", // 大文本字符串
"blob", // 二进制大对象(可存储二进制数据)
"tinyblob", // 小二进制大对象
"mediumblob", // 中等大小二进制大对象
"longblob",
"point", // 表示一个点
"linestring", // 表示一个线串
"polygon", // 表示一个多边形
"multipoint", // 表示多个点
"multilinestring", // 表示多个线串
"multipolygon", // 表示多个多边形
"geometry", // 表示任意几何类型
"geometrycollection" // 表示几何对象集合
};
String[] integerDataTypes = {
"tinyint", // 存储非常小的整数,范围从 -128 到 127 或从 0 到 255
"smallint", // 存储小范围整数,范围从 -32,768 到 32,767 或从 0 到 65,535
"mediumint", // 存储中等范围整数,范围从 -8,388,608 到 8,388,607 或从 0 到 16,777,215
"int", // 存储标准整数,范围从 -2,147,483,648 到 2,147,483,647 或从 0 到 4,294,967,295
"bigint" // 存储大范围整数,范围从 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 或从 0 到 18,446,744,073,709,551,615
};
String[] booleanDataTypes = {
"tinyint", // MySQL 中的 BOOLEAN 对应的是 TINYINT(1),表示真(1)或假(0)
"bool" // BOOL 是 TINYINT(1) 的别名
};
String[] floatDataTypes = {
"float", // MySQL 中的 FLOAT 类型
"double", // MySQL 中的 DOUBLE 类型,表示双精度浮点数
"decimal", // MySQL 中的 DECIMAL 类型,适用于精确的浮点数
"real" // MySQL 中的 REAL 类型,通常等同于 FLOAT
};
String[] localDateTimeDataTypes = {
"datetime", // MySQL 中的 DATETIME 类型,表示日期和时间
"timestamp", // MySQL 中的 TIMESTAMP 类型,表示时间戳
"date", // MySQL 中的 DATE 类型,表示日期(不包括时间)
"time" // MySQL 中的 TIME 类型,表示时间(不包括日期)
};
@Autowired
private TableStructureDAO tableStructureDAO;
@SneakyThrows
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
checkTableStructures();
}
private void checkTableStructures() throws IOException {
// 加载配置文件
Properties properties = new Properties();
InputStream input = getClass().getClassLoader().getResourceAsStream("filters/config-dev.properties");
properties.load(input);
// 获取参数
String includePath = properties.getProperty("include.path");
String[] split = includePath.split(",");
for (String path : split) {
//把需要扫描的包路径放进去
Set<Class<?>> entityClasses = getEntityClasses(path);
for (Class<?> entityClass : entityClasses) {
checkEntityTable(entityClass);
}
}
}
private Set<Class<?>> getEntityClasses(String path) {
// 1. 使用 Reflections 库扫描指定包
Reflections reflections = new Reflections(path); // 指定你的包名
// 2. 查找所有带有 @TableName 注解的类
Set<Class<?>> entityClasses = reflections.getTypesAnnotatedWith(TableName.class);
return entityClasses; // 替换为你的实体类
}
private void checkEntityTable(Class<?> entityClass) {
String tableName = entityClass.getAnnotation(TableName.class) != null
? entityClass.getAnnotation(TableName.class).value()
: entityClass.getSimpleName(); // 默认使用类名作为表名
//通过类字节码获取该类的所有字段
List<String> entityFields = getEntityFields(entityClass);
//将实体类字段和类型关联起来
Map<String, String> entityFieldsMap = getEntityFieldsMap(entityClass);
//将字段和注解值对应起来 字段名为key 注解的value为value
Map entityMap = getEntityMap(entityClass);
//将表字段和字段类型关联起来
Map columnTypeMap = getColumnType(tableName);
//所有的表字段
Set dbColumns = columnTypeMap.keySet();
//检查对比字段等
inspection(dbColumns,entityFields,entityMap,tableName,columnTypeMap,entityFieldsMap,entityClass);
}
//查询表字段和类型
private Map getColumnType(String tableName) {
List<Map<String, String>> tableMetaData = tableStructureDAO.getTableMetaData(tableName);
// 转换为 Map<String, String>
Map<String, String> fieldTypeMap = new HashMap<>();
for (Map<String, String> row : tableMetaData) {
fieldTypeMap.put(row.get("columnName"), row.get("dataType"));
}
return fieldTypeMap;
}
//通过类字节码获取该类的所有字段
private List<String> getEntityFields(Class<?> entityClass) {
Field[] fields = entityClass.getDeclaredFields();
List<String> fieldNames = new ArrayList<>();
for (Field field : fields) {
//遍历类中所有的TableField注解
if (field.isAnnotationPresent(TableField.class)) {
// 4. 获取 @TableField 注解
TableField tableField = field.getAnnotation(TableField.class);
String value = tableField.value();
//如果为空是没有开启,不用保存@TableField(exist = false)
if (!"".equals(value)){
fieldNames.add(field.getName());
}
}else
//遍历类中所有的TableField注解
if (field.isAnnotationPresent(TableId.class)) {
// 4. 获取 @TableField 注解
TableId tableId = field.getAnnotation(TableId.class);
String value = tableId.value();
//如果为空是没有开启,不用保存@TableField(exist = false)
if (!"".equals(value)){
fieldNames.add(field.getName());
}
}
}
return fieldNames;
}
//通过类字节码获取该类的所有字段和字段类型关联起来
private Map<String,String> getEntityFieldsMap(Class<?> entityClass) {
HashMap<String,String> entityFieldsMap = new HashMap<>();
Field[] fields = entityClass.getDeclaredFields();
for (Field field : fields) {
entityFieldsMap.put(field.getName(),field.getType().getSimpleName());
}
return entityFieldsMap;
}
//通过类字节码获取该类的所有字段
private Map getEntityMap(Class<?> entityClass) {
//字段名为key 注解的value为 value
HashMap<String, String> map = new HashMap<>();
Field[] fields = entityClass.getDeclaredFields();
for (Field field : fields) {
//遍历类中所有的TableField注解
if (field.isAnnotationPresent(TableField.class)) {
// 4. 获取 @TableField 注解
TableField tableField = field.getAnnotation(TableField.class);
// 5. 获取并 value
String value = tableField.value();
//如果为空是没有开启,不用保存@TableField(exist = false)
if (!"".equals(value)){
map.put(field.getName(),value);
}
}else
//遍历类中所有的TableField注解
if (field.isAnnotationPresent(TableId.class)) {
// 4. 获取 @TableField 注解
TableId tableId = field.getAnnotation(TableId.class);
// 5. 获取并 value
String value = tableId.value();
//如果为空是没有开启,不用保存@TableField(exist = false)
if (!"".equals(value)){
map.put(field.getName(),value);
}
}
}
return map;
}
/**
*
* 检查对比字段,检查记录异常
* @param dbColumns 表的列
* @param entityFields 实体类的字段
* @param entityMap 实体类注解的value
* @param tableName 表名
* @param columnTypeMap 表字段和类型
* @param entityFieldsMap 实体类字段和类型
*/
private void inspection(Set<String> dbColumns, List<String> entityFields, Map<String,String> entityMap
,String tableName, Map<String,String> columnTypeMap, Map<String,String> entityFieldsMap,Class<?> entityClass) {
if (entityMap.size()>0){
// 检查缺失字段 实体类和数据库相比,以实体类为主
for (String entityField : entityFields) {
//比较数据缺少
if (entityMap.get(entityField) != null && !dbColumns.contains(entityMap.get(entityField))){
logger.info("字段缺失表名和字段名和类地址分别为: {},{},{},{}", tableName, entityMap.get(entityField),entityField,entityClass.getName());
//比较数据类型
}else if ("String".equals(entityFieldsMap.get(entityField))||"char".equals(entityFieldsMap.get(entityField)) ) {
if (!(Arrays.asList(StringArray).contains(columnTypeMap.get(entityMap.get(entityField))))){
logger.info("类型不匹配,表名和字段名和类地址分别为: {},{},{},{}", tableName, entityMap.get(entityField),entityField,entityClass.getName());
}
} else if ("int".equals(entityFieldsMap.get(entityField)) || "Integer".equals(entityFieldsMap.get(entityField))
|| "long".equals(entityFieldsMap.get(entityField))|| "Long".equals(entityFieldsMap.get(entityField))){
if (!(Arrays.asList(integerDataTypes).contains(columnTypeMap.get(entityMap.get(entityField))))
&&!"serialVersionUID".equals(entityField)){
logger.info("类型不匹配,表名和字段名和类地址分别为: {},{},{},{}", tableName, entityMap.get(entityField),entityField,entityClass.getName());
}
} else if ("boolean".equals(entityFieldsMap.get(entityField)) || "Boolean".equals(entityFieldsMap.get(entityField))){
if (!(Arrays.asList(booleanDataTypes).contains(columnTypeMap.get(entityMap.get(entityField))))){
logger.info("类型不匹配,表名和字段名和类地址分别为: {},{},{},{}", tableName, entityMap.get(entityField),entityField,entityClass.getName());
}
} else if ("float".equals(entityFieldsMap.get(entityField)) || "Float".equals(entityFieldsMap.get(entityField))){
if (!(Arrays.asList(floatDataTypes).contains(columnTypeMap.get(entityMap.get(entityField))))){
logger.info("类型不匹配,表名和字段名和类地址分别为: {},{},{},{}", tableName, entityMap.get(entityField),entityField,entityClass.getName());
}
} else if ("double".equals(entityFieldsMap.get(entityField)) || "Double".equals(entityFieldsMap.get(entityField))){
if (!(Arrays.asList(floatDataTypes).contains(columnTypeMap.get(entityMap.get(entityField))))){
logger.info("类型不匹配,表名和字段名和类地址分别为: {},{},{},{}", tableName, entityMap.get(entityField),entityField,entityClass.getName());
}
} else if ("LocalDateTime".equals(entityFieldsMap.get(entityField)) || "Date".equals(entityFieldsMap.get(entityField))){
if (!(Arrays.asList(localDateTimeDataTypes).contains(columnTypeMap.get(entityMap.get(entityField))))){
logger.info("类型不匹配,表名和字段名和类地址分别为: {},{},{},{}", tableName, entityMap.get(entityField),entityField,entityClass.getName());
}
}
}
}
}
}