Ignore:
Timestamp:
May 6, 2011, 5:41:49 PM (9 years ago)
Author:
robert@…
Message:

Adjusted querying process in order to improve speed with large amounts of data. The querying is now performed directly in HQL, instead of fetching all possible objects from the database and filtering them.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/groovy/dbnp/query/Search.groovy

    r1800 r1820  
    112112                this.executionDate = new Date();
    113113
    114                 switch( searchMode ) {
    115                         case SearchMode.and:
    116                                 executeAnd();
    117                                 break;
    118                         case SearchMode.or:
    119                                 executeOr();
    120                                 break;
    121                 }
     114                // Execute the search
     115                executeSearch();
    122116
    123117                // Save the value of this results for later use
     
    126120
    127121        /**
    128          * Executes an inclusive (AND) search based on the given criteria. Should be filled in by
    129          * subclasses searching for a specific entity
    130          */
    131         protected void executeAnd() {
    132 
    133         }
    134 
    135         /**
    136          * Executes an exclusive (OR) search based on the given criteria. Should be filled in by
    137          * subclasses searching for a specific entity
    138          */
    139         protected void executeOr() {
    140 
    141         }
    142 
    143         /**
    144          * Default implementation of an inclusive (AND) search. Can be called by subclasses in order
    145          * to simplify searches.
    146          *
    147          * Filters the list of objects on study, subject, sample, event, samplingevent and assaycriteria,
    148          * based on the closures defined in valueCallback. Afterwards, the objects are filtered on module
    149          * criteria
    150          *
    151          * @param objects       List of objects to search in
    152          */
    153         protected void executeAnd( List objects ) {
    154                 // If no criteria are found, return all studies
    155                 if( !criteria || criteria.size() == 0 ) {
    156                         results = objects;
    157                         return;
    158                 }
    159 
    160                 // Perform filters
    161                 objects = filterOnStudyCriteria( objects );
    162                 objects = filterOnSubjectCriteria( objects );
    163                 objects = filterOnSampleCriteria( objects );
    164                 objects = filterOnEventCriteria( objects );
    165                 objects = filterOnSamplingEventCriteria( objects );
    166                 objects = filterOnAssayCriteria( objects );
    167 
    168                 // Filter on criteria for which the entity is unknown
    169                 objects = filterOnAllFieldsCriteria( objects );
    170                
    171                 // Filter on module criteria
    172                 objects = filterOnModuleCriteria( objects );
    173 
    174                 // Save matches
    175                 results = objects;
    176         }
    177 
    178         /**
    179         * Default implementation of an exclusive (OR) search. Can be called by subclasses in order
    180         * to simplify searches.
    181         *
    182         * Filters the list of objects on study, subject, sample, event, samplingevent and assaycriteria,
    183         * based on the closures defined in valueCallback. Afterwards, the objects are filtered on module
    184         * criteria
    185         *
    186         * @param allObjects     List of objects to search in
    187         */
    188    protected void executeOr( List allObjects ) {
    189                 // If no criteria are found, return all studies
    190                 if( !criteria || criteria.size() == 0 ) {
    191                         results = allObjects;
    192                         return;
    193                 }
    194 
    195                 // Perform filters on those objects not yet found by other criteria
    196                 def objects = []
    197                 objects = ( objects + filterOnStudyCriteria( allObjects - objects ) ).unique();
    198                 objects = ( objects + filterOnSubjectCriteria( allObjects - objects ) ).unique();
    199                 objects = ( objects + filterOnSampleCriteria( allObjects - objects ) ).unique();
    200                 objects = ( objects + filterOnEventCriteria( allObjects - objects ) ).unique();
    201                 objects = ( objects + filterOnSamplingEventCriteria( allObjects - objects ) ).unique();
    202                 objects = ( objects + filterOnAssayCriteria( allObjects - objects ) ).unique();
    203                
    204                 // Filter on criteria for which the entity is unknown
    205                 objects = ( objects + filterOnAllFieldsCriteria( allObjects - objects ) ).unique();
    206                
    207                 // All objects (including the ones already found by another criterion) are sent to
    208                 // be filtered on module criteria, in order to have the module give data about all
    209                 // objects (for showing purposes later on)
    210                 objects = ( objects + filterOnModuleCriteria( allObjects ) ).unique();
    211                
    212                 // Save matches
    213                 results = objects;
    214    }
    215 
     122         * Executes a query
     123         */
     124        protected void executeSearch() {
     125                // Create HQL query for criteria for the entity being sought
     126                def selectClause = ""
     127                def fullHQL = createHQLForEntity( this.entity );
     128
     129                // Create SQL for other entities, by executing a subquery first, and
     130                // afterwards selecting the study based on the entities found
     131                def resultsFound
     132
     133                def entityNames = [ "Study", "Subject", "Sample", "Assay", "Event", "SamplingEvent" ];
     134                for( entityToSearch in entityNames ) {
     135                        // Add conditions for all criteria for the given entity. However,
     136                        // the conditions for the 'main' entity (the entity being sought) are already added
     137                        if( entity != entityToSearch ) {
     138                                resultsFound = addEntityConditions(
     139                                        entityToSearch,                                                                                                                 // Name of the entity to search in
     140                                        TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ),  // Class of the entity to search in
     141                                        elementName( entityToSearch ),                                                                                  // HQL name of the collection to search in
     142                                        entityToSearch[0].toLowerCase() + entityToSearch[1..-1],                                // Alias for the entity to search in
     143                                        fullHQL                                                                                                                                 // Current HQL statement
     144                                )
     145                               
     146                                // If no results are found, and we are searching 'inclusive', there will be no
     147                                // results whatsoever. So we can quit this method now.
     148                                if( !resultsFound && searchMode == SearchMode.and ) {
     149                                        return
     150                                }
     151                        }
     152                }
     153                       
     154                // Search in all entities
     155                resultsFound = addWildcardConditions( fullHQL, entityNames )
     156                if( !resultsFound && searchMode == SearchMode.and ) {
     157                        return
     158                }
     159               
     160                // Combine all parts to generate a full HQL query
     161                def hqlQuery = selectClause + " " + fullHQL.from + ( fullHQL.where ? "  WHERE " + fullHQL.where.join( " " + searchMode.toString() + " "  ) : "" );
     162               
     163                // Find all objects
     164                def entities = entityClass().findAll( hqlQuery, fullHQL.parameters );
     165               
     166                // Find criteria that match one or more 'complex' fields
     167                // These criteria must be checked extra, since they are not correctly handled
     168                // by the HQL criteria. See also Criterion.manyToManyWhereCondition and
     169                // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
     170                entities = filterForComplexCriteria( entities, getEntityCriteria( this.entity ) );
     171               
     172                // Filter on module criteria. If the search is 'and', only the entities found until now
     173                // should be queried in the module. Otherwise, all entities are sent, in order to retrieve
     174                // data (to show on screen) for all entities
     175                if( hasModuleCriteria() ) {
     176                        if( searchMode == SearchMode.and ) {
     177                                entities = filterOnModuleCriteria( entities );
     178                        } else {
     179                                entities = filterOnModuleCriteria( entityClass().list().findAll { this.isAccessible( it ) } )
     180                        }
     181                }
     182               
     183                // Determine which studies can be read
     184                results = entities;
     185               
     186        }
    216187               
    217188        /************************************************************************
     
    279250                }
    280251        }
    281 
     252       
     253        /**
     254        * Returns the HQL name for the element or collections to be searched in, for the given entity name
     255        * For example: when searching for Subject.age > 50 with Study results, the system must search in all study.subjects for age > 50.
     256        * But when searching for Sample results, the system must search in sample.parentSubject for age > 50
     257        *
     258        * This method should be overridden in child classes
     259        *
     260        * @param entity Name of the entity of the criterion
     261        * @return                       HQL name for this element or collection of elements
     262        */
     263   protected String elementName( String entity ) {
     264           switch( entity ) {
     265                   case "Study":                       
     266                   case "Subject":                     
     267                   case "Sample":                       
     268                   case "Event":                       
     269                   case "SamplingEvent":       
     270                   case "Assay":                       
     271                                return entity[ 0 ].toLowerCase() + entity[ 1 .. -1 ]
     272                   default:                             return null;
     273           }
     274   }
     275   
     276        /**
     277        * Returns the a where clause for the given entity name
     278        * For example: when searching for Subject.age > 50 with Study results, the system must search
     279        *               
     280        *       WHERE EXISTS( FROM study.subjects subject WHERE subject IN (...)
     281        *
     282        * The returned string is fed to sprintf with 3 string parameters:
     283        *               from (in this case 'study.subjects'
     284        *               alias (in this case 'subject'
     285        *               paramName (in this case '...')
     286        *
     287        * This method can be overridden in child classes to enable specific behaviour
     288        *
     289        * @param entity         Name of the entity of the criterion
     290        * @return                       HQL where clause for this element or collection of elements
     291        */
     292   protected String entityClause( String entity ) {
     293           return ' EXISTS( FROM %1$s %2$s WHERE %2$s IN (:%3$s) )'
     294   }
     295   
     296   /**
     297    * Returns true iff the given entity is accessible by the user currently logged in
     298    *
     299    * This method should be overridden in child classes, since the check is different for every type of search
     300    *
     301    * @param entity             Entity to determine accessibility for. The entity is of the type 'this.entity'
     302    * @return                   True iff the user is allowed to access this entity
     303    */
     304   protected boolean isAccessible( def entity ) {
     305           return false
     306   }
     307
     308        /****************************************************
     309         *
     310         * Helper methods for generating HQL statements
     311         *
     312         ****************************************************/
     313       
     314        /**
     315         * Add all conditions for criteria for a specific entity
     316         *
     317         * @param entityName    Name of the entity to search in
     318         * @param entityClass   Class of the entity to search
     319         * @param from                  Name of the HQL collection to search in (e.g. study.subjects)
     320         * @param alias                 Alias of the HQL collection objects (e.g. 'subject')
     321         * @param fullHQL               Original HQL map to be extended (fields 'from', 'where' and 'parameters')
     322         * @param determineParentId     Closure to determine the id of the final entity to search, based on these objects
     323         * @param entityCriteria        (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found
     324         * @return                              True if one ore more entities are found, false otherwise
     325         */
     326        protected boolean addEntityConditions( String entityName, def entityClass, String from, String alias, def fullHQL, def entityCriteria = null ) {
     327                if( entityCriteria == null )
     328                        entityCriteria = getEntityCriteria( entityName )
     329               
     330                // Create HQL for these criteria
     331                def entityHQL = createHQLForEntity( entityName, entityCriteria );
     332               
     333                // If any clauses are generated for these criteria, find entities that match these criteria
     334                def whereClauses = entityHQL.where?.findAll { it && it?.trim() != "" }
     335                if( whereClauses ) {
     336                        // First find all entities that match these criteria
     337                        def hqlQuery = entityHQL.from + " WHERE " + whereClauses.join( searchMode == SearchMode.and ? " AND " : " OR " );                       
     338                        def entities = entityClass.findAll( hqlQuery, entityHQL.parameters )
     339                       
     340                        // If there are entities matching these criteria, put a where clause in the full HQL query
     341                        if( entities ) {
     342                                // Find criteria that match one or more 'complex' fields
     343                                // These criteria must be checked extra, since they are not correctly handled
     344                                // by the HQL criteria. See also Criterion.manyToManyWhereCondition and
     345                                // http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
     346                                entities = filterForComplexCriteria( entities, entityCriteria );
     347                               
     348                                def paramName = from.replaceAll( /\W/, '' );
     349                                fullHQL.where << sprintf( entityClause( entityName ), from, alias, paramName );
     350                                fullHQL.parameters[ paramName ] = entities
     351                                return true;
     352                        } else {
     353                                results = [];
     354                                return false
     355                        }
     356                }
     357               
     358                return true;
     359        }
     360       
     361        /**
     362         * Add all conditions for a wildcard search (all fields in a given entity)
     363         * @param fullHQL       Original HQL map to be extended (fields 'from', 'where' and 'parameters')
     364         * @return                      True if the addition worked
     365         */
     366        protected boolean addWildcardConditions( def fullHQL, def entities) {
     367                // Append study criteria
     368                def entityCriteria = getEntityCriteria( "*" );
     369               
     370                // If no wildcard criteria are found, return immediately
     371                if( !entityCriteria )
     372                        return true
     373                       
     374                // Wildcards should be checked within each entity
     375                def wildcardHQL = createHQLForEntity( this.entity );
     376               
     377                // Create SQL for other entities, by executing a subquery first, and
     378                // afterwards selecting the study based on the entities found
     379                entities.each { entityToSearch ->
     380                        // Add conditions for all criteria for the given entity. However,
     381                        // the conditions for the 'main' entity (the entity being sought) are already added
     382                        if( entity != entityToSearch ) {
     383                                addEntityConditions(
     384                                        entityToSearch,                                                                                                                 // Name of the entity to search in
     385                                        TemplateEntity.parseEntity( 'dbnp.studycapturing.' + entityToSearch ),  // Class of the entity to search in
     386                                        elementName( entityToSearch ),                                                                                  // HQL name of the collection to search in
     387                                        entityToSearch[0].toLowerCase() + entityToSearch[1..-1],                                // Alias for the entity to search in
     388                                        wildcardHQL,                                                                                                                    // Current HQL statement
     389                                        entityCriteria                                                                                                                  // Only create HQL for these criteria
     390                                )
     391                        }
     392                }
     393               
     394                // Add these clauses to the full HQL statement
     395                def whereClauses = wildcardHQL.where.findAll { it };
     396
     397                if( whereClauses ) {
     398                        fullHQL.from += wildcardHQL.from
     399                        fullHQL.where << whereClauses.join( " OR " )
     400                         
     401                        wildcardHQL[ "parameters" ].each {
     402                                fullHQL.parameters[ it.key ] = it.value
     403                        }
     404                }
     405               
     406                return true;
     407        }
     408       
     409        /**
     410         * Create HQL statement for the given criteria and a specific entity
     411         * @param entityName            Name of the entity
     412         * @param entityCriteria        (optional) list of criteria to create the HQL for. If no criteria are given, all criteria for the entity are found
     413         * @param includeFrom           (optional) If set to true, the 'FROM entity' is prepended to the from clause. Defaults to true
     414         * @return
     415         */
     416        def createHQLForEntity( String entityName, def entityCriteria = null, includeFrom = true ) {
     417                def fromClause = includeFrom ? "FROM " + entityName + " " + entityName.toLowerCase() : ""
     418                def whereClause = []
     419                def parameters = [:]
     420                def criterionNum = 0;
     421               
     422                // Append study criteria
     423                if( entityCriteria == null )
     424                        entityCriteria = getEntityCriteria( entityName );
     425               
     426                entityCriteria.each {
     427                        def criteriaHQL = it.toHQL( "criterion" +entityName + criterionNum++, entityName.toLowerCase() );
     428                        fromClause += " " + criteriaHQL[ "join" ]
     429                        whereClause << criteriaHQL[ "where" ]
     430                        criteriaHQL[ "parameters" ].each {
     431                                parameters[ it.key ] = it.value
     432                        }
     433                }
     434               
     435                // Add a filter such that only readable studies are returned
     436                if( entityName == "Study" ) {
     437                       
     438                        if( this.user == null ) {
     439                                // Anonymous readers are only given access when published and public
     440                                whereClause << "( study.publicstudy = true AND study.published = true )"
     441                        } else if( !this.user.hasAdminRights() ) {
     442                                // Administrators are allowed to read every study
     443
     444                                // Owners and writers are allowed to read this study
     445                                // Readers are allowed to read this study when it is published
     446                                whereClause << "( study.owner = :sessionUser OR :sessionUser member of study.writers OR ( :sessionUser member of study.readers AND study.published = true ) )"
     447                                parameters[ "sessionUser" ] = this.user
     448                        }
     449                }
     450               
     451                return [ "from": fromClause, "where": whereClause, "parameters": parameters ]
     452        }
     453       
    282454        /*****************************************************
    283455         *
     
    405577                }
    406578        }
    407                
    408         /**
    409          * Filters the given list of studies on the study criteria
    410          * @param studies               Original list of studies
    411          * @param entity                Name of the entity to check the criteria for
    412          * @param valueCallback Callback having a study and criterion as input, returning the value of the field to check on
    413          * @return                              List with all studies that match the Criteria
    414          */
    415         protected List filterOnTemplateEntityCriteria( List studies, String entityName, Closure valueCallback ) {
    416                 def criteria = getEntityCriteria( entityName );
    417 
    418                 def checkCallback = { study, criterion ->
    419                         def value = valueCallback( study, criterion );
    420 
    421                         if( value == null ) {
    422                                 return false
    423                         }
    424 
    425                         if( value instanceof Collection ) {
    426                                 return criterion.matchAny( value )
    427                         } else {
    428                                 return criterion.match( value );
    429                         }
    430                 }
    431 
    432                 return filterEntityList( studies, criteria, checkCallback);
    433         }
    434 
    435         /**
    436          * Filters the given list of studies on the study criteria
    437          * @param studies       Original list of studies
    438          * @return                      List with all studies that match the Study criteria
    439          */
    440         protected List filterOnStudyCriteria( List studies ) {
    441                 def entity = "Study"
    442                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    443         }
    444 
    445         /**
    446          * Filters the given list of studies on the subject criteria
    447          * @param studies       Original list of studies
    448          * @return                      List with all studies that match the Subject-criteria
    449          */
    450         protected List filterOnSubjectCriteria( List studies ) {
    451                 def entity = "Subject"
    452                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    453         }
    454 
    455         /**
    456          * Filters the given list of studies on the sample criteria
    457          * @param studies       Original list of studies
    458          * @return                      List with all studies that match the sample-criteria
    459          */
    460         protected List filterOnSampleCriteria( List studies ) {
    461                 def entity = "Sample"
    462                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    463         }
    464 
    465         /**
    466          * Filters the given list of studies on the event criteria
    467          * @param studies       Original list of studies
    468          * @return                      List with all studies that match the event-criteria
    469          */
    470         protected List filterOnEventCriteria( List studies ) {
    471                 def entity = "Event"
    472                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    473         }
    474 
    475         /**
    476          * Filters the given list of studies on the sampling event criteria
    477          * @param studies       Original list of studies
    478          * @return                      List with all studies that match the event-criteria
    479          */
    480         protected List filterOnSamplingEventCriteria( List studies ) {
    481                 def entity = "SamplingEvent"
    482                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    483         }
    484 
    485         /**
    486          * Filters the given list of studies on the assay criteria
    487          * @param studies       Original list of studies
    488          * @return                      List with all studies that match the assay-criteria
    489          */
    490         protected List filterOnAssayCriteria( List studies ) {
    491                 def entity = "Assay"
    492                 return filterOnTemplateEntityCriteria(studies, entity, valueCallback( entity ) )
    493         }
    494        
    495        
    496         /**
    497          * Filters the given list of entities on criteria that mention all fields (e.g. search for studies with 'bacteria' in any field)
    498          * @param objects       Original list of entities.
    499          * @return                      List of all entities that match the given criteria
    500          */
    501         protected List filterOnAllFieldsCriteria( List objects ) {
    502                 def criteria = getEntityCriteria( "*" );
    503                
    504                 // Find methods to determine a value for a criterion, based on all entities
    505                 def valueCallbacks = [:];
    506                 def entities = [ "Study", "Subject", "Sample", "Event", "SamplingEvent", "Assay" ];
    507                 entities.each {
    508                         valueCallbacks[ it ] = valueCallback( it );
    509                 }
    510                
    511                 // Create a closure that checks all entities
    512                 def checkCallback = { object, criterion ->
    513                         def value = "";
    514                         for( def entity in entities ) {
    515                                 value = valueCallbacks[ entity ]( object, criterion );
    516                                
     579       
     580        /**
     581         * Filters an entity list manually on complex criteria found in the criteria list.
     582         * This method is needed because hibernate contains a bug in the HQL INDEX() function.
     583         * See also Criterion.manyToManyWhereCondition and
     584         *http://opensource.atlassian.com/projects/hibernate/browse/HHH-4615
     585         *
     586         * @param entities                      List of entities
     587         * @param entityCriteria        List of criteria that apply to the type of entities given       (e.g. Subject criteria for Subjects)
     588         * @return                                      Filtered entity list
     589         */
     590        protected filterForComplexCriteria( def entities, def entityCriteria ) {
     591                def complexCriteria = entityCriteria.findAll { it.isComplexCriterion() }
     592                if( complexCriteria ) {
     593                        def checkCallback = { entity, criterion ->
     594                                def value = criterion.getFieldValue( entity )
     595
    517596                                if( value == null ) {
    518                                         continue;
    519                                 }
    520        
     597                                        return false
     598                                }
     599
    521600                                if( value instanceof Collection ) {
    522                                         if( criterion.matchAny( value ) )
    523                                                 return true;
     601                                        return criterion.matchAny( value )
    524602                                } else {
    525                                         if( criterion.match( value ) )
    526                                                 return true;
    527                                 }
    528                         }
    529                        
    530                         // If no match is found, return
    531                         return false;
    532                 }
    533 
    534                 return filterEntityList( objects, criteria, checkCallback);
    535         }
    536        
     603                                        return criterion.match( value );
     604                                }
     605                        }
     606
     607                        entities = filterEntityList( entities, complexCriteria, checkCallback );
     608                }
     609               
     610                return entities;
     611        }
     612
    537613        /********************************************************************
    538614         *
     
    541617         ********************************************************************/
    542618
     619        protected boolean hasModuleCriteria() {
     620               
     621                return AssayModule.list().any { module ->
     622                        // Remove 'module' from module name
     623                        def moduleName = module.name.replace( 'module', '' ).trim()
     624                        def moduleCriteria = getEntityCriteria( moduleName );
     625                        return moduleCriteria?.size() > 0
     626                }
     627        }
     628       
    543629        /**
    544630         * Filters the given list of entities on the module criteria
     
    611697                                }
    612698               
    613                                 println this.resultFields;
    614                                
    615699                                return resultingEntities;
    616700                        default:
     
    878962                                        criteria.containsAll( s.criteria ) );
    879963        }
     964       
     965        /**
     966        * Returns the class for the entity being searched
     967        * @return
     968        */
     969        public Class entityClass() {
     970                if( !this.entity )
     971                        return null;
     972                       
     973                try {
     974                        return TemplateEntity.parseEntity( 'dbnp.studycapturing.' + this.entity)
     975                } catch( Exception e ) {
     976                        throw new Exception( "Unknown entity for criterion " + this, e );
     977                }
     978        }
     979       
    880980}
Note: See TracChangeset for help on using the changeset viewer.