检查字段或类型是否匹配

检查实体类字段或类型是否与数据库匹配

需求:要求在项目启动时,对系统中存在的实体类做数据库表结构检查,缺少字段或类型不匹配的要日志打印(单独日志文件)。

思路:通过获取所有配置的路径扫描所有的贴了@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());
                    }

                }
            }

        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值