解决Spring Data JPA 3.x.x 自动生成表时列顺序混乱的问题

445 阅读3分钟

项目场景

基于 Spring Boot 3.x.x

引入 spring-boot-starter-data-jpa-3.x.x.jar


问题描述

项目中配置了:spring.jpa.hibernate.ddl-auto=update

当使用Spring Data JPA自动生成表的时候,所产生的列顺序与Entity类中定义的顺序不一致,会按照字母序来定义列的顺序。

当查看表中数据时,会有点不太方便;


原因分析

Hibernate 源码中的PropertyContainer类,里面的逻辑使用了无序的数据结构,即:TreeMap。


解决方案

解决思路: 替换Hibernate的实现,相关逻辑换成有序的数据结构,即:LinkedHashMap

老版本解决方案

  1. 在工程中新建org.hibernate.cfg
  2. 找到hibernate-core包下的org.hibernate.cfg包下的PropertyContainer类,复制到本工程的org.hibernate.cfg包下
  3. 把PropertyContainer类中定义的persistentAttributeMap类型,从TreeMap修改为LinkedHashMap

新版本解决方案

注意: 因为 spring-boot-starter-data-jpa-3.x.x.jar 中,依赖的Hibernate版本已经是6.x.x 了,这个PropertyContainer类已经被修改,具体的可以到github上查看,所以在具体解决时,需要注意新老版本的不同;

在我处理的版本里,相关代码如下:

public PropertyContainer(XClass clazz, XClass entityAtStake, AccessType defaultClassLevelAccessType) {
    this.xClass = clazz;
    this.entityAtStake = entityAtStake;

    if ( defaultClassLevelAccessType == AccessType.DEFAULT ) {
        // this is effectively what the old code did when AccessType.DEFAULT was passed in
        // to getProperties(AccessType) from AnnotationBinder and InheritanceState
        defaultClassLevelAccessType = AccessType.PROPERTY;
    }

    AccessType localClassLevelAccessType = determineLocalClassDefinedAccessStrategy();
    assert localClassLevelAccessType != null;

    this.classLevelAccessType = localClassLevelAccessType != AccessType.DEFAULT
            ? localClassLevelAccessType
            : defaultClassLevelAccessType;
    assert classLevelAccessType == AccessType.FIELD || classLevelAccessType == AccessType.PROPERTY
            || classLevelAccessType == AccessType.RECORD;


    final List<XProperty> fields = xClass.getDeclaredProperties( AccessType.FIELD.getType() );
    final List<XProperty> getters = xClass.getDeclaredProperties( AccessType.PROPERTY.getType() );
    final List<XProperty> recordComponents = xClass.getDeclaredProperties( AccessType.RECORD.getType() );

    preFilter( fields, getters, recordComponents );

    final Map<String,XProperty> persistentAttributesFromGetters = new HashMap<>();
    final Map<String,XProperty> persistentAttributesFromComponents = new HashMap<>();

    final Map<String, XProperty> localAttributeMap;
    // If the record class has only record components which match up with fields and no additional getters,
    // we can retain the property order, to match up with the record component order
    if ( !recordComponents.isEmpty() && recordComponents.size() == fields.size() && getters.isEmpty() ) {
        localAttributeMap = new LinkedHashMap<>();
    }
    //otherwise we sort them in alphabetical order, since this is at least deterministic
    else {
        localAttributeMap = new TreeMap<>();
    }
    collectPersistentAttributesUsingLocalAccessType(
            xClass,
            localAttributeMap,
            persistentAttributesFromGetters,
            persistentAttributesFromComponents,
            fields,
            getters,
            recordComponents
    );
    collectPersistentAttributesUsingClassLevelAccessType(
            xClass,
            classLevelAccessType,
            localAttributeMap,
            persistentAttributesFromGetters,
            persistentAttributesFromComponents,
            fields,
            getters,
            recordComponents
    );
    this.persistentAttributes = verifyAndInitializePersistentAttributes( xClass, localAttributeMap );
}

通过上面的源码,大家可以看到这段判断逻辑:

final Map<String, XProperty> localAttributeMap;
// If the record class has only record components which match up with fields and no additional getters,
// we can retain the property order, to match up with the record component order
if ( !recordComponents.isEmpty() && recordComponents.size() == fields.size() && getters.isEmpty() ) {
	localAttributeMap = new LinkedHashMap<>();
}
//otherwise we sort them in alphabetical order, since this is at least deterministic
else {
	localAttributeMap = new TreeMap<>();
}

如果条件满足的话,localAttributeMap 的类型就是 LinkedHashMap,则不会出现问题;

判断条件解释如下: 如果定义的Entity类是 Record类型,就会通过调用 Class 类的 getRecordComponents 方法获取 java.lang.reflect.RecordComponent,然后判断它的数量是否和字段数量相同,且没有getter方法。

注:这里的Record类型 指的是 java 14的新特性,如果不了解的童鞋可以看下面的链接

Java - Record

如果不满足的情况下,即还是 TreeMap。而我现在遇到的情况,就是这种,因为我的项目中定义Entity使用的是Class,且有getter和setter等方法,所以还是需要使用下面的步骤来解决。

解决步骤:

  1. 在工程中新建org.hibernate.boot.model.internal
  2. 找到hibernate-core包下的org.hibernate.boot.model.internal下的PropertyContainer类,复制到本工程的org.hibernate.boot.model.internal包下
  3. 把PropertyContainer类中,定义的localAttributeMap = new TreeMap<>();修改为localAttributeMap = new LinkedHashMap<>();

以上就是我的分享,实测可以解决,如果大家有问题,可以留言交流,欢迎大家点赞和分享,谢谢。