All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sqlg3.runtime.GBase Maven / Gradle / Ivy

Go to download

SQLG is a preprocessor and a library that uses code generation to simplify writing JDBC code

The newest version!
package sqlg3.runtime;

import sqlg3.runtime.queries.QueryParser;
import sqlg3.types.SQLGException;

import java.lang.reflect.Proxy;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

/**
 * Base class for all classes which are processed by preprocessor. Wraps access to JDBC methods allowing
 * preprocessor to intercept them and extract required information.
 * 

* This class itself is not thread-safe, so it cannot be used by more than * one thread at a time. Use wrappers generated by preprocessor to access business methods. */ public class GBase { /** * Use it for {@code autoKeys} parameter of {@link #executeUpdate(QueryLike, String[])} to * retrieve all generated columns. */ public static final String[] ALL_KEYS = new String[0]; static GTest test = null; private final GContext ctx; /** * Constructor. Usually it is called by generated wrappers. * * @param ctx context */ public GBase(GContext ctx) { this.ctx = ctx; } /** * Returns true if method is called at preprocessing time (false at application run time). */ public static boolean isTesting() { return test != null; } private Connection getConnection() throws SQLException { return ctx.connection; } /** * Access to raw JDBC connection. Can be used only at application run time, not at preprocess time, * so check {@link #isTesting} before calling this method. */ public final Connection getJdbcConnection() throws SQLException { if (test != null) throw new IllegalStateException("Cannot use Connection in preprocess mode"); return getConnection(); } public final DatabaseMetaData getMetaData() throws SQLException { DatabaseMetaData dbmd = getConnection().getMetaData(); if (test != null) { return (DatabaseMetaData) Proxy.newProxyInstance( getClass().getClassLoader(), new Class[] {DatabaseMetaData.class}, (proxy, method, args) -> { if ("getConnection".equals(method.getName()) && method.getParameterCount() == 0) { throw new IllegalStateException("Cannot use Connection in preprocess mode"); } else { return method.invoke(dbmd, args); } } ); } else { return dbmd; } } ///////////////////////////////// Query piece creation ///////////////////////////////// /** * Creates SQL query piece containing query text and its parameters. * Example: *

     * QueryPiece piece = query(" AND type_id = ?", in(typeId, Long.class));
     * 
* It is more convenient to use {@link sqlg3.annotations.Query} annotation to generate such pieces than * to use this method manually. * * @param sql query text, possibly containing references to parameters in the form of {@code ?} * @param params query parameters, see {@link #in} */ public static QueryPiece query(CharSequence sql, Parameter... params) { return new QueryPiece(sql, params); } ///////////////////////////////// Statement preparation ///////////////////////////////// private interface StatementFactory { S create(Connection connection, String sql) throws SQLException; } private interface StatementExecutor { R execute(S stmt) throws SQLException; } private R doExecuteAnyStatement(String sql, List params, StatementExecutor executor, StatementFactory factory) throws SQLException { Connection connection = getConnection(); long t0 = System.currentTimeMillis(); boolean ok = false; try { try (S stmt = factory.create(connection, sql)) { Parameter.setParameters(ctx.global.mappers, stmt, params); R result = executor.execute(stmt); ok = true; return result; } } finally { long time = System.currentTimeMillis() - t0; ctx.global.trace.trace(ok, time, () -> { List messages = new ArrayList<>(); messages.add("Last SQL:"); messages.add(sql); if (!params.isEmpty()) { StringBuilder buf = new StringBuilder(); buf.append("with params ("); for (int i = 0; i < params.size(); i++) { if (i > 0) { buf.append(", "); } buf.append(params.get(i)); } buf.append(")"); messages.add(buf.toString()); } return messages; }); } } private R doExecuteStatement(String[] autoKeys, QueryLike query, StatementExecutor executor) throws SQLException { String unparsedSql = query.getSql(); List params = query.getParameters(); String parsedSql = QueryParser.parseQuery(unparsedSql); if (autoKeys == null) { return doExecuteAnyStatement(parsedSql, params, executor, Connection::prepareStatement); } else { return doExecuteAnyStatement(parsedSql, params, executor, (connection, sql) -> { if (autoKeys.length > 0) { DatabaseMetaData meta = connection.getMetaData(); Function canonicalizer = QueryParser.getCanonicalizer(meta); String[] autoColumns = new String[autoKeys.length]; for (int i = 0; i < autoKeys.length; i++) { autoColumns[i] = canonicalizer.apply(autoKeys[i]); } return connection.prepareStatement(sql, autoColumns); } else { return connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); } }); } } private R doExecuteStatement(QueryLike query, StatementExecutor executor) throws SQLException { return doExecuteStatement(null, query, executor); } ///////////////////////////////// Raw PreparedStatements ///////////////////////////////// public final RawStatements raw() throws SQLException { return new RawStatements(getConnection()); } /** * Binds prepared statement parameters to specific values. * * @param st SQL statement * @param in parameter values */ public final void setParameters(PreparedStatement st, Parameter... in) throws SQLException { Parameter.setParameters(ctx.global.mappers, st, Arrays.asList(in)); } /** * Binds single prepared statement parameter to specific value. * * @param st SQL statement * @param index index of the parameter (from 1) * @param value parameter value * @param cls parameter class */ public final void setParameter(PreparedStatement st, int index, T value, Class cls) throws SQLException { in(value, cls).set(ctx.global.mappers, st, index); } public static int executeUpdate(PreparedStatement stmt) throws SQLException { if (test != null) { return 0; } else { return stmt.executeUpdate(); } } ///////////////////////////////// Setting parameters ///////////////////////////////// /** * For internal use. Do not use this method at runtime, it throws exception when not preprocessing. */ public static Parameter inP(Object value, String paramId) { if (test != null) { Class cls = test.setParamType(paramId, value.getClass()); return Parameter.in(value, cls); } else { throw new SQLGException("No type is defined for parameter " + paramId); } } /** * Same as {@link #in} but generated by preprocessor. */ public static Parameter inP(T value, Class cls) { return in(value, cls); } /** * Creates parameter for prepared statement. * * @param value parameter value. Can be null. * @param cls parameter class. Should be not null. */ public static Parameter in(T value, Class cls) { return Parameter.in(value, cls); } /** * For internal use. Do not use this method at runtime, it throws exception when not preprocessing. */ public static Parameter outP(Object value, String paramId) { if (test != null) { if (value == null || !value.getClass().isArray()) throw new SQLGException("Parameter should be an array"); test.setParamType(paramId, value.getClass().getComponentType()); return Parameter.out(value); } else { throw new SQLGException("No type is defined for out parameter " + paramId); } } /** * Same as {@link #out} but generated by preprocessor. */ public static Parameter outP(Object value) { return out(value); } /** * Creates OUT parameter for stored procedure call. * * @param value Should be an array with at least one element to store output value. * Should be not null. */ public static Parameter out(Object value) { if (test != null) { if (value == null || !value.getClass().isArray()) throw new SQLGException("Parameter should be an array"); } return Parameter.out(value); } ///////////////////////////////// Simple value statements ///////////////////////////////// private static boolean checkNext(ResultSet rs, boolean optional) throws SQLException { boolean hasNext = rs.next(); if (!hasNext) { if (optional) { return false; } else { throw new SQLException("No rows found"); } } else { return true; } } private static void tooManyRows(ResultSet rs) throws SQLException { if (rs.next()) throw new SQLException("Too many rows"); } private T singleOrOptionalRowQueryReturningT(Class cls, QueryLike query, boolean optional) throws SQLException { return doExecuteStatement(query, stmt -> { TypeMapper mapper = getMapper(cls); try (ResultSet rs = stmt.executeQuery()) { if (test != null) { test.checkOneColumn(rs, cls); return cls.cast(test.getTestObject(cls)); } else { if (!checkNext(rs, optional)) return null; T ret = mapper.fetch(rs, 1); tooManyRows(rs); return ret; } } }); } /** * Executes select query, which should return one row and one column (more or less than * one row raises runtime exception, more or less than one column raises * preprocess-time exception). * * @param cls class with user-defined mapping (see {@link RuntimeMapper}) */ public final T singleRowQueryReturning(Class cls, QueryLike query) throws SQLException { return singleOrOptionalRowQueryReturningT(cls, query, false); } /** * Executes select query, which should return one row and one column (more or less than * one row raises runtime exception, more or less than one column raises * preprocess-time exception). Result is returned as a single int. NULLs are returned as zeroes. */ public final int singleRowQueryReturningInt(QueryLike query) throws SQLException { Integer value = singleRowQueryReturning(Integer.class, query); return value == null ? 0 : value.intValue(); } /** * Executes select query, which should return one row and one column (more or less than * one row raises runtime exception, more or less than one column raises * preprocess-time exception). Result is returned as a single long. NULLs are returned as zeroes. */ public final long singleRowQueryReturningLong(QueryLike query) throws SQLException { Long value = singleRowQueryReturning(Long.class, query); return value == null ? 0L : value.longValue(); } /** * Executes select query, which should return one row and one column (more or less than * one row raises runtime exception, more or less than one column raises * preprocess-time exception). Result is returned as a single double. NULLs are returned as zeroes. */ public final double singleRowQueryReturningDouble(QueryLike query) throws SQLException { Double value = singleRowQueryReturning(Double.class, query); return value == null ? 0.0 : value.doubleValue(); } /** * Same as {@link #singleRowQueryReturning(Class, QueryLike)} but returns * null when no rows found. * * @param cls class with user-defined mapping (see {@link RuntimeMapper}) */ public final T optionalRowQueryReturning(Class cls, QueryLike query) throws SQLException { return singleOrOptionalRowQueryReturningT(cls, query, true); } /** * Executes select query returning single column of T. * * @param cls class with user-defined mapping (see {@link RuntimeMapper}) */ public final List columnOf(Class cls, QueryLike query) throws SQLException { return doExecuteStatement(query, stmt -> { TypeMapper mapper = getMapper(cls); List list = new ArrayList<>(); try (ResultSet rs = stmt.executeQuery()) { if (test != null) { test.checkOneColumn(rs, cls); } else { while (rs.next()) { list.add(mapper.fetch(rs, 1)); } } } return list; }); } ///////////////////////////////// Class statements ///////////////////////////////// private T fetchFromResultSet(Class rowType, ResultSet rs, boolean meta) throws SQLException { RowTypeFactory factory = ctx.global.getRowTypeFactory(rowType, meta); return factory.fetch(ctx.global.mappers, rs); } private T singleOrOptionalRowQuery(QueryLike query, boolean optional, Class rowType) throws SQLException { return doExecuteStatement(query, stmt -> { try (ResultSet rs = stmt.executeQuery()) { boolean meta = false; if (test != null) { test.getRowTypeFields(rowType, rs, meta); return null; } else { if (!checkNext(rs, optional)) return null; T ret = fetchFromResultSet(rowType, rs, meta); tooManyRows(rs); return ret; } } }); } /** * Executes select query, which should return exactly one row (more or less than * one rows raises runtime exception). * Result is returned as an object which class implementation is generated by preprocessor. * * @param query SQL statement * @param rowType row type class or interface generated by preprocessor */ public final T singleRowQuery(QueryLike query, Class rowType) throws SQLException { return singleOrOptionalRowQuery(query, false, rowType); } /** * Same as {@link #singleRowQuery(QueryLike, Class)} but returns * null when no rows found. */ public final T optionalRowQuery(QueryLike query, Class rowType) throws SQLException { return singleOrOptionalRowQuery(query, true, rowType); } /** * Executes select query returning multiple (zero or more) rows. * Result is returned as a list of objects which class implementation is generated by preprocessor. * * @param query SQL statement * @param rowType row type class or interface generated by preprocessor */ public final List multiRowQuery(QueryLike query, Class rowType) throws SQLException { return doExecuteStatement(query, stmt -> { List result = new ArrayList<>(); try (ResultSet rs = stmt.executeQuery()) { boolean meta = false; if (test != null) { test.getRowTypeFields(rowType, rs, meta); } else { RowTypeFactory factory = ctx.global.getRowTypeFactory(rowType, meta); while (rs.next()) { T row = factory.fetch(ctx.global.mappers, rs); result.add(row); } } } return result; }); } /** * Returns query ResultSet metadata as RowType object. */ public final T metaRowQuery(ResultSet rs, Class rowType) throws SQLException { boolean meta = true; if (test != null) { test.getRowTypeFields(rowType, rs, meta); return null; } else { return fetchFromResultSet(rowType, rs, meta); } } /** * Returns query ResultSet metadata as RowType object. */ public final T metaRowQuery(QueryLike query, Class rowType) throws SQLException { return doExecuteStatement(query, stmt -> { try (ResultSet rs = stmt.executeQuery()) { return metaRowQuery(rs, rowType); } }); } ///////////////////////////////// Executing DML ///////////////////////////////// private static int doExecuteUpdate(PreparedStatement stmt, QueryLike query) throws SQLException { if (test != null) { test.checkSql(stmt, query.getSql()); return 0; } else { return stmt.executeUpdate(); } } /** * Executes update/delete/insert SQL statement. This method should always be used * instead of {@link PreparedStatement#executeUpdate()} because the latter can modify * database state at preprocess phase. * * @param query SQL statement * @return number of modified database rows */ public int executeUpdate(QueryLike query) throws SQLException { return doExecuteStatement(null, query, stmt -> doExecuteUpdate(stmt, query)).intValue(); } /** * Executes update or insert SQL statement. This method should always be used * instead of {@link PreparedStatement#executeUpdate()} because the latter can modify * database state at preprocess phase. * * @param query SQL statement * @param autoKeys generated column names * @return update result (number of modified database rows + generated keys) */ public UpdateResult executeUpdate(QueryLike query, String[] autoKeys) throws SQLException { return doExecuteStatement(autoKeys, query, stmt -> { int rows = doExecuteUpdate(stmt, query); Object[] generatedKeys = getGeneratedKeys(stmt); return new UpdateResult(rows, generatedKeys); }); } /** * Executes update or insert SQL statement. This method should always be used * instead of {@link PreparedStatement#executeUpdate()} because the latter can modify * database state at preprocess phase. * * @param query SQL statement * @param autoKey first generated column name * @param otherAutoKeys other generated column names * @return update result (number of modified database rows + generated keys) */ public UpdateResult executeUpdate(QueryLike query, String autoKey, String... otherAutoKeys) throws SQLException { String[] autoKeys = new String[1 + otherAutoKeys.length]; autoKeys[0] = autoKey; System.arraycopy(otherAutoKeys, 0, autoKeys, 1, otherAutoKeys.length); return executeUpdate(query, autoKeys); } private static Object[] getGeneratedKeys(PreparedStatement stmt) throws SQLException { if (test != null) { Number[] ret = new Number[10]; Arrays.fill(ret, 0); return ret; } else { try (ResultSet rs = stmt.getGeneratedKeys()) { ResultSetMetaData rsmd = rs.getMetaData(); int count = rsmd.getColumnCount(); rs.next(); Object[] ret = new Number[count]; for (int i = 0; i < count; i++) { ret[i] = rs.getObject(i + 1); } return ret; } } } ///////////////////////////////// Executing calls ///////////////////////////////// private static String getProcCallSql(String name, Parameter[] in) { StringBuilder buf = new StringBuilder("{ call " + name + "("); int argCount = in.length; for (int i = 0; i < argCount; i++) { if (i > 0) buf.append(", "); buf.append("?"); } buf.append(") }"); return buf.toString(); } /** * Calls stored procedure. * Example: *
     * callStoredProc("trace", in(message, String.class));
     * 
* * @param name Stored procedure name. SQL statement is generated by procedure name * and parameters as { call name(in) }. * @param params input/output parameters array (see {@link #in} and {@link #out}). */ public final void callStoredProc(String name, Parameter... params) throws SQLException { if (test != null) { test.checkStoredProcName(name, params); } else { String sql = getProcCallSql(name, params); List paramList = Arrays.asList(params); doExecuteAnyStatement(sql, paramList, cs -> { cs.execute(); Parameter.getOutParameters(ctx.global.mappers, cs, paramList); return null; }, Connection::prepareCall); } } public final void callStoredProc(QueryLike query) throws SQLException { List params = query.getParameters(); doExecuteAnyStatement(query.getSql(), params, cs -> { if (test != null) { test.checkSql(cs, query.getSql()); } else { cs.execute(); Parameter.getOutParameters(ctx.global.mappers, cs, params); } return null; }, Connection::prepareCall); } ///////////////////////////////// Utility methods ///////////////////////////////// /** * Returns next number in sequence. * * @param sequence sequence name */ public final long getNextId(String sequence) throws SQLException { String sql = ctx.global.db.getNextIdSql(sequence); if (test != null) { try (PreparedStatement stmt = getConnection().prepareStatement(sql)) { test.checkSql(stmt, sql); } return 0; } else { return singleRowQueryReturningLong(query(sql)); } } private TypeMapper getMapper(Class cls) { return ctx.global.mappers.getMapper(cls); } public interface RowFetcher { T fetchNext() throws SQLException; } /** * Fetches rows from result set. * * @param rowType row type class */ public final RowFetcher getRowFetcher(Class rowType, ResultSet rs) { boolean meta = false; if (test != null) { return () -> { test.getRowTypeFields(rowType, rs, meta); return null; }; } else { RowTypeFactory factory = ctx.global.getRowTypeFactory(rowType, meta); return () -> { if (rs.next()) { return factory.fetch(ctx.global.mappers, rs); } else { return null; } }; } } public final GContext getContext() { return ctx; } }