Alfresco Canned Queries – IntroducciónAlfresco Canned Queries

Las instalaciones de Alfresco configuradas para usar Solr no incluyen la capacidad de realizar indexaciones transaccionales: la indexación no se realiza en el momento en el que el cambio es persistido en la base de datos. En su lugar Solr emplea un tracker que consulta a Alfresco, según un intervalo de tiempo configurable, por lo últimos cambios realizados para incorporarlos al índice de Lucene. Como consecuencia de este comportamiento es posible que los índices de Lucene no estén en perfecta sincronización con la base de datos. A esta característica Alfresco la denomina eventual consistencyEn algunos casos este comportamiento puede ser un problema, ya que puede ocurrir, por ejemplo, que una consulta sobre un documento recientemente modificado, no devuelva los resultados esperados. En estos casos la solución pasa por realizar la consulta contra la base de datos, en vez de contra Solr. En el siguiente post se introduce la característica denominada Canned Query que permite realizar consultas SQL a medida contra la base de datos de Alfresco. Esta utilidad es especialmente útil cuando se está usando Solr como motor de búsqueda y se detecta un caso de uso incompatible con la eventual consistency

¿Cómo funcionan las Alfresco Canned Queries?

En las primeras versiones de Alfresco se empleaba Hibernate como framework de mapeo objeto relacional (ORM). A partir de la versión Enterprise 4.0 (y creo recordar que Community 3.4.a ) Alfresco introduce ibatis como reemplazo a Hibernate. Con ibatis se obtiene un mayor control sobre el código SQL a ejecutar y abre la puerta a la escritura de consultas SQL a medida, y en consecuencia a la programación de canned queries a medida.

Sequence Diagram
Diagrama de secuencia Canned Queries

En el diagrama de secuencia anterior se muestra el funcionamiento básico de la ejecución de una canned query. Supuesta la clase de servicio AlbaranService, se obtiene una instancia de CannedQuery mediante la CannedQueryFactory, que no es más que un tipo de NamedObjectRegistry. El objeto CannedQuery proporcionado permite la ejecución de la query a través del método

CannedQueryResults CannedQuery#execute()

En el siguiente diagrama de clases se muestran las clases del modelo de datos para las canned queries. Además de las interfaces principales ya mencionadas, se incluyen clases abstract que facilitan la implementación de canned queries a medida. Como se puede comprobar, una característica muy interesante, es que las interfaces usan generics, con lo cuál se permite parametrizar las consultas adaptándolas a cualquier modelo de negocio específico.

Class Diagram - Canned Queries Data Model
Modelo de datos para Canned Queries

Implementación de ejemplo para Canned queries

A continuación veremos cómo implementar una consulta a medida, que se adapte a nuestro modelo de negocio. Para ello supongamos que tenemos definido el siguiente tipo de documento spv:albaran, y que queremos hacer consultas SQL usando como parámetro el albaran:numeroPedido

 
<type name="spv:albaran">
    <title>Albarán</title>
    <parent>cm:content</parent>
    <properties>
        <property name="albaran:numeroPedido">
            <title>Número de Pedido</title>
            <type>d:text</type>
            <mandatory>true</mandatory>
        </property>
        <property name="albaran:fechaEntrega">
            <title>Fecha de entrega</title>
            <type>d:date</type>
            <mandatory>true</mandatory>
        </property>                
    </properties>
</type>

Empezaremos por definir la consulta SQL a realizar usando la sintaxis adecuada de ibatis.

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="alfresco.query.spv">

    <select id="select_GetAlbaranByPedidoCQ" parameterType="Node" resultMap="result_Node">
		             
        <include refid="alfresco.node.select_Node_Results"/>
        from
            alf_node node
            join alf_store store on (store.id = node.store_id)
            join alf_transaction txn on (txn.id = node.transaction_id)
            left join alf_node_properties prop_numPedido on ( 
                                prop_numPedido.node_id = node.id and prop_numPedido.qname_id = #{numeroPedidoQNameId} )     
        where
            store.protocol = #{store.protocol} and
            store.identifier = #{store.identifier} and
            node.type_qname_id = #{typeQNameId} and 
            prop_numPedido.string_value = #{numeroPedidoValue}
    </select>
    
</mapper>

Empezar escribiendo la consulta nos permite identificar los parámetros de la query, y así poder escribir en consecuencia la clase que servirá de soporte para parametrizar la consulta. A continuación se muesta la clase GetAlbaranByPedidoCQParams adaptada para la consulta anterior.

public class GetAlbaranByPedidoCQParams extends NodeEntity{

    /** Id del alf_qname.id para la prop {@link Albaran#ALBARAN_NUMEROPEDIDO}. */
    private Long numeroPedidoQNameId;
    /** Valor de la propiedad {@link Albaran#ALBARAN_NUMEROPEDIDO} a buscar. */
    private String numeroPedidoValue;
    private StoreRef store;

    public GetAlbaranByPedidoCQParams(Long albaranTypeQNameId, Long numeroPedidoQNameId, 
            String numeroPedidoValue, StoreRef store) {
        this.numeroPedidoQNameId = numeroPedidoQNameId;
        this.numeroPedidoValue = numeroPedidoValue;
        this.store = store;
        setTypeQNameId(albaranTypeQNameId);
    }

}

Implementación de AbstractQNameAwareCannedQueryFactory

Una vez identificada la consulta de ibatis, y la clase de soporte para su parametrización, el siguiente paso es escribir la factoria para la consulta. Para implementar la interfaz CannedQueryFactory nos apoyamos en la clase AbstractQNameAwareCannedQueryFactory, cuya jerarquía de clases se muestra a continuación.

Class Diagram Basic CQFactory
Implementación de CannedQueryFactory

A continuación se muestra la implementación de CannedQueryFactory para nuestra consulta de albaranes por número de pedido. Esta clase se encarga de construir la CannedQuery con los parámetros adecuados.  También esta clase es la encargada de establecer los criterios de ordenación y paginación de resultados.

public class GetAlbaranByPedidoCQFactory extends AbstractQNameAwareCannedQueryFactory{

    @Override
    public CannedQuery getCannedQuery(CannedQueryParameters parameters) {
        return (CannedQuery) new GetAlbaranByPedidoCQ(cannedQueryDAO, tenantService, methodSecurity, parameters);
    }

    public CannedQuery getCannedQuery(String numeroPedido, PagingRequest pagingRequest){
        ParameterCheck.mandatory("numeroPedido", numeroPedido);

        // specific query params
        GetAlbaranByPedidoCQParams paramBean = new GetAlbaranByPedidoCQParams(
                qnameDAO.getQName(Albaran.TYPE_ALBARAN).getFirst(), 
                qnameDAO.getQName(Albaran.ALBARAN_NUMEROPEDIDO).getFirst(), 
                numeroPedido,
                StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);

         // Page details
        CannedQueryPageDetails cqpd = createCQPageDetails(pagingRequest);       

        // Sort details
        // Si se requiere ordenación ver AbstractQNameAwareCannedQueryFactory#createCQSortDetails(List sort)
        CannedQuerySortDetails cqsd = null;

        // create query params holder
        int requestTotalCountMax = pagingRequest.getRequestTotalCountMax();
        CannedQueryParameters params = new CannedQueryParameters(paramBean, cqpd, cqsd, requestTotalCountMax, pagingRequest.getQueryExecutionId());

        // return canned query instance
        return getCannedQuery(params);
    }
}

Como se puede comprobar, GetAlbaranByPedidoCQFactory se apoya en la clase GetAlbaranByPedidoCQParams, que se emplea para dar el soporte necesario para la parametrización de la query.

Implementación de AbstractCannedQueryPermissions

Por último se escribe la implementación de CannedQuery, que simplemente se encarga de invocar la consulta, apoyándose para ello en CannedQueryDAO. Para implementar la interfaz CannedQuery nos apoyamos en la clase AbstractCannedQueryPermissions. A continuación se muestra la jerarquía de clases

Class Diagram Basic CannedQuery
Implementación Canned Query

La clase AbstractCannedQueryPermissions se encarga de realizar la consulta, y si fuera necesario, de hacer un filtrado sobre los resultados obtenidos.

public class GetAlbaranByPedidoCQ extends AbstractCannedQueryPermissions {

    private Logger logger = Logger.getLogger(GetAlbaranByPedidoCQ.class);
    private static final String QUERY_NAMESPACE = "alfresco.query.spv";
    private static final String QUERY_SELECT_GET_ALBARAN_BY_PEDIDO = "select_GetAlbaranByPedidoCQ";
    private CannedQueryDAO cannedQueryDAO;

    public GetAlbaranByPedidoCQ(CannedQueryDAO cannedQueryDAO, TenantService tenantService,
            MethodSecurityBean methodSecurity, CannedQueryParameters parameters) {
        super(parameters, methodSecurity);
        this.cannedQueryDAO = cannedQueryDAO;
    }

    @Override
    protected List queryAndFilter(CannedQueryParameters parameters) {
        Object paramBeanObj = parameters.getParameterBean();
        if (paramBeanObj == null) {
            throw new NullPointerException("Null GetEspecificaciones query params");
        }

        // Hacer query
        List results = cannedQueryDAO.executeQuery(QUERY_NAMESPACE, 
                QUERY_SELECT_GET_ALBARAN_BY_PEDIDO, paramBeanObj, 0, Integer.MAX_VALUE);
        logger.debug( results.size() + " resultados obtenidos por la consulta ibatis " + QUERY_SELECT_GET_ALBARAN_BY_PEDIDO);

        // No es necesario filtrado sobre los resultados
        // Ver GetCalendarEntriesCannedQuery para un ejemplo de filtrado sobre los resultados de la consulta sql

        return results;
    }

}

Implementación de CannedQueryDAOImpl

Como hemos visto, la implementación de CannedQuery se apoya en CannedQueryDAO para realizar la consulta ibatis. A continuación se muestra como se puede definir un CannedQueryDAOImpl que proporciona mayor control sobre los mapping de ibatis a usar. Y también es menos intrusivo, ya que es independiente del DAO definido por Alfresco.

<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>
    
    <!-- MyBatis-Spring sqlSessionFactory -->
    <bean id="customSqlSessionFactory" class="org.alfresco.ibatis.HierarchicalSqlSessionFactoryBean">
        <property name="useLocalCaches" value="${mybatis.useLocalCaches}"/>
        <property name="resourceLoader" ref="dialectResourceLoader"/>
        <property name="dataSource" ref="dataSource"/>
         <property name="configLocation">
            <value>classpath:alfresco/extension/module/${pom.groupId}.${pom.artifactId}/ibatis/alfrescoExtension-SqlMapConfig.xml</value>
        </property>
    </bean>
    
    <!-- MyBatis-Spring sqlSessionTemplate -->
    <bean id="customSqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="customSqlSessionFactory"/>
    </bean>
    
    
    <!-- 
        Alternativa al cannedQueryDAO de Alfresco definido en alfresco/dao/dao-context.xml
        que carga el customSqlSessionTemplate, que a su vez usa customSqlSessionFactory que
        incorpora los ibatis/XXX-SqlMapConfig.xml de la extensión desarrollada.
    -->
    <bean id="queresCannedQueryDAO" class="org.alfresco.repo.domain.query.ibatis.CannedQueryDAOImpl" 
          init-method="init">
        <property name="sqlSessionTemplate" ref="customSqlSessionTemplate"/>
        <property name="controlDAO" ref="controlDAO"/>
    </bean>
   
</beans>

Custom ibatis mappings: alfrescoExtension-SqlMapConfig.xml

En archivo de mapping alfrescoExtension-SqlMapConfig.xml, además de aprovechar los type definidos por Alfresco, podemos introducir los mappings adecuados a nuestro modelo de negocio.

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    
    <typeAliases>
        
        <!-- Content -->
        
        <typeAlias alias="Mimetype" type="org.alfresco.repo.domain.mimetype.MimetypeEntity"/>
        <typeAlias alias="Encoding" type="org.alfresco.repo.domain.encoding.EncodingEntity"/>
        <typeAlias alias="ContentUrl" type="org.alfresco.repo.domain.contentdata.ContentUrlEntity"/>
        <typeAlias alias="ContentUrlUpdate" type="org.alfresco.repo.domain.contentdata.ContentUrlUpdateEntity"/>
        <typeAlias alias="ContentData" type="org.alfresco.repo.domain.contentdata.ContentDataEntity"/>
        <typeAlias alias="ContentUrlOrphanQuery" type="org.alfresco.repo.domain.contentdata.ContentUrlOrphanQuery"/>
        <typeAlias alias="Ids" type="org.alfresco.ibatis.IdsEntity"/>
        
        <!-- QName -->
        
        <typeAlias alias="Namespace" type="org.alfresco.repo.domain.qname.NamespaceEntity"/>
        <typeAlias alias="QName" type="org.alfresco.repo.domain.qname.QNameEntity"/>
        
        <!-- Locale -->
        
        <typeAlias alias="Locale" type="org.alfresco.repo.domain.locale.LocaleEntity"/>
        
        <!-- Node -->
        
        <typeAlias alias="Server" type="org.alfresco.repo.domain.node.ServerEntity"/>
        <typeAlias alias="Transaction" type="org.alfresco.repo.domain.node.TransactionEntity"/>
        <typeAlias alias="TransactionQuery" type="org.alfresco.repo.domain.node.TransactionQueryEntity"/>
        <typeAlias alias="Store" type="org.alfresco.repo.domain.node.StoreEntity"/>
        <typeAlias alias="Node" type="org.alfresco.repo.domain.node.NodeEntity"/>
        <typeAlias alias="NodeBatchLoad" type="org.alfresco.repo.domain.node.ibatis.NodeBatchLoadEntity"/>
        <typeAlias alias="NodeUpdate" type="org.alfresco.repo.domain.node.NodeUpdateEntity"/>
        <typeAlias alias="AuditProps" type="org.alfresco.repo.domain.node.AuditablePropertiesEntity"/>
        <typeAlias alias="NodePropertyKey" type="org.alfresco.repo.domain.node.NodePropertyKey"/>
        <typeAlias alias="NodePropertyValue" type="org.alfresco.repo.domain.node.NodePropertyValue"/>
        <typeAlias alias="NodeProperty" type="org.alfresco.repo.domain.node.NodePropertyEntity"/>
        <typeAlias alias="NodeAspects" type="org.alfresco.repo.domain.node.NodeAspectsEntity"/>
        <typeAlias alias="NodeAssoc" type="org.alfresco.repo.domain.node.NodeAssocEntity"/>
        <typeAlias alias="ChildAssoc" type="org.alfresco.repo.domain.node.ChildAssocEntity"/>
        <typeAlias alias="ChildProperty" type="org.alfresco.repo.domain.node.ChildPropertyEntity"/>
        <typeAlias alias="PrimaryChildrenAclUpdate" type="org.alfresco.repo.domain.node.PrimaryChildrenAclUpdateEntity"/>
        
        <!--GetChildren CQ (currently used by FileFolderService.list / PersonService.getPeople) -->
        <typeAlias alias="FilterSortNode" type="org.alfresco.repo.node.getchildren.FilterSortNodeEntity"/>
        
        <!--GetChildren by Auditable CQ -->
        <typeAlias alias="NodeBackedEntity" type="org.alfresco.repo.query.NodeBackedEntity"/>
        <typeAlias alias="NodeWithTargetsEntity" type="org.alfresco.repo.query.NodeWithTargetsEntity"/>
        
        <!-- Permissions -->
        
        <typeAlias alias="Acl" type="org.alfresco.repo.domain.permissions.AclEntity"/>
        <typeAlias alias="AclMember" type="org.alfresco.repo.domain.permissions.AclMemberEntity"/>
        <typeAlias alias="AclChangeSet" type="org.alfresco.repo.domain.permissions.AclChangeSetEntity"/>
        <typeAlias alias="Ace" type="org.alfresco.repo.domain.permissions.AceEntity"/>
        <typeAlias alias="AceContext" type="org.alfresco.repo.domain.permissions.AceContextEntity"/>
        <typeAlias alias="Permission" type="org.alfresco.repo.domain.permissions.PermissionEntity"/>
        <typeAlias alias="Authority" type="org.alfresco.repo.domain.permissions.AuthorityEntity"/>
        <typeAlias alias="AuthorityAlias" type="org.alfresco.repo.domain.permissions.AuthorityAliasEntity"/>
        
    </typeAliases>
    
    <mappers>
        <mapper resource="alfresco/ibatis/#resource.dialect#/alfresco-util-SqlMap.xml"/>
        <mapper resource="alfresco/ibatis/#resource.dialect#/qname-common-SqlMap.xml"/>
        <mapper resource="alfresco/ibatis/#resource.dialect#/locale-common-SqlMap.xml"/>
        <mapper resource="alfresco/ibatis/#resource.dialect#/content-common-SqlMap.xml"/>
        <mapper resource="alfresco/ibatis/#resource.dialect#/node-common-SqlMap.xml"/>
        <mapper resource="alfresco/ibatis/#resource.dialect#/node-select-children-SqlMap.xml"/>
        <mapper resource="alfresco/ibatis/#resource.dialect#/permissions-common-SqlMap.xml"/>
        <!-- Custom -->
        <mapper resource="/alfresco/extension/module/com.queres.alfresco.HR.spv/ibatis/#resource.dialect#/spv-SqlMap.xml"/>
    </mappers>
    
</configuration>

El contenido del archivo spv-SqlMap.xml es el que contiene la consulta de albaranes por número de pedido.

Definición de beans de Spring

Por último, solo queda definir los beans de Spring con las implementaciones que se han descrito en los apartados anteriores.

 
 <!--
        =========================
        = Canned Queries Config
        =========================
    -->
    
    <!-- List of Canned queries -->
    <bean id="spvCannedQueryRegistry" class="org.alfresco.util.registry.NamedObjectRegistry">
        <property name="storageType" value="org.alfresco.query.CannedQueryFactory"/>
    </bean>
    
    <!-- The regular GetChildren Canned Query Factory -->
    <bean name="spvGetAlbaranByPedidoCannedQueryFactory" 
          class="com.queres.alfresco.HR.spv.cannedqueries.GetAlbaranByPedidoCQFactory">
        <property name="registry" ref="spvCannedQueryRegistry"/>
        <property name="tenantService" ref="tenantService"/>
        <property name="nodeDAO" ref="nodeDAO"/>
        <property name="qnameDAO" ref="qnameDAO"/>
        <property name="cannedQueryDAO" ref="queresCannedQueryDAO"/>
        <property name="methodSecurity" >
            <bean class="org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityBean">
                <property name="methodSecurityInterceptor" ref="AlbaranService_CannedQuery_security" />
                <property name="service" value="com.queres.alfresco.HR.spv.service.IAlbaranService" />
                <property name="methodName" value="yaExisteAlbaran" />
            </bean>
        </property>
    </bean>
   
   
    <!-- 
        Define el nivel de seguridad de las canned queries del AlbaranService.
        Ver ejemplos en los beans de Alfresco: NodeService_security, FileFolderService_security, ...
    -->
    <bean id="AlbaranService_CannedQuery_security" 
          class="org.alfresco.repo.security.permissions.impl.acegi.MethodSecurityInterceptor">
        <property name="authenticationManager">
            <ref bean="authenticationManager"/>
        </property>
        <property name="accessDecisionManager">
            <ref bean="accessDecisionManager"/>
        </property>
        <property name="afterInvocationManager">
            <ref bean="afterInvocationManager"/>
        </property>
        <property name="objectDefinitionSource">
            <value>
                com.queres.alfresco.HR.spv.service.IAlbaranService.yaExisteAlbaran=ACL_NODE.0.sys:base.ReadProperties
            </value>
        </property>
    </bean>

Invocación de CannedQueries

Así que ya está todo listo para hacer uso adecuado de todo ello a través de la interfaz de servicio. Por ejemplo:

public class AlbaranService implements IAlbaranService {

    protected static final String GET_ALBARAN_BY_PEDIDO_CQF = "spvGetAlbaranByPedidoCannedQueryFactory";
    private Logger logger = Logger.getLogger(AlbaranService.class);

    @Override
    public boolean yaExisteAlbaran(String numeroPedido) {
          GetAlbaranByPedidoCQFactory cqf = (GetAlbaranByPedidoCQFactory) cannedQueryRegistry.getNamedObject(GET_ALBARAN_BY_PEDIDO_CQF);
        GetAlbaranByPedidoCQ cq = (GetAlbaranByPedidoCQ) cqf.getCannedQuery(numeroPedido, new PagingRequest(Integer.MAX_VALUE));
        CannedQueryResults<NodeEntity> results = cq.execute();
        int numResults = results.getPagedResultCount();
        return numResults != 0;
    }

    public void setCannedQueryRegistry(NamedObjectRegistry<CannedQueryFactory<? extends Object>> cannedQueryRegistry) {
        this.cannedQueryRegistry = cannedQueryRegistry;
    }
}