org.apache.cayenne.access.trans.QueryAssemblerHelper Maven / Gradle / Ivy
Show all versions of cayenne-client-nodeps
/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
****************************************************************/
package org.apache.cayenne.access.trans;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataObject;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.ObjRelationship;
/**
* Translates parts of the query to SQL.
* Always works in the context of parent Translator.
*
* @author Andrei Adamchik
*/
public abstract class QueryAssemblerHelper {
protected QueryAssembler queryAssembler;
public QueryAssemblerHelper() {
}
/** Creates QueryAssemblerHelper. Sets queryAssembler property. */
public QueryAssemblerHelper(QueryAssembler queryAssembler) {
this.queryAssembler = queryAssembler;
}
/** Returns parent QueryAssembler that uses this helper. */
public QueryAssembler getQueryAssembler() {
return queryAssembler;
}
public void setQueryAssembler(QueryAssembler queryAssembler) {
this.queryAssembler = queryAssembler;
}
/** Translates the part of parent translator's query that is supported
* by this PartTranslator. For example, QualifierTranslator will process
* qualifier expression, OrderingTranslator - ordering of the query.
* In the process of translation parent translator is notified of any
* join tables added (so that it can update its "FROM" clause).
* Also parent translator is consulted about table aliases to use
* when translating columns. */
public abstract String doTranslation();
public ObjEntity getObjEntity() {
return getQueryAssembler().getRootEntity();
}
public DbEntity getDbEntity() {
return getQueryAssembler().getRootDbEntity();
}
/** Processes parts of the OBJ_PATH expression. */
protected void appendObjPath(StringBuffer buf, Expression pathExp) {
Iterator it = getObjEntity().resolvePathComponents(pathExp);
ObjRelationship lastRelationship = null;
while (it.hasNext()) {
Object pathComp = it.next();
if (pathComp instanceof ObjRelationship) {
ObjRelationship rel = (ObjRelationship) pathComp;
// if this is a last relationship in the path,
// it needs special handling
if (!it.hasNext()) {
processRelTermination(buf, rel);
}
else {
// find and add joins ....
Iterator relit = rel.getDbRelationships().iterator();
while (relit.hasNext()) {
queryAssembler.dbRelationshipAdded((DbRelationship) relit.next());
}
}
lastRelationship = rel;
}
else {
ObjAttribute objAttr = (ObjAttribute) pathComp;
if (lastRelationship != null) {
List lastDbRelList = lastRelationship.getDbRelationships();
DbRelationship lastDbRel =
(DbRelationship) lastDbRelList.get(lastDbRelList.size() - 1);
processColumn(buf, objAttr.getDbAttribute(), lastDbRel);
}
else {
processColumn(buf, objAttr.getDbAttribute());
}
}
}
}
protected void appendDbPath(StringBuffer buf, Expression pathExp) {
Iterator it = getDbEntity().resolvePathComponents(pathExp);
while (it.hasNext()) {
Object pathComp = it.next();
if (pathComp instanceof DbRelationship) {
DbRelationship rel = (DbRelationship) pathComp;
// if this is a last relationship in the path,
// it needs special handling
if (!it.hasNext()) {
processRelTermination(buf, rel);
}
else {
// find and add joins ....
queryAssembler.dbRelationshipAdded(rel);
}
}
else {
DbAttribute dbAttr = (DbAttribute) pathComp;
processColumn(buf, dbAttr);
}
}
}
/** Appends column name of a column in a root entity. */
protected void processColumn(StringBuffer buf, Expression nameExp) {
if (queryAssembler.supportsTableAliases()) {
String alias = queryAssembler.aliasForTable(getDbEntity());
buf.append(alias).append('.');
}
buf.append(nameExp.getOperand(0));
}
protected void processColumn(
StringBuffer buf,
DbAttribute dbAttr,
DbRelationship relationship) {
String alias = null;
if (queryAssembler.supportsTableAliases()) {
if (relationship != null) {
alias = queryAssembler.aliasForTable(
(DbEntity) dbAttr.getEntity(),
relationship);
}
// sometimes lookup for relationship fails (any specific case other than
// relationship being null?), so lookup by entity. Note that as CAY-194
// shows, lookup by DbEntity may produce incorrect results for
// reflexive relationships.
if (alias == null) {
alias = queryAssembler.aliasForTable((DbEntity) dbAttr.getEntity());
}
}
buf.append(dbAttr.getAliasedName(alias));
}
protected void processColumn(StringBuffer buf, DbAttribute dbAttr) {
String alias =
(queryAssembler.supportsTableAliases())
? queryAssembler.aliasForTable((DbEntity) dbAttr.getEntity())
: null;
buf.append(dbAttr.getAliasedName(alias));
}
/**
* Appends SQL code to the query buffer to handle val
as a
* parameter to the PreparedStatement being built. Adds val
* into QueryAssembler parameter list.
*
* If val
is null, "NULL" is appended to the query.
*
* If val
is a DataObject, its primary key value is
* used as a parameter. Only objects with a single column primary key
* can be used.
*
* @param buf query buffer.
*
* @param val object that should be appended as a literal to the query.
* Must be of one of "standard JDBC" types, null or a DataObject.
*
* @param attr DbAttribute that has information on what type of parameter
* is being appended.
*
*/
protected void appendLiteral(
StringBuffer buf,
Object val,
DbAttribute attr,
Expression parentExpression) {
if (val == null) {
buf.append("NULL");
}
else if (val instanceof DataObject) {
ObjectId id = ((DataObject) val).getObjectId();
// check if this id is acceptable to be a parameter
if (id == null) {
throw new CayenneRuntimeException("Can't use TRANSIENT object as a query parameter.");
}
if (id.isTemporary()) {
throw new CayenneRuntimeException("Can't use NEW object as a query parameter.");
}
Map snap = id.getIdSnapshot();
if (snap.size() != 1) {
StringBuffer msg = new StringBuffer();
msg
.append("Object must have a single primary key column ")
.append("to serve as a query parameter. ")
.append("This object has ")
.append(snap.size())
.append(": ")
.append(snap);
throw new CayenneRuntimeException(msg.toString());
}
// checks have been passed, use id value
appendLiteralDirect(
buf,
snap.get(snap.keySet().iterator().next()),
attr,
parentExpression);
}
else {
appendLiteralDirect(buf, val, attr, parentExpression);
}
}
/**
* Appends SQL code to the query buffer to handle val
as a
* parameter to the PreparedStatement being built. Adds val
* into QueryAssembler parameter list.
*
*
* @param buf query buffer
* @param val object that should be appended as a literal to the query.
* Must be of one of "standard JDBC" types. Can not be null.
*/
protected void appendLiteralDirect(
StringBuffer buf,
Object val,
DbAttribute attr,
Expression parentExpression) {
buf.append('?');
// we are hoping that when processing parameter list,
// the correct type will be
// guessed without looking at DbAttribute...
queryAssembler.addToParamList(attr, val);
}
/**
* Returns database type of expression parameters or
* null if it can not be determined.
*/
protected DbAttribute paramsDbType(Expression e) {
int len = e.getOperandCount();
// ignore unary expressions
if (len < 2) {
return null;
}
// naive algorithm:
// if at least one of the sibling operands is a
// OBJ_PATH or DB_PATH expression, use its attribute type as
// a final answer.
// find attribute or relationship matching the value
DbAttribute attribute = null;
DbRelationship relationship = null;
for (int i = 0; i < len; i++) {
Object op = e.getOperand(i);
if (op instanceof Expression) {
Expression expression = (Expression) op;
if (expression.getType() == Expression.OBJ_PATH) {
Object last = getObjEntity().lastPathComponent(expression);
if (last instanceof ObjAttribute) {
attribute = ((ObjAttribute) last).getDbAttribute();
break;
}
else if (last instanceof ObjRelationship) {
ObjRelationship objRelationship = (ObjRelationship) last;
List dbPath = objRelationship.getDbRelationships();
if (dbPath.size() > 0) {
relationship = (DbRelationship) dbPath.get(dbPath.size() - 1);
break;
}
}
}
else if (expression.getType() == Expression.DB_PATH) {
Object last = getDbEntity().lastPathComponent(expression);
if (last instanceof DbAttribute) {
attribute = (DbAttribute) last;
break;
}
else if (last instanceof DbRelationship) {
relationship = (DbRelationship) last;
break;
}
}
}
}
if (attribute != null) {
return attribute;
}
if (relationship != null) {
// Can't properly handle multiple joins....
if (relationship.getJoins().size() == 1) {
DbJoin join = (DbJoin) relationship.getJoins().get(0);
return join.getSource();
}
}
return null;
}
/** Processes case when an OBJ_PATH expression ends with relationship.
* If this is a "to many" relationship, a join is added and a column
* expression for the target entity primary key. If this is a "to one"
* relationship, column expresion for the source foreign key is added.
*/
protected void processRelTermination(StringBuffer buf, ObjRelationship rel) {
Iterator dbRels = rel.getDbRelationships().iterator();
// scan DbRelationships
while (dbRels.hasNext()) {
DbRelationship dbRel = (DbRelationship) dbRels.next();
// if this is a last relationship in the path,
// it needs special handling
if (!dbRels.hasNext()) {
processRelTermination(buf, dbRel);
}
else {
// find and add joins ....
queryAssembler.dbRelationshipAdded(dbRel);
}
}
}
/**
* Handles case when a DB_NAME expression ends with relationship.
* If this is a "to many" relationship, a join is added and a column
* expression for the target entity primary key. If this is a "to one"
* relationship, column expresion for the source foreign key is added.
*/
protected void processRelTermination(StringBuffer buf, DbRelationship rel) {
if (rel.isToMany()) {
// append joins
queryAssembler.dbRelationshipAdded(rel);
}
// get last DbRelationship on the list
List joins = rel.getJoins();
if (joins.size() != 1) {
StringBuffer msg = new StringBuffer();
msg
.append("OBJ_PATH expressions are only supported ")
.append("for a single-join relationships. ")
.append("This relationship has ")
.append(joins.size())
.append(" joins.");
throw new CayenneRuntimeException(msg.toString());
}
DbJoin join = (DbJoin) joins.get(0);
DbAttribute att = null;
if (rel.isToMany()) {
DbEntity ent = (DbEntity) join.getRelationship().getTargetEntity();
List pk = ent.getPrimaryKey();
if (pk.size() != 1) {
StringBuffer msg = new StringBuffer();
msg
.append("DB_NAME expressions can only support ")
.append("targets with a single column PK. ")
.append("This entity has ")
.append(pk.size())
.append(" columns in primary key.");
throw new CayenneRuntimeException(msg.toString());
}
att = (DbAttribute) pk.get(0);
}
else {
att = join.getSource();
}
processColumn(buf, att);
}
}