/* * Copyright 2011, Mysema Ltd * * Licensed 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 com.mysema.query.sql; import java.lang.reflect.InvocationTargetException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mysema.commons.lang.CloseableIterator; import com.mysema.query.DefaultQueryMetadata; import com.mysema.query.JoinFlag; import com.mysema.query.Query; import com.mysema.query.QueryException; import com.mysema.query.QueryFlag; import com.mysema.query.QueryFlag.Position; import com.mysema.query.QueryMetadata; import com.mysema.query.QueryModifiers; import com.mysema.query.SearchResults; import com.mysema.query.Tuple; import com.mysema.query.support.ProjectableQuery; import com.mysema.query.support.QueryMixin; import com.mysema.query.types.Expression; import com.mysema.query.types.ExpressionUtils; import com.mysema.query.types.FactoryExpression; import com.mysema.query.types.ParamExpression; import com.mysema.query.types.ParamNotSetException; import com.mysema.query.types.Path; import com.mysema.query.types.Predicate; import com.mysema.query.types.QTuple; import com.mysema.query.types.SubQueryExpression; import com.mysema.query.types.query.ListSubQuery; import com.mysema.query.types.template.NumberTemplate; import com.mysema.query.types.template.SimpleTemplate; import com.mysema.util.ResultSetAdapter; /** * /** * AbstractSQLQuery is the base type for SQL query implementations * * @author tiwe * * @param concrete subtype */ public abstract class AbstractSQLQuery & Query> extends ProjectableQuery implements SQLCommonQuery { private static final Logger logger = LoggerFactory.getLogger(AbstractSQLQuery.class); @Nullable private final Connection conn; @Nullable private List constants; @Nullable private List> constantPaths; @Nullable protected Expression union; private final Configuration configuration; protected final QueryMixin queryMixin; protected boolean unionAll; public AbstractSQLQuery(@Nullable Connection conn, Configuration configuration) { this(conn, configuration, new DefaultQueryMetadata().noValidate()); } @SuppressWarnings("unchecked") public AbstractSQLQuery(@Nullable Connection conn, Configuration configuration, QueryMetadata metadata) { super(new QueryMixin(metadata, false)); this.queryMixin = (QueryMixin)super.queryMixin; this.queryMixin.setSelf((Q) this); this.conn = conn; this.configuration = configuration; } /** * Add the given String literal as a join flag to the last added join with the position * BEFORE_TARGET * * @param flag * @return */ public Q addJoinFlag(String flag) { return addJoinFlag(flag, JoinFlag.Position.BEFORE_TARGET); } /** * Add the given String literal as a join flag to the last added join * * @param flag * @param position * @return */ @SuppressWarnings("unchecked") public Q addJoinFlag(String flag, JoinFlag.Position position) { queryMixin.addJoinFlag(new JoinFlag(flag, position)); return (Q)this; } /** * Add the given prefix and expression as a general query flag * * @param position position of the flag * @param prefix prefix for the flag * @param expr expression of the flag * @return */ public Q addFlag(Position position, String prefix, Expression expr) { Expression flag = SimpleTemplate.create(expr.getType(), prefix + "{0}", expr); return queryMixin.addFlag(new QueryFlag(position, flag)); } /** * Add the given String literal as query flag * * @param position * @param flag * @return */ public Q addFlag(Position position, String flag) { return queryMixin.addFlag(new QueryFlag(position, flag)); } /** * Add the given Expression as a query flag * * @param position * @param flag * @return */ public Q addFlag(Position position, Expression flag) { return queryMixin.addFlag(new QueryFlag(position, flag)); } protected String buildQueryString(boolean forCountRow) { SQLSerializer serializer = createSerializer(); if (union != null) { serializer.serializeUnion(union, queryMixin.getMetadata(), unionAll); } else { serializer.serialize(queryMixin.getMetadata(), forCountRow); } constants = serializer.getConstants(); constantPaths = serializer.getConstantPaths(); return serializer.toString(); } @Override public long count() { try { return unsafeCount(); } catch (SQLException e) { String error = "Caught " + e.getClass().getName(); logger.error(error, e); throw new QueryException(e.getMessage(), e); } } @Override public boolean exists() { return limit(1).singleResult(NumberTemplate.ONE) != null; } /** * If you use forUpdate() with a backend that uses page or row locks, rows examined by the * query are write-locked until the end of the current transaction. * * Not supported for SQLite and CUBRID * * @return */ public Q forUpdate() { return addFlag(Position.END, configuration.getTemplates().getForUpdate()); } protected SQLSerializer createSerializer() { return new SQLSerializer(configuration); } public Q from(Expression arg) { return queryMixin.from(arg); } public Q from(Expression... args) { return queryMixin.from(args); } @SuppressWarnings("unchecked") public Q from(SubQueryExpression subQuery, Path alias) { return queryMixin.from(ExpressionUtils.as((Expression)subQuery, alias)); } public Q fullJoin(RelationalPath target) { return queryMixin.fullJoin(target); } public Q fullJoin(RelationalFunctionCall target, Path alias) { return queryMixin.fullJoin(target, alias); } public Q fullJoin(SubQueryExpression target, Path alias) { return queryMixin.fullJoin(target, alias); } public Q fullJoin(ForeignKey key, RelationalPath entity) { return queryMixin.fullJoin(entity).on(key.on(entity)); } public Q innerJoin(RelationalPath target) { return queryMixin.innerJoin(target); } public Q innerJoin(RelationalFunctionCall target, Path alias) { return queryMixin.innerJoin(target, alias); } public Q innerJoin(SubQueryExpression target, Path alias) { return queryMixin.innerJoin(target, alias); } public Q innerJoin(ForeignKey key, RelationalPath entity) { return queryMixin.innerJoin(entity).on(key.on(entity)); } public Q join(RelationalPath target) { return queryMixin.join(target); } public Q join(RelationalFunctionCall target, Path alias) { return queryMixin.join(target, alias); } public Q join(SubQueryExpression target, Path alias) { return queryMixin.join(target, alias); } public Q join(ForeignKey key, RelationalPath entity) { return queryMixin.join(entity).on(key.on(entity)); } public Q leftJoin(RelationalPath target) { return queryMixin.leftJoin(target); } public Q leftJoin(RelationalFunctionCall target, Path alias) { return queryMixin.leftJoin(target, alias); } public Q leftJoin(SubQueryExpression target, Path alias) { return queryMixin.leftJoin(target, alias); } public Q leftJoin(ForeignKey key, RelationalPath entity) { return queryMixin.leftJoin(entity).on(key.on(entity)); } public Q rightJoin(RelationalPath target) { return queryMixin.rightJoin(target); } public Q rightJoin(RelationalFunctionCall target, Path alias) { return queryMixin.rightJoin(target, alias); } public Q rightJoin(SubQueryExpression target, Path alias) { return queryMixin.rightJoin(target, alias); } public Q rightJoin(ForeignKey key, RelationalPath entity) { return queryMixin.rightJoin(entity).on(key.on(entity)); } @Nullable private T get(ResultSet rs, Expression expr, int i, Class type) throws SQLException { return configuration.get(rs, expr instanceof Path ? (Path)expr : null, i, type); } private void set(PreparedStatement stmt, Path path, int i, Object value) throws SQLException{ configuration.set(stmt, path, i, value); } public QueryMetadata getMetadata() { return queryMixin.getMetadata(); } /** * Get the results as an JDBC result set * * @param args * @return */ public ResultSet getResults(Expression... exprs) { queryMixin.addProjection(exprs); String queryString = buildQueryString(false); if (logger.isDebugEnabled()) { logger.debug("query : {}", queryString); } try { final PreparedStatement stmt = conn.prepareStatement(queryString); setParameters(stmt, constants, constantPaths, getMetadata().getParams()); final ResultSet rs = stmt.executeQuery(); return new ResultSetAdapter(rs) { @Override public void close() throws SQLException { try { super.close(); } finally { stmt.close(); } } }; } catch (SQLException e) { throw new QueryException(e); } finally { reset(); } } private Union innerUnion(SubQueryExpression... sq) { queryMixin.getMetadata().setValidate(false); if (!queryMixin.getMetadata().getJoins().isEmpty()) { throw new IllegalArgumentException("Don't mix union and from"); } this.union = UnionUtils.union(sq, unionAll); return new UnionImpl((Q)this, sq[0].getMetadata().getProjection()); } protected Configuration getConfiguration() { return configuration; } @Override public CloseableIterator iterate(Expression... args) { return iterate(new QTuple(args)); } @Override public CloseableIterator iterate(Expression expr) { expr = queryMixin.addProjection(expr); return iterateSingle(queryMixin.getMetadata(), expr); } @SuppressWarnings("unchecked") private CloseableIterator iterateSingle(QueryMetadata metadata, @Nullable final Expression expr) { final String queryString = buildQueryString(false); if (logger.isDebugEnabled()) { logger.debug("query : {}", queryString); } try { final PreparedStatement stmt = conn.prepareStatement(queryString); setParameters(stmt, constants, constantPaths, metadata.getParams()); final ResultSet rs = stmt.executeQuery(); if (expr == null) { return new SQLResultIterator(stmt, rs) { @Override public RT produceNext(ResultSet rs) throws Exception { return (RT) rs.getObject(1); } }; } else if (expr instanceof FactoryExpression) { return new SQLResultIterator(stmt, rs) { @Override public RT produceNext(ResultSet rs) throws Exception { return newInstance((FactoryExpression) expr, rs, 0); } }; } else if (expr.getType().isArray()) { return new SQLResultIterator(stmt, rs) { @Override public RT produceNext(ResultSet rs) throws Exception { Object[] rv = new Object[rs.getMetaData().getColumnCount()]; for (int i = 0; i < rv.length; i++) { rv[i] = rs.getObject(i+1); } return (RT) rv; } }; } else { return new SQLResultIterator(stmt, rs) { @Override public RT produceNext(ResultSet rs) throws Exception { return get(rs, expr, 1, expr.getType()); } }; } } catch (SQLException e) { throw new QueryException("Caught " + e.getClass().getSimpleName() + " for " + queryString, e); } finally { reset(); } } @Override public List list(Expression... args) { return list(new QTuple(args)); } @Override public List list(Expression expr) { expr = queryMixin.addProjection(expr); final String queryString = buildQueryString(false); if (logger.isDebugEnabled()) { logger.debug("query : {}", queryString); } try { final PreparedStatement stmt = conn.prepareStatement(queryString); try { setParameters(stmt, constants, constantPaths, queryMixin.getMetadata().getParams()); final ResultSet rs = stmt.executeQuery(); try { final List rv = new ArrayList(); if (expr instanceof FactoryExpression) { FactoryExpression fe = (FactoryExpression)expr; while (rs.next()) { rv.add(newInstance(fe, rs, 0)); } } else if (expr.getType().isArray()) { while (rs.next()) { Object[] row = new Object[rs.getMetaData().getColumnCount()]; for (int i = 0; i < row.length; i++) { row[i] = rs.getObject(i+1); } rv.add((RT)row); } } else { while (rs.next()) { rv.add(get(rs, expr, 1, expr.getType())); } } return rv; } catch (IllegalAccessException e) { throw new QueryException(e); } catch (InvocationTargetException e) { throw new QueryException(e); } catch (InstantiationException e) { throw new QueryException(e); } catch (SQLException e) { throw new QueryException(e); } finally { rs.close(); } } finally { stmt.close(); } } catch (SQLException e) { throw new QueryException("Caught " + e.getClass().getSimpleName() + " for " + queryString, e); } finally { reset(); } } @Override public SearchResults listResults(Expression... args) { return listResults(new QTuple(args)); } @Override public SearchResults listResults(Expression expr) { queryMixin.addProjection(expr); long total = count(); try { if (total > 0) { queryMixin.getMetadata().clearProjection(); QueryModifiers modifiers = queryMixin.getMetadata().getModifiers(); return new SearchResults(list(expr), modifiers, total); } else { return SearchResults.emptyResults(); } } finally { reset(); } } private RT newInstance(FactoryExpression c, ResultSet rs, int offset) throws InstantiationException, IllegalAccessException, InvocationTargetException, SQLException{ Object[] args = new Object[c.getArgs().size()]; for (int i = 0; i < args.length; i++) { args[i] = get(rs, c.getArgs().get(i), offset + i + 1, c.getArgs().get(i).getType()); } return c.newInstance(args); } public Q on(Predicate condition) { return queryMixin.on(condition); } public Q on(Predicate... conditions) { return queryMixin.on(conditions); } private void reset() { queryMixin.getMetadata().reset(); constants = null; } protected void setParameters(PreparedStatement stmt, List objects, List> constantPaths, Map, ?> params) { if (objects.size() != constantPaths.size()) { throw new IllegalArgumentException("Expected " + objects.size() + " paths, but got " + constantPaths.size()); } for (int i = 0; i < objects.size(); i++) { Object o = objects.get(i); try { if (o instanceof ParamExpression) { if (!params.containsKey(o)) { throw new ParamNotSetException((ParamExpression) o); } o = params.get(o); } set(stmt, constantPaths.get(i), i+1, o); } catch (SQLException e) { throw new IllegalArgumentException(e); } } } @Override public String toString() { return buildQueryString(false).trim(); } /** * Creates an union expression for the given subqueries * * @param * @param sq * @return */ public Union union(ListSubQuery... sq) { return innerUnion(sq); } /** * Creates an union expression for the given subqueries * * @param * @param sq * @return */ public Q union(Path alias, ListSubQuery... sq) { return from(UnionUtils.union(sq, alias, false)); } /** * Creates an union expression for the given subqueries * * @param * @param sq * @return */ public Union union(SubQueryExpression... sq) { return innerUnion(sq); } /** * Creates an union expression for the given subqueries * * @param * @param sq * @return */ public Q union(Path alias, SubQueryExpression... sq) { return from(UnionUtils.union(sq, alias, false)); } /** * Creates an union expression for the given subqueries * * @param * @param sq * @return */ public Union unionAll(ListSubQuery... sq) { unionAll = true; return innerUnion(sq); } /** * Creates an union expression for the given subqueries * * @param * @param sq * @return */ public Q unionAll(Path alias, ListSubQuery... sq) { return from(UnionUtils.union(sq, alias, true)); } /** * Creates an union expression for the given subqueries * * @param * @param sq * @return */ public Union unionAll(SubQueryExpression... sq) { unionAll = true; return innerUnion(sq); } /** * Creates an union expression for the given subqueries * * @param * @param sq * @return */ public Q unionAll(Path alias, SubQueryExpression... sq) { return from(UnionUtils.union(sq, alias, true)); } @Override public Tuple uniqueResult(Expression... expr) { return uniqueResult(new QTuple(expr)); } @Override public RT uniqueResult(Expression expr) { if (getMetadata().getModifiers().getLimit() == null && !expr.toString().contains("count(")) { limit(2); } CloseableIterator iterator = iterate(expr); return uniqueResult(iterator); } private long unsafeCount() throws SQLException { final String queryString = buildQueryString(true); if (logger.isDebugEnabled()) { logger.debug("query : {}", queryString); } PreparedStatement stmt = null; ResultSet rs = null; try { stmt = conn.prepareStatement(queryString); setParameters(stmt, constants, constantPaths, getMetadata().getParams()); rs = stmt.executeQuery(); rs.next(); return rs.getLong(1); } catch (SQLException e) { throw new QueryException(e.getMessage(), e); } finally { try { if (rs != null) { rs.close(); } } finally { if (stmt != null) { stmt.close(); } } } } }