Overview of the Query API

This document gives an overview of the Query API.

Contents

  1. UML Diagram
  2. Query, HqlQuery, SqlQuery and QueryType
  3. Runtime query filters
  4. QueryRuntimeFilterFactory and QueryRuntimeFilterManager

See also

Last updated: $Date: 2009-04-06 14:52:39 +0200 (må, 06 apr 2009) $

1. UML diagram

2. Query, HqlQuery, SqlQuery and QueryType

The general query is an object implementing the Query interface. The interface is targeted at relational queries, ie. queries that can be expressed by SQL or some similar query language (like Hibernate Query Language, HQL). These two query langauages are reflected in the subinterfaces HqlQuery and SqlQuery and in the QueryType enumeration.

The general template for of a relational query is:

SELECT [DISTINCT] <selection-list>
FROM <root-entity>
[JOIN <joined-entity>]
[WHERE <restrictions>]
[GROUP BY <groupby-list]
[HAVING <restrictions>]
[ORDER BY <orderby-list>]

The Query interface has methods for manipulating all aspects of the query except the root-entity. The root entity should be specified by the code that creates an actual instance of a query. For example:

// News.getQuery()
SELECT nws
FROM NewsData nws

Here the root entity is the "NewsData" entity and has been given an alias "nws". This alias is the string returned by the Query.getRootAlias() method. All Hibernate queries must have a root alias.

An actual implementation are not required to support manipulation of all aspects of a query. For example the EntityQuery represents a query for Hibernate entities. In this case the selection-list is fixed to return only items of the specified type. See the "news" example above. If an implementation doesn't support a method it should throw a UnsupportedOperationException.

// AbstractEntityQuery.java
public void select(Select s)
{
   throw new UnsupportedOperationException();
}

Currently, we have three implementations of the Query interface:

3. Runtime query filters

Runtime query filters are used by Hibernate queries for two purposes:

The namespace for runtime filters are global. The filter-def.hbm.xml defines the name and parameters of a filter. The actual SQL code for a filter is defined on a per-item basis. We ahve choosen to define the actual filters programmatically in the HibernateUtil.addFilterConditions() method. The drawback is that it is not possible to change the filter without compiling. The advantage is that a hacker cannot change the filters either.

// filter-def.hbm.xml
<!-- 
  Filter Ownable items on the owner to only return
  items owned by the logged in user.
-->
<filter-def name="ownedBy">
  <filter-param name="owner" type="int" />
</filter-def>

// HibernateUtil.addFilterConditions()
PersistentClass pc = ...
Class dataClass = pc.getMappedClass();
if (OwnableData.class.isAssignableFrom(dataClass))
{
  pc.addFilter("ownedBy", ":owner = owner");
}

The PersistentClass object is a Hibernate object representing the mapping for the dataClass object which is one of the data layer classes. If the class is ownable (implements the OwnableData interface) we create a filter condition which checks the owner column.

Enabling and using the filter is then as easy as:

int loggedInUserId = ...
org.hibernate.Session session = ... 
org.hibernate.Filter filter = session.enableFilter("ownedBy");
filter.setParameter("owner", loggedInUserId);

Now, all queries executed in that session will have the filter applied to them:

SELECT Samples.*
FROM Samples s
WHERE :owner = s.owner

One important note is that the filter are applied in the SQL layer of Hibernate, not at the HQL layer. It means that the filter must use the real database column names and not the entity properties. This limits the use of filters to a single table, since it is not possible to know which other tables are joined and what alias they may have.

4. QueryRuntimeFilterFactory and QueryRuntimeFilterManager

Runtime filters are enabled/disabled on a per-query basis by instances of the QueryRuntimeFilter interface. The actual enabling/disabling of filters is done by a QueryRuntimeFilterManager object. It keeps track of all filters that has been enabled, and disallows a filter to be enabled twice. It is also used to disable all filters after the query has been executed.

The QueryRuntimFilterFactory class has already created instances for the most commonly used filters. For example all removable items has a RemovableFilter which checks the Include.REMOVED and Include.NOT_REMOVED options and applies a filter if neccessary.

Most items have more than one filter. They are collected by the ChainedFilter class which calls enableFilters on each one of them.

The DenyAllFilter filter is important. It checks if the logged in user has been denied access to a certain type of item. If so, it applies a filter that cause all queryes not to return any items.

The OwnableFilter and ShareableFilter are used for items implementing the OwnableData and SharableData interfaces. The check options like: Include.MINE, Include.SHARED and Include.IN_PROJECT.

The BasicFilter is assigned to all other items. It just checks if the logged in user has generic read permission or not. If not, no items are returned by a query, otherwise all items are returned. This filter may not be what is wanted by many items. For example, news can be read by any user if the today is between the start date and end date. Therfore this filter is optional and can be replaced by a query with another filter. The News class does exactly this:

// News.java
private static final QueryRuntimeFilter RUNTIME_FILTER = 
   new QueryRuntimeFilterImpl();
... other code

public static ItemQuery<News> getQuery()
{
   return new ItemQuery<News>(News.class, RUNTIME_FILTER);
}
... other code

private static class QueryRuntimeFilterImpl
   implements QueryRuntimeFilter
{
   public void enableFilters(QueryRuntimeFilterManager manager, 
      EntityQuery query, DbControl dc)
   {
      SessionControl sc = dc.getSessionControl();
      if (!sc.hasPermission(Permission.READ, Item.NEWS))
      {
         org.hibernate.Filter filter = manager.enableFilter("todaysNews");
         if (filter != null)
         {
            filter.setParameter("today", new Date());
         }
      }
   }
}

// HibernateUtil.addFilterConditions()
PersistentClass pc = ...
Class dataClass = pc.getMappedClass();
if (NewsData.class.isAssignableFrom(dataClass))
{
   pc.addFilter("todaysNews", 
      ":today >= start_date AND (:today <= end_date OR end_date IS NULL)");
}

// filter-def.hbm.xml
<filter-def name="todaysNews">
   <filter-param name="today" type="date" />
</filter-def>

As you can see, it requires a long chain to get a filter to work.

Most replacements of the optional filter is done by child items which needs to check the logged in user's permission on the parent item type instead of the actual item type. For example Help items which are children to Client.

// Help.java
private static final QueryRuntimeFilter RUNTIME_FILTER = 
   new QueryRuntimeFilterFactory.ChildFilter(Item.HELP, Item.CLIENT);
... other code

public static ItemQuery<Help> getQuery(Client client)
{
   ItemQuery<Help> query = null;
   if (client != null)
   {
      query = new ItemQuery<Help>(Help.class, null);
      query.restrictPermanent(
         Restrictions.eq(
            Hql.property("client"), 
            Hql.entity(client)
         )
      );
   }
   else
   {
      query = new ItemQuery<Help>(Help.class, RUNTIME_FILTER);
   }
   return query;
}

A query for child items usually take a parameter of the parent type. If the logged in user had read access to that item the user should also have read access to any child items. Therefore we can disable the optional filter by passing in null as a paremter to the ItemQuery constructor.

If the parent item is null we must check if the logged in user has generic read permission for all parents. This is done by the ChildFilter implementation, and we pass an instance of that class that to the ItemQuery constructor.