Changeset 642 for trunk/src


Ignore:
Timestamp:
Sep 5, 2010, 8:06:53 PM (14 years ago)
Author:
gav
Message:

HqlBuilder and tests, draftB.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/groovy/HqlBuilder.groovy

    r641 r642  
    1616/**
    1717 * Provides a DSL for building and managing HQL strings.
    18  * For usage examples see the HqlBuilderTests.
     18 * For more usage examples see the HqlBuilderTests.
    1919 * HQL reference see http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/queryhql.html
     20 *
     21 * DML reference see http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/batch.html#batch-direct
     22 * DML-style clauses: "update, delete, insert into" may not be well tested, the "set" clause has basic implementation.
     23 * So double check the q.query output for these cases.
    2024 *
    2125 * Primary goals:
     
    2529 * 4. Easy to change a clause and execute again.
    2630 *
     31 * Basic usage:
     32 *    def q = new HqlBuilder().query {
     33 *        select 'count(distinct book)'
     34 *        from 'Book as book'
     35 *        where 'book.id > 100'
     36 *            and 'book.inStock = true'
     37 *    }
     38 *    def totalCount = Book.executeQuery(q.query, q.namedParams, q.paginateParams)[0].toInteger()
     39 *    q.select = 'distinct book'
     40 *    def list = Book.executeQuery(q.query, q.namedParams, q.paginateParams)
     41 *
     42 *    def bookList = new PagedResultList(list, totalCount)
     43 *    log.debug '\n' + q.printFormattedQuery
     44 *
    2745 * @author Gavin Kromhout
    28  * @version DraftA
     46 * @version DraftB
    2947 *
    3048 */
    3149class HqlBuilder {
    3250
    33     // Query clauses.
    34     // Each clause is a key with a list of terms.
     51    // HQL clauses.
     52    // Each clause is a map key with a list of terms.
    3553    def clauses = [:]
    3654
     
    4260    def paginateParams = [max: 1000, offset: 0]
    4361
    44     // It is easier and more flexible to simply add order as a clause, e.g: order 'by id asc'
    45     //def sort = "" // e.g. instanceName.id
    46     //def order = "" // e.g. asc or desc
    47 
    48     def HqlBuilder(debug = false) {
     62    // The where clause terms are handled separately from other clauses
     63    // and are a list of logicalTerms.
     64    // The where clause is built by buildWhereClause.
     65    def whereClauseTerms = []
     66
     67    // LogicalIndexStack holds indexes of the current whereClauseTerm nesting.
     68    def logicalIndexStack = []
     69
     70   def logicalBuilders = [AND: 'and',
     71                                        OR: 'or']
     72
     73    def nestingStack = []
     74
     75    // Sort and Order.
     76    // It is easier and more flexible to simply add order as a clause, e.g: order 'by name desc, id asc'
     77    // def sort = "" // e.g. instanceName.id
     78    // def order = "" // e.g. asc or desc
     79
     80    /**
     81     * Constructor.
     82     * Any property that exists (or responds) in the class may be supplied as an argument.
     83     * E.g: max:20, offset:10, debug:true
     84     * The debug property does not really exist, but if true and no external log property
     85     * has been setup then the internal mockLogger will be configured in debug mode.
     86     *
     87     * @param args A map of arguments, defaults to an empty map.
     88     *
     89     */
     90    def HqlBuilder(Map args = [:]) {
     91        args.each { arg ->
     92            def argKey = arg.key.toLowerCase()
     93            if(super.hasProperty(argKey))
     94                this[argKey] = arg.value
     95        }
    4996        if(!super.metaClass.hasMetaProperty('log'))
    50             mockLogging(debug)
     97            mockLogging(args.debug)
    5198        log.debug "HqlBuilder()"
    5299    }
    53100
     101    /**
     102     * Call with no args.
     103     *  Has no real use other than to prevent obscure errors.
     104     */
    54105    def call() {
    55106        log.debug "call()"
     
    69120     *  Has no real use other than to prevent obscure errors
    70121     * when user makes a call to query() and Groovy calls query(Closure cl)
     122     *
    71123     * @returns This object.
     124     *
    72125     */
    73126    def query() {
     
    78131    /**
    79132     * Domain specific build method.
    80      * A typically used build call, e.g: def q = new HqlBuilder().query { }
    81      *
    82      * @param cl The supplied Closure.
     133     * The recommended build call, e.g: def q = new HqlBuilder().query { }
     134     *
     135     * @param cl The closure that will be used to build the query.
    83136     * @returns This object.
    84137     *
     
    92145    /**
    93146     * InvokeMethod resolves all undefined methods.
    94      * Which include the clause methods, e.g select 'book' is equivalent to select('book').
     147     * Which include the clause methods, e.g: select 'book' is equivalent to select('book').
    95148     * Note that defined methods will be called directly since this class does not implement GroovyInterceptable.
    96149     * If class was "HqlBuilder implements GroovyInterceptable" then even println would be intercepted and
    97150     * several exlusions might be needed. e.g: if(methodName != 'call' && methodName != 'println')
    98151     */
    99     def invokeMethod(String name, args) {
     152    def invokeMethod(String methodName, args) {
    100153
    101154        log.debug "invokeMethod(${methodName}, ${args})"
    102155
    103         if(!this.clauses[name])
    104             this.clauses[name] = []
     156        // Call any closures first, that way the nesting is handled and we just keep a reference.
     157        if(args[-1] instanceof Closure) {
     158            handleClosure(args[-1], methodName)
     159            args = args.minus(args[-1])
     160        }
     161
     162        if(!clauses.containsKey(methodName) && !isLogicalBuilder(methodName))
     163            clauses[methodName] = []
     164
     165        if(args) {
     166            if(isWhereClauseBuilder(methodName)) {
     167                logicalBuilder(methodName, args)
     168                return
     169            }
     170        }
    105171
    106172        for(arg in args) {
    107             if(arg instanceof String)
    108                 this.clauses[name] << arg
    109         }
    110 
    111         if(args[-1] instanceof Closure)
    112             handleClosure(args[-1])
     173            if(arg instanceof String || arg instanceof GString)
     174                clauses[methodName] << arg
     175        }
    113176
    114177    } // invokeMethod()
     
    119182     * and clauses to be removed, e.g: q.order = null
    120183     */
    121     def propertyMissing(String name, value) {
     184    def propertyMissing(String propertyName, value) {
    122185        log.debug "propertyMissing(${propertyName}, ${value})"
     186
    123187        if(value == null) {
    124             clauses.remove(name)
     188            removeClause(propertyName)
     189            if(propertyName.toLowerCase() == 'where')
     190                whereClauseTerms.clear()
    125191            return
    126192        }
    127193
    128         if(value instanceof String)
    129             clauses[name] = [value]
    130     }
     194        if(!clauses.containsKey(propertyName))
     195            clauses[propertyName] = []
     196
     197        // Occurs when user assigns to where clause, e.g: q.where = 'book.id > 100'
     198        if(propertyName.toLowerCase() == 'where') {
     199            whereClauseTerms.clear()
     200            logicalBuilder(propertyName, [value])
     201            return
     202        }
     203
     204        if(value instanceof String || value instanceof GString)
     205            clauses[propertyName] = [value]
     206    } // propertyMissing(String propertyName, value)
    131207
    132208    /**
     
    135211     * Since clauses is a Map null is simply returned for a non-existant clause.
    136212     */
    137     def propertyMissing(String name) {
    138         log.debug "propertyMissing(${name})"
    139         clauses[name]
     213    def propertyMissing(String propertyName) {
     214        log.debug "propertyMissing(${propertyName})"
     215
     216        if(!clauses.containsKey(propertyName))
     217            clauses[propertyName] = []
     218
     219        // Occurs when user performs an operation on where clause.
     220        // E.g: q.where << "book.id = 100" which is actually NOT a supported operation since
     221        // calling the method provides the correct function e.g: q.where "book.id > 100".
     222        // Also allows `println q.where` to be short hand for `println q.whereClauseTerms`
     223        if(propertyName.toLowerCase() == 'where') {
     224            return whereClauseTerms
     225        }
     226
     227        clauses[propertyName]
     228    } // propertyMissing(String propertyName)
     229
     230    def setMax(Integer value) {
     231        paginateParams.max = value
     232    }
     233
     234    def getMax() {
     235        paginateParams.max
     236    }
     237
     238    def setOffset(Integer value) {
     239        paginateParams.offset = value
     240    }
     241
     242    def getOffset() {
     243        paginateParams.offset
     244    }
     245
     246    /**
     247     * RemoveClause.
     248     * Allows clauses to be removed, e.g: q.removeClause('order')
     249     *
     250     * @param clauseName The clause to remove.
     251     *
     252     */
     253    def removeClause(String clauseName) {
     254            clauses.remove(clauseName)
     255    }
     256
     257    /**
     258     * BuildWhereClause.
     259     * Build the where clause from whereClauseTerms.
     260     */
     261    def buildWhereClause(printFormat = false) {
     262        //log.debug "buildWhereClause()"
     263
     264        if(!whereClauseTerms)
     265            return ''
     266
     267        def whereClause = 'where '
     268
     269        def buildExpression // declared separately to allow recurrsion.
     270        buildExpression = { term ->
     271            def result = ''
     272            def termCount = term.expressions.size()
     273            if(termCount > 1) {
     274                term.expressions.eachWithIndex { t, index ->
     275                    if(index == 0)
     276                        result += buildExpression(t)
     277                    else if(printFormat)
     278                        result += " \n\t${t.logic} ${buildExpression(t)}"
     279                    else
     280                        result += " ${t.logic} ${buildExpression(t)}"
     281
     282                }
     283                result = "( "+result+" )"
     284            }
     285            else {
     286                if(term.expressions[0] instanceof Map)
     287                    result += "${term.expressions[0].expressions[0]}"
     288                else
     289                    result += "${term.expressions[0]}"
     290            }
     291            return result
     292        }
     293
     294        whereClauseTerms.eachWithIndex { tm, index ->
     295            if(index == 0)
     296                whereClause += buildExpression(tm)
     297            else if(printFormat)
     298                whereClause += " \n\t${tm.logic} ${buildExpression(tm)}"
     299            else
     300                whereClause += " ${tm.logic} ${buildExpression(tm)}"
     301        }
     302
     303        return whereClause
     304    } // buildWhereClause(printFormat = false)
     305
     306    /**
     307     * LogicalBuilder.
     308     * Build the whereClauseTerms
     309     * by appending logicalTerms to the appropriate expressions.
     310     */
     311    def logicalBuilder(logicalName, args) {
     312        log.debug "logicalBuilder(${logicalName}, ${args})"
     313        log.debug "logicalIndexStack: ${logicalIndexStack}"
     314
     315        def logic = getLogicalString(logicalName)
     316
     317        for(arg in args) {
     318            if(arg instanceof String || arg instanceof GString) {
     319                arg = arg.trim()
     320                if(arg) { // prevent empty strings being added.
     321                    if(logicalIndexStack.size() > 0) {
     322                        // Append to current index position.
     323                        whereClauseTerms[logicalIndexStack[-1]].expressions << logicalTerm(logic, arg)
     324                    }
     325                    else {
     326                        // Append to 'root'.
     327                        whereClauseTerms << logicalTerm(logic, null) // empty expression logicalTerm.
     328                        whereClauseTerms[-1].expressions << logicalTerm(logic, arg) // append logicalTerm to expressions
     329                    }
     330                } // if(arg)
     331            } // if(arg instanceof)
     332        } // for
     333
     334    } // logicalBuilder(logicalName, args)
     335
     336    /**
     337     * LogicalTerm.
     338     * A logicalTerm is a map object that holds the logic and list of expressions of a whereClauseTerm.
     339     */
     340    def logicalTerm = { logic, expression ->
     341        expression = expression ? [expression] : []
     342        ['logic': getLogicalString(logic), 'expressions': expression]
     343    }
     344
     345    /**
     346     * GetLogicalString.
     347     *
     348     * @param logicalName The name to get the matching logicalBuilder string for.
     349     */
     350    private getLogicalString(logicalName) {
     351
     352        switch(logicalName.toLowerCase()) {
     353            case 'where':
     354                logicalBuilders.AND
     355                break
     356            case logicalBuilders.AND:
     357                logicalBuilders.AND
     358                break
     359            case logicalBuilders.OR:
     360                logicalBuilders.OR
     361                break
     362        }
     363
    140364    }
    141365
     
    144368     * Setting delegate and DELEGATE_FIRST allows closure to access this object's properties first.
    145369     */
    146     private handleClosure(Closure cl) {
     370    private handleClosure(Closure cl, String methodName = 'root') {
     371        log.debug "handleClosure(${cl.toString()}, ${methodName})"
     372        if(isWhereClauseBuilder(methodName)) {
     373            whereClauseTerms << logicalTerm(getLogicalString(methodName), null)
     374            logicalIndexStack << whereClauseTerms.size()-1
     375        }
     376        nestingStack.push(methodName)
    147377        cl.delegate = this
    148378        cl.resolveStrategy = Closure.DELEGATE_FIRST
    149379        cl.call()
     380        //log.debug "nestingStack: $nestingStack"
     381        nestingStack.pop()
     382        if(isWhereClauseBuilder(methodName)) {
     383            logicalIndexStack.pop()
     384        }
    150385    }
    151386
     
    166401
    167402    /**
     403     * IsLogicalBuilder.
     404     * Determine if a method is a logicalBuilder.
     405     */
     406    private isLogicalBuilder(String methodName) {
     407        logicalBuilders.find{ it.value == methodName.toLowerCase()} ? true:false
     408    }
     409
     410    /**
     411     * IsWhereClauseBuilder.
     412     * Determine if a method is a where clause builder.
     413     */
     414    private isWhereClauseBuilder(String methodName) {
     415        methodName = methodName.toLowerCase()
     416        if(methodName == 'where' || isLogicalBuilder(methodName))
     417            return true
     418        else
     419            return false
     420    }
     421
     422    /**
    168423     * GetQuery.
    169424     * Assemble and return the query in a format that can be directly executed.
     
    171426     */
    172427    def getQuery() {
    173         this.clauses.collect {
    174             it.key + ' ' + it.value.join(' ')
     428        clauses.collect { clause ->
     429            switch (clause.key.toLowerCase()) {
     430                case 'select':
     431                    clause.key + ' ' + clause.value.join(', ')
     432                    break
     433                case 'set':
     434                    clause.key + ' ' + clause.value.join(', ')
     435                    break
     436                case 'where':
     437                    buildWhereClause()
     438                    break
     439                case 'order':
     440                    clause.key + ' ' + clause.value.join(', ')
     441                    break
     442                case 'group':
     443                    clause.key + ' ' + clause.value.join(', ')
     444                    break
     445                default:
     446                    clause.key + ' ' + clause.value.join(' ')
     447            }
    175448        }.join(' ')
    176     }
     449    } // getQuery()
    177450
    178451    /**
     
    182455     */
    183456    def getPrintFormattedQuery() {
    184         this.clauses.collect {
    185             it.key + ' ' + it.value.join(' \n')
     457        clauses.collect { clause ->
     458            switch (clause.key.toLowerCase()) {
     459                case 'select':
     460                    clause.key + ' ' + clause.value.join(', \n\t')
     461                    break
     462                case 'set':
     463                    clause.key + ' ' + clause.value.join(', \n\t')
     464                    break
     465                case 'where':
     466                    buildWhereClause(true)
     467                    break
     468                case 'order':
     469                    clause.key + ' ' + clause.value.join(', \n\t')
     470                    break
     471                case 'group':
     472                    clause.key + ' ' + clause.value.join(', \n\t')
     473                    break
     474                default:
     475                    clause.key + ' ' + clause.value.join(' \n\t')
     476            }
    186477        }.join(' \n')
    187     }
     478    } // getPrintFormattedQuery()
    188479
    189480} // end class
Note: See TracChangeset for help on using the changeset viewer.