Search code examples
grailsmappinggrails-orm

Grails GORM default sort field which is also a field of a composite key


My Grails is version 2.5.0

I have two domain classes:

import org.apache.commons.lang.builder.HashCodeBuilder

class TemplateHeader implements  Serializable {
  int headerId
  int ver
  String templateName
  String extraDescription
  String serviceType
  String createdBy
  boolean isActive
  String updatedBy
  Date dateCreated
  Date lastUpdated

  static hasMany = [templateItems: TemplateItem]

  static mapping = {
    id composite: ['headerId', 'ver']
    ver             comment:''
    templateName    length: 40
    serviceType     length: 20
    extraDescription length: 60
    isActive        defaultValue:false
    templateItems   sort:'itemNo', order:'asc'
  }
  static constraints = {
    headerId unique: 'ver'
    templateName nullable: false
    serviceType nullable: false
  }

  boolean equals(other) {
    if (!(other instanceof TemplateItem)) {
        return false
    }
    other.id == id && other.ver == ver
  }

  int hashCode() {
    def builder = new HashCodeBuilder()
    builder.append id
    builder.append ver
    builder.toHashCode()
  }

}

=======================================================

import org.apache.commons.lang.builder.HashCodeBuilder

class TemplateItem implements Serializable {
    int itemNo
    String itemName;
    String unitName;
    int unitPrice;
    double defaultValue
    boolean allowChange
    String extraComment
    String createdBy
    String updatedBy
    Date dateCreated 
    Date lastUpdated


    static belongsTo = [templateHeader:TemplateHeader]
    static mapping = {
        id composite: [ 'templateHeader',  'itemNo']
        itemNo      comment:''
        itemName     length: 60
        unitName     length: 4
        unitPrice    comment:''
        extraComment length: 60
        defaultValue comment:''
        allowChange  comment:''
    }
    static constraints = {
        itemName    nullable: false
        unitName    nullable: false
        extraComment    nullable: true
        defaultValue    nullable: false
    }

    boolean equals(other) {
        if (!(other instanceof TemplateItem)) {
            return false
        }
        other.itemNo == itemNo && other.templateHeaderId == templateHeaderId
    }

    int hashCode() {
        def builder = new HashCodeBuilder()
        builder.append templateHeaderId
        builder.append itemNo
        builder.toHashCode()
    }
}

When I run the grails application, it shows the below Exception when build tables:

|Running Grails application
context.GrailsContextLoaderListener Error initializing the application: Error creating bean with name 'transactionManagerPostProcessor': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManager': Cannot resolve reference to bean 'sessionFactory' while setting bean property 'sessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionFactory': Invocation of init method failed; nested exception is org.codehaus.groovy.grails.exceptions.GrailsDomainException: property from sort clause not found: accudata_portal.TemplateItem.itemNo
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManagerPostProcessor': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManager': Cannot resolve reference to bean 'sessionFactory' while setting bean property 'sessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionFactory': Invocation of init method failed; nested exception is org.codehaus.groovy.grails.exceptions.GrailsDomainException: property from sort clause not found: accudata_portal.TemplateItem.itemNo
    at java.util.concurrent.FutureTask.run(FutureTask.java:262)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'transactionManager': Cannot resolve reference to bean 'sessionFactory' while setting bean property 'sessionFactory'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionFactory': Invocation of init method failed; nested exception is org.codehaus.groovy.grails.exceptions.GrailsDomainException: property from sort clause not found: accudata_portal.TemplateItem.itemNo
    ... 4 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionFactory': Invocation of init method failed; nested exception is org.codehaus.groovy.grails.exceptions.GrailsDomainException: property from sort clause not found: accudata_portal.TemplateItem.itemNo
    ... 4 more
Caused by: org.codehaus.groovy.grails.exceptions.GrailsDomainException: property from sort clause not found: accudata_portal.TemplateItem.itemNo
    ... 4 more
Error |
Forked Grails VM exited with error

Does somebody tell me how to fix it ? Thank you


Solution

  • I wonder why your'e not getting errors for fields that you are using in mapping closure and for which you have not defined any mappings like itemNo, unitPrice, defaultValue etc.

    Also the equals condition in your TemapletHeader domain should be:

    if (!(other instanceof TemplateHeader))
    

    Now comming to your question, you are getting this error because the itemNo property is inside TemplateItem domain not in TemplateHeader domain. Also the sort keyword accepts a map.

    To sort TemplateItem objects based on the itemNo field, you would have to define the sort property in mapping closure for TemplateItem domain

    static mapping = {
        .
        .
        .
        sort 'itemNo': 'asc'
    }
    

    Now whenever you will make a query to TemplateItem, the default sorting will be on itemNo field. So if you use

    TemplateItem.findAllByTemplateHeader(templateHeaderInstance)
    

    it will give sorted TemplateItems based on itemNo.

    By defining the sort property in TemplateItem domain will not sort the templateItems field in TemplateHeader domain. Reason for which is that while fetching an association for a domain object Grails ignores the sort field for that domain. If you look at the sql being generated while fetching the TemplateHeader object and its associations you will see the difference.

    Apparently there is no direct way for what you want to achieve, depending upon whether do you want to sort TemapleHeader objects based on templateItems or just sort TemplateItems within a single TemplateHeader object

    To sort TemplateHeader objects based on the TemplateItems, add below sort property to mapping closure of TemplateHeader domain:

    static mapping = {
        .
        .
        .
        sort 'templateItems.itemNo': 'asc'
    }
    

    And when you will execute this GORM

    TemplateHeader header = TemplateHeader.list()
    

    it generates following sql

    select this_.header_id as header_i1_0_1_, this_.ver as ver2_0_1_, .... from template_header this_ inner join template_item templateit1_ on this_.header_id=templateit1_.template_header_header_id and this_.ver=templateit1_.template_header_ver order by templateit1_.item_no asc
    

    Now the problem with this query is that its execution cost is very high. It is using inner join between TemplateHeader and TemplateItem domains. So if you have 2 records in TemplateHeader domain and 10 in TemplateItem domain, it will fetch 10 record while you need only 2. Another issue is that templateItems property will still be fetched lazily. So while you do header.templateItems, it will again make a query to database and the result will not be sorted based on itemNo. sql will look like this:

    select templateit0_.template_header_header_id as template1_0_0_, templateit0_.template_header_ver as template2_0_0_, ... from template_item templateit0_ where templateit0_.template_header_header_id=? and templateit0_.template_header_ver=?
    

    So if you want to sort the templateItems for a TemplateHeader object, you can over ride the getter for templateItems and sort the result there:

    Set<TemplateItem> getTemplateItems() {
        return templateItems?.sort { item_1, item_2 -> item_1?.itemNo <=> item_2?.itemNo }
    }
    

    Just in case if you get an error like this while adding TemplateItem object to TemplateHeader object:

    No signature of method: TemplateHeader.addToTemplateItems() is applicable for argument types: (TemplateItem)
    

    then try with declaring templateItems as List.