I have a project that uses grails 2.2.4 and hibernate 3. I am migrating it over to grails 3.2.4, which uses hibernate 5. I started by creating a grails 3.2.4 project from scratch, and now I am slowly copying over small bits of the code from the old project and getting it working. At first, I only copied over my User, UserRole, and Role classes and got my entire spring security login flow working. Then, I copied over my remaining domain objects and tried to start my project. My domain objects use GORM mappings, rather than JPA annotations. The project won't even start now and gets the following exception on start up (I linked to a public gist because the content is too long for what stack overflow allows).
https://gist.github.com/schmickie/10522d3a2b8a66b6fb79f76e2af0fd72
I did some searching online for the error and everything I can find says it's related to issues with setting up composite primary keys. However, I am not using any composite primary keys. Anyone have any ideas what's going wrong? The domain objects referenced in the error, Api and Method, are shown below.
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)
}
}
Well, I figured it out. I did a lot of stepping through the hibernate/GORM initialization code. I found that the problem is actually with an unrelated mapping in the same class. In the Method class, there is another mapping shown above called errorResponses
. When the method DefaultGrailsDomainClass.establishRelationshipForCollection
is called to establish the relationship between the Api and Method classes, it calls another method GrailsClassUtils.getPropertiesOfType
. This class iterates over all of the properties of the Method class and tries to find any whose type matches the type of the api
association. Here is the method implementation:
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()]);
}
The first property it finds is the api
property, which passes the isTypeInstanceOfPropertyType
check and is added to the properties
Set. Due to a poorly named helper method named getApiErrorResponse
, the call to BeanUtils.getPropertyDescriptors(clazz)
included a field called apiErrorResponse
in the returned collection. However, there is no such field in my Method class (there is a mapped association called errorResponses
, but nothing called apiErrorResponse
). Thus, the propertyType
of the PropertyDescriptor
for this non-existent field is null. Then, when isTypeInstanceOfPropertyType
is called with the null
value for currentPropertyType
, it throws a NullPointerException. This results in the getPropertiesOfType
method returning new PropertyDescriptor[0]
as shown in the catch
clause. Later down the line, this failure to return the property descriptor that maps to api
causes an incorrect OneToOne mapping type to be generated for the association and that mapping is missing a mapped field as well.
Basically this was a case of the grails code not providing enough information about what really went wrong. It silently suppressed the NullPointerException that was occuring on another field, and the error on that other field was causing improper initialization of the api
field. That meant the later error messages were improperly complaining because information/configuration was presumed to be missing when it wasn't, leading to a very confused engineer.
My ultimate fix was to move the getApiErrorResponse helper method out of the Method class and into MethodService. Note that having this helper method in my domain object worked fine in my old Grails project (Grails 2.2.4 with Hiberate 3) that I was migrating from, so it is presumably new behavior in the latest version of Grails/GORM/Spring/Groovy to assume that all get* methods map to fields. I don't know which of these would be the culprit, but they are all on newer versions than my old project.
I also submitted a PR to grails-core to log helpful error messages when these kinds of exceptions occur instead of silently swallowing them.