从 Hibernate 3/Grails 2.2.4 迁移到 Hiberate 5/Grails 3.2.4 时出现 MappingException

MappingException when migrating from Hibernate 3 / Grails 2.2.4 to Hiberate 5 / Grails 3.2.4

我有一个使用 grails 2.2.4 和 hibernate 3 的项目。我正在将它迁移到使用 hibernate 5 的 grails 3.2.4。我从头开始创建一个 grails 3.2.4 项目,现在我正在慢慢地从旧项目中复制一小部分代码并让它运行起来。起初,我只复制了我的用户、用户角色和角色 类 并让我的整个 spring 安全登录流程正常工作。然后,我复制了剩余的域对象并尝试启动我的项目。我的域对象使用 GORM 映射,而不是 JPA 注释。该项目现在甚至不会启动,并在启动时出现以下异常(我链接到 public 要点,因为内容对于堆栈溢出允许的内容来说太长了)。

https://gist.github.com/schmickie/10522d3a2b8a66b6fb79f76e2af0fd72

我在网上搜索了一些错误,我能找到的所有内容都表明它与设置复合主键的问题有关。但是,我没有使用任何复合主键。任何人都知道出了什么问题?错误中引用的域对象 Api 和方法如下所示。

class Api implements Serializable {

    String name
    boolean enabled
    String apiVersion
    String swaggerVersion
    String resourcePath
    String jsonData
    boolean https = true
    String regionSource
    String contractName
    String contractBasePath

    Date dateCreated
    Date lastUpdated
    Date deprecationDate

    static hasMany = [methods: Method, models: Model, apiServers: ApiServer]

    static mapping = {
        cache true
    }

    static constraints = {
        name(nullable: false, blank: false)
        enabled()
        apiVersion(nullable: true, blank:  true)
        resourcePath(nullable: true, blank: true)
        swaggerVersion(nullable: true, blank:  true)
        jsonData(type: 'text', nullable: true, blank: true)
        dateCreated()
        lastUpdated()
        deprecationDate(nullable: true)
        regionSource(nullable: true)
        contractName(nullable: true)
        contractBasePath(nullable: true)
    }

    public Method getMethod(String name) {
        for (Method method : methods) {
            if (method.name.equals(name)) {
                return method
            }
        }
        return null
    }
}

class Method implements Serializable, Comparable<Method> {

    String name
    boolean enabled
    String edgePath
    HttpMethodEnum edgeHttpMethod
    String servicePath
    HttpMethodEnum serviceHttpMethod
    String summary
    String notes
    boolean deprecatedMethod
    String responseClass
    SortedSet<EdgeParameter> edgeParameters
    SortedSet<ServiceParameter> serviceParameters
    Date dateCreated
    Date lastUpdated
    String publicResponseTransformScript
    String baseResponseTransformScript
    String regionFieldName
    String platformFieldName
    String apiKeyFieldName
    String uriTransformScript
    long cacheExpiry

    static belongsTo = [api: Api]
    static hasMany = [edgeParameters: EdgeParameter, serviceParameters: ServiceParameter, errorResponses: ApiErrorResponse]


    static mapping = {
        cache true
        errorResponses sort: 'code', order: "asc"
    }

    static constraints = {
        name(blank: false)
        enabled()
        edgePath(nullable: false, blank: false)
        edgeHttpMethod(nullable: false, blank: false)
        servicePath(nullable: false, blank: false)
        serviceHttpMethod(nullable: false, blank: false)
        summary(nullable: false, blank: false)
        notes(nullable: true, blank: true)
        deprecatedMethod()
        responseClass(nullable: true, blank: true)
        publicResponseTransformScript(nullable: true)
        baseResponseTransformScript(nullable: true)
        regionFieldName(nullable: true)
        platformFieldName(nullable: true)
        apiKeyFieldName(nullable: true)
        uriTransformScript(nullable: true)
        cacheExpiry(nullable: false, blank: true)
    }
}

好吧,我明白了。我对 hibernate/GORM 初始化代码做了很多单步执行。我发现问题实际上出在同一 class 中的 无关 映射。在方法 class 中,还有另一个如上所示的映射,称为 errorResponses。当调用方法 DefaultGrailsDomainClass.establishRelationshipForCollection 来建立 Api 和方法 class 之间的关系时,它会调用另一个方法 GrailsClassUtils.getPropertiesOfType。此 class 遍历方法 class 的所有属性,并尝试查找其类型与 api 关联类型匹配的任何属性。这是方法实现:

public static PropertyDescriptor[] getPropertiesOfType(Class<?> clazz, Class<?> propertyType) {
    if (clazz == null || propertyType == null) {
        return new PropertyDescriptor[0];
    }

    Set<PropertyDescriptor> properties = new HashSet<PropertyDescriptor>();
    try {
        for (PropertyDescriptor descriptor : BeanUtils.getPropertyDescriptors(clazz)) {
            Class<?> currentPropertyType = descriptor.getPropertyType();
            if (isTypeInstanceOfPropertyType(propertyType, currentPropertyType)) {
                properties.add(descriptor);
            }
        }
    }
    catch (Exception e) {
        // if there are any errors in instantiating just return null for the moment
        return new PropertyDescriptor[0];
    }
    return properties.toArray(new PropertyDescriptor[properties.size()]);
}

它找到的第一个 属性 是 api 属性,它通过了 isTypeInstanceOfPropertyType 检查并被添加到 properties 集合。由于名为 getApiErrorResponse 的辅助方法命名不当,对 BeanUtils.getPropertyDescriptors(clazz) 的调用在 returned 集合中包含了一个名为 apiErrorResponse 的字段。但是,在我的方法 class 中没有这样的字段(有一个名为 errorResponses 的映射关联,但没有名为 apiErrorResponse 的关联)。因此,此 non-existent 字段的 PropertyDescriptorpropertyType 为空。然后,当使用 currentPropertyTypenull 值调用 isTypeInstanceOfPropertyType 时,它会抛出 NullPointerException。这导致 getPropertiesOfType 方法 returning new PropertyDescriptor[0],如 catch 子句中所示。稍后,return 映射到 api 的 属性 描述符的失败导致为关联生成不正确的 OneToOne 映射类型,并且该映射也缺少映射字段.

基本上这是一个 grails 代码没有提供足够的信息来说明真正出错的情况。它默默地抑制了另一个字段上发生的 NullPointerException,而另一个字段上的错误导致 api 字段的初始化不正确。这意味着后来的错误消息是错误的抱怨,因为 information/configuration 被假定为丢失,而实际上它没有丢失,导致工程师非常困惑。

我的最终解决方法是将 getApiErrorResponse 辅助方法从 Method class 移到 MethodService 中。请注意,在我的域对象中使用此辅助方法在我从中迁移的旧 Grails 项目(带有 Hiberate 3 的 Grails 2.2.4)中运行良好,因此可能是最新版本 Grails/GORM/Spring/Groovy 中的新行为假设所有 get* 方法都映射到字段。我不知道其中哪一个是罪魁祸首,但它们的版本都比我的旧项目新。

我还向 grails-core 提交了一个 PR,以便在发生此类异常时记录有用的错误消息,而不是静静地吞下它们。

https://github.com/grails/grails-core/pull/10400