Last changed: May 22, 2005 12:15 by
Andrus Adamchik
With Cayenne development schedule that we are trying to predict and maintain, some of the 1.2 features will come as a complete surprise even to me. I came across one of them just today when working on the new multi-tier design.
Certain operations in Cayenne are expressed internally using instances of Query
. For example if a DataObject needs to inflate one of its to-many relationships, behind the scenes a SelectQuery is created and then passed to a DataContext for execution. For the client tier I wanted to use the same approach, however the queries had to be more lightweight and less dependent on the server-side mapping data. Unfortunately adding new kinds of queries to Cayenne caused a ripple effect all the way through the access stack, as DataNode needed to know how to process them. If you multiply this by 11 kinds of DbAdapters that we currently have, things got really ugly. So adding new query types was certanly a bad idea.
The solution turned out to be much easier than I thought. With my current obsession with the Visitor pattern
, in just a few hours I was able to refactor DataNode to match arbitrary queries with pluggable JDBC execution algorithms by adding SQLActionVisitor interface to the query package, and letting DbAdapter to provide its own visitor implementation. As a result I cleaned up a lot of garbage code from DbAdapters and DataNode, and now I will be able to build my special client tier queries. But it was a side effect of this change that will have the biggest consequences in improving Cayenne. We ended up having a user-level API for extending Cayenne query behavior.
Two main types of extensions that Cayenne users can build now are (1) specialized queries that can internally delegate the execution step to a standard Cayenne query and (2) custom queries that provide their own JDBC-level execution algorithm. Here is some examples (note that to run these you will need HEAD CVS code or nightly build starting from May 22, 2005 and newer).
First we create a convenience superclass to quickly build custom selecting queries:
package query.ext;
import java.util.Collection;
import java.util.Collections;
import org.objectstyle.cayenne.query.AbstractQuery;
import org.objectstyle.cayenne.query.GenericSelectQuery;
import org.objectstyle.cayenne.query.SQLAction;
import org.objectstyle.cayenne.query.SQLActionVisitor;
/**
* A convenience superclass for custom select queries.
*
* @author Andrei Adamchik
*/
public abstract class AbstractSelectQuery extends AbstractQuery implements
GenericSelectQuery {
public abstract SQLAction toSQLAction(SQLActionVisitor visitor);
public String getCachePolicy() {
return GenericSelectQuery.NO_CACHE;
}
public int getFetchLimit() {
return -1;
}
public Collection getJointPrefetches() {
return Collections.EMPTY_SET;
}
public int getPageSize() {
return -1;
}
public boolean isFetchingDataRows() {
return true;
}
public boolean isRefreshingObjects() {
return false;
}
public boolean isResolvingInherited() {
return false;
}
}
Now we can build a custom selecting query that returns a list of related objects for a given DataObject and a relationships name:
package query.ext;
import org.objectstyle.cayenne.DataObject;
import org.objectstyle.cayenne.access.util.QueryUtils;
import org.objectstyle.cayenne.query.SQLAction;
import org.objectstyle.cayenne.query.SQLActionVisitor;
import org.objectstyle.cayenne.query.SelectQuery;
/**
* A query that fetches related objects for a given DataObject. Demonstrates how a custom
* query can delegate its execution to a "standard" query that Cayenne understands.
*
* @author Andrei Adamchik
*/
public class RelationshipQuery extends AbstractSelectQuery {
protected SelectQuery substituteQuery;
public RelationshipQuery(DataObject object, String relationship) {
this.substituteQuery = QueryUtils.selectRelationshipObjects(object
.getDataContext(), object, relationship);
}
public Object getRoot() {
return substituteQuery.getRoot();
}
public SQLAction toSQLAction(SQLActionVisitor visitor) {
return substituteQuery.toSQLAction(visitor);
}
}
As you see a custom query can be built in just a few lines of code and no changes to Cayenne. The trick is in implementing new Query interface method "toSQLAction". But lets go further and implement a query that returns a single data row with the information about the database. This time we need to provide both Query and SQLAction implementations:
package query.ext;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.objectstyle.cayenne.access.OperationObserver;
import org.objectstyle.cayenne.query.SQLAction;
/**
* A custom SQLAction that returns a single row from the database that contains some of
* the DB metadata.
*
* @author Andrei Adamchik
*/
public class DBInfoAction implements SQLAction {
protected DBInfoQuery query;
public DBInfoAction(DBInfoQuery query) {
this.query = query;
}
public void performAction(Connection connection, OperationObserver observer)
throws SQLException, Exception {
Map infoRow = new HashMap();
DatabaseMetaData metaData = connection.getMetaData();
infoRow.put("DB.name", metaData.getDatabaseProductName());
infoRow.put("DB.product.version", metaData.getDatabaseProductVersion());
infoRow.put("DB.version", metaData.getDatabaseMajorVersion()
+ "."
+ metaData.getDatabaseMinorVersion());
observer.nextDataRows(query, Collections.singletonList(infoRow));
}
}
package query.ext;
import org.objectstyle.cayenne.query.SQLAction;
import org.objectstyle.cayenne.query.SQLActionVisitor;
/**
* @author Andrei Adamchik
*/
public class DBInfoQuery extends AbstractSelectQuery {
/**
* Create a DBInfoQuery with "root". Root can be any Cayenne root, such as DataObject
* class or DataMap name.
*/
public DBInfoQuery(Object root) {
setRoot(root);
}
public SQLAction toSQLAction(SQLActionVisitor visitor) {
return new DBInfoAction(this);
}
}
... and now call the query ...
DataContext context = DataContext.createDataContext();
DataMap map = context.getEntityResolver().getDataMap("map");
List infos = context.performQuery(new DBInfoQuery(map));
System.out.println("DB INFO: " + infos.get(0));
DBInfoQuery works just like a regular Cayenne selecting query, without Cayenne knowing anything about its execution strategy. I can see lots of possibilities with this feature....