Sunteți pe pagina 1din 5

Query Object

An object that represents a database query.

SQL can be an involved language, and many developers aren't particularly familiar with it. Furthermore, you need to know what the database schema looks like to form queries. You can avoid this by creating specialized finder methods that hide the SQL inside parameterized methods, but that makes it difficult to form more ad hoc queries. It also leads to duplication in the SQL statements should the database schema change. A Query Object is an interpreter [Gang of Four], that is, a structure of objects that can form itself into a SQL query. You can create this query by referring to classes and fields rather than tables and columns. In this way those who write the queries can do so independently of the database schema and changes to the schema can be localized in a single place. How It Works A Query Object is an application of the Interpreter pattern geared to represent a SQL query. Its primary roles are to allow a client to form queries of various kinds and to turn those object structures into the appropriate SQL string. In order to represent any query, you need a flexible Query Object. Often, however, applications can make do with a lot less than the full power of SQL, in which case your Query Object can be simpler. It won't be able to represent anything, but it can satisfy your particular needs. Moreover, it's usually no more work to enhance it when you need more capability than it is to create a fully capable Query Object right from the beginning. As a result you should create a minimally functional Query Object for your current needs and evolve it as those needs grow. A common feature of Query Object is that it can represent queries in the language of the in-memory objects rather than the database schema. That means that, instead of using table and column names, you can use object and field names. While this isn't important if your objects and database have the same structure, it can be very useful if you get

variations between the two. In order to perform this change of view, the Query Object needs to know how the database structure maps to the object structure, a capability that really needs Metadata Mapping (306). For multiple databases you can design your Query Object so that it produces different SQL depending on which database the query is running against. At it's simplest level it can take into account the annoying differences in SQL syntax that keep cropping up; at a more ambitious level it can use different mappings to cope with the same classes being stored in different database schemas. A particularly sophisticated use of Query Object is to eliminate redundant queries against a database. If you see that you've run the same query earlier in a session, you can use it to select objects from the Identity Map (195) and avoid a trip to the database. A more sophisticated approach can detect whether one query is a particular case of an earlier query, such as a query that is the same as an earlier one but with an additional clause linked with an AND. Exactly how to achieve these more sophisticated features is beyond the scope of this book, but they're the kind of features that O/R mapping tools may provide. A variation on the Query Object is to allow a query to be specified by an example domain object. Thus, you might have a person object whose last name is set to Fowler but all of those other attributes are set to null. You can treat it as a query by example that's processed like the Interpreter-style Query Object. That returns all people in the database whose last name is Fowler, and it's very simple and convenient to use. However, it breaks down for complex queries. When to Use It Query Objects are a pretty sophisticated pattern to put together, so most projects don't use them if they have a handbuilt data source layer. You only really need them when you're using Domain Model (116) and Data Mapper (165); you also really need Metadata Mapping (306) to make serious use of them. Even then Query Objects aren't always necessary, as many developers are comfortable with SQL. You can hide many of the details of the database schema behind specific finder methods. The advantages of Query Object come with more sophisticated needs: keeping database schemas encapsulated, supporting multiple databases, supporting multiple schemas, and optimizing to avoid multiple queries. Some projects with a particularly sophisticated data source team might want to build these capabilities themselves, but most people who use Query Object do so with a commercial tool. My inclination is that you're almost always better off buying a tool. All that said, you may find that a limited Query Object fulfills your needs without being difficult to build on a project that doesn't justify a fully featured version. The trick is to pare down the functionality to no more than you actually use. Further Reading

You can find an example of Query Object in [Alpert et al.] in the discussion of interpreters. Query Object is also closely linked to the Specification pattern in [Evans and Fowler] and [Evans]. Example: A Simple Query Object (Java) This is a simple example of a Query Objectrather less than would be useful for most situations but enough to give you an idea of what a Query Object is about. It can query a single table based on set of criteria "AND'ed" together (in slightly more technical language, it can handle a conjunction of elementary predicates). The Query Object is set up using the language of domain objects rather than that of the table structure. Thus, a query knows the class that it's for and a collection of criteria that correspond to the clauses of a where clause.
class QueryObject... private Class klass; private List criteria = new ArrayList();

A simple criterion is one that takes a field and a value and an SQL operator to compare them.
class Criteria... private String sqlOperator; protected String field; protected Object value;

To make it easier to create the right criteria, I can provide an appropriate creation method.
class Criteria... public static Criteria greaterThan(String fieldName, int value) { return Criteria.greaterThan(fieldName, new Integer(value)); } public static Criteria greaterThan(String fieldName, Object value) { return new Criteria(" > ", fieldName, value); } private Criteria(String sql, String field, Object value) { this.sqlOperator = sql; this.field = field; this.value = value; }

This allows me to find everyone with dependents by forming a query such as


class Criteria... QueryObject query = new QueryObject(Person.class); query.addCriteria(Criteria.greaterThan("numberOfDependents", 0));

Thus, if I have a person object such as this:


class Person... private String lastName; private String firstName; private int numberOfDependents;

I can ask for all people with dependents by creating a query for person and adding a criterion.
QueryObject query = new QueryObject(Person.class); query.addCriteria(Criteria.greaterThan("numberOfDependents", 0));

That's enough to describe the query. Now the query needs to execute by turning itself into a SQL select. In this case I assume that my mapper class supports a method that finds objects based on a string that's a where clause.
class QueryObject... public Set execute(UnitOfWork uow) { this.uow = uow; return uow.getMapper(klass).findObjectsWhere(generateWhereClause()); } class Mapper... public Set findObjectsWhere (String whereClause) { String sql = "SELECT" + dataMap.columnList() + " FROM " + dataMap.getTableName() + " WHERE " + whereClause; PreparedStatement stmt = null; ResultSet rs = null; Set result = new HashSet(); try { stmt = DB.prepare(sql); rs = stmt.executeQuery(); result = loadAll(rs); } catch (Exception e) { throw new ApplicationException (e); } finally {DB.cleanUp(stmt, rs); } return result; }

Here I'm using a Unit of Work (184) that holds mappers indexed by the class and a mapper that uses Metadata Mapping (306). The code is the same as that in the example in Metadata Mapping (306) to save repeating the code in this section. To generate the where clause, the query iterates through the criteria and has each one print itself out, tying them together with ANDs.
class QueryObject... private String generateWhereClause() {

StringBuffer result = new StringBuffer(); for (Iterator it = criteria.iterator(); it.hasNext();) { Criteria c = (Criteria)it.next(); if (result.length() != 0) result.append(" AND "); result.append(c.generateSql(uow.getMapper(klass).getDataMap())); } return result.toString(); } class Criteria... public String generateSql(DataMap dataMap) { return dataMap.getColumnForField(field) + sqlOperator + value; } class DataMap... public String getColumnForField (String fieldName) { for (Iterator it = getColumns(); it.hasNext();) { ColumnMap columnMap = (ColumnMap)it.next(); if (columnMap.getFieldName().equals(fieldName)) return columnMap.getColumnName(); } throw new ApplicationException ("Unable to find column for " + fieldName); }

As well as criteria with simple SQL operators, we can create more complex criteria classes that do a little more. Consider a case-insensitive pattern match query, like one that finds all people whose last names start with F. We can form a query object for all people with such dependents.
QueryObject query = new QueryObject(Person.class); query.addCriteria(Criteria.greaterThan("numberOfDependents", 0)); query.addCriteria(Criteria.matches("lastName", "f%"));

This uses a different criteria class that forms a more complex clause in the where statement.
class Criteria... public static Criteria matches(String fieldName, String pattern){ return new MatchCriteria(fieldName, pattern); } class MatchCriteria extends Criteria... public String generateSql(DataMap dataMap) { return "UPPER(" + dataMap.getColumnForField(field) + ") LIKE UPPER('" + value + "')"; }

S-ar putea să vă placă și