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

There is a newer version: 3.1
Show newest version
package sqlg3.runtime;

import sqlg3.core.IDBCommon;
import sqlg3.core.ISimpleTransaction;
import sqlg3.core.SQLGException;
import sqlg3.runtime.queries.QueryParser;

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. */ @SuppressWarnings("UnusedDeclaration") public class GBase implements ISimpleTransaction { /** * Use it for {@code autoKeys} parameter of {@link #prepareStatementKey} to * retrieve all generated columns. * * @see sqlg3.annotations.PrepareKey#value() */ 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.transaction.getConnection(); } /** * 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; } } private void setSql(String sql, Parameter[] params) { ctx.call.setSql(sql, params); } ///////////////////////////////// Query piece creation ///////////////////////////////// /** * Used for preprocessor-generated query pieces (see {@link sqlg3.annotations.Query}) */ public static QueryPiece createQueryPiece(QueryPiece query) { return query; } /** * Creates SQL query piece containing query text and its parameters. * Example: *

     * QueryPiece piece = createQueryPiece(" 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. *

* After piece is created, it can be used to build large query (see {@link QueryBuilder}) or execute query * (see {@link #prepareStatement(QueryPiece)}) * * @param sql query text, possibly containing references to parameters in the form of {@code ?} * @param params query parameters, see {@link #in} */ public static QueryPiece createQueryPiece(CharSequence sql, Parameter... params) { return new QueryPiece(sql, params); } ///////////////////////////////// Statement preparation ///////////////////////////////// private interface StatementFactory { T create(Connection connection, String sql) throws SQLException; } private T doPrepareAnyStatement(String sql, Parameter[] params, StatementFactory factory) throws SQLException { Connection connection = getConnection(); T stmt = factory.create(connection, sql); if (test != null) { test.statementCreated(stmt, sql); } ctx.call.statementCreated(stmt, params); Parameter.setParameters(ctx.global.mappers, stmt, params); return stmt; } private PreparedStatement doPrepareStatement(String[] autoKeys, String unparsedSql, Parameter[] params) throws SQLException { setSql(unparsedSql, params); String parsedSql = QueryParser.parseQuery(unparsedSql); if (autoKeys == null) { return doPrepareAnyStatement(parsedSql, params, Connection::prepareStatement); } else { return doPrepareAnyStatement(parsedSql, params, (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); } }); } } /** * Creates prepared statement from SQL query and its parameters. * Later this statement can be executed by {@link #executeUpdate} or by one of many selection methods. * Example: *

     * PreparedStatement stmt = prepareStatement("SELECT value FROM table WHERE id = ?", in(id, Long.class));
     * int result = singleRowQueryReturningInt(stmt);
     * 
* It is more convenient to use {@link sqlg3.annotations.Prepare} annotation to generate such queries 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 final PreparedStatement prepareStatement(String sql, Parameter... params) throws SQLException { return doPrepareStatement(null, sql, params); } /** * Creates prepared statement with auto-generated keys from SQL query and its parameters. * Later this statement can be executed by {@link #executeUpdate} or by one of many selection methods. * Example: *
     * PreparedStatement stmt = prepareStatementKey(new String[] {"id"}, "INSERT INTO table (name) values ('test')");
     * executeUpdate(stmt);
     * int generatedId = getGeneratedKeys()[0].intValue();
     * 
* It is more convenient to use {@link sqlg3.annotations.PrepareKey} annotation to generate such queries than * to use this method manually. * * @param autoKeys array of column names which are auto-generated. * Pass {@link #ALL_KEYS} to retrieve all generated columns (does not work on some DBs). * @param sql query text, possibly containing references to parameters in the form of {@code ?} * @param params query parameters, see {@link #in} */ public final PreparedStatement prepareStatementKey(String[] autoKeys, String sql, Parameter... params) throws SQLException { return doPrepareStatement(autoKeys, sql, params); } /** * Creates prepared statement from a piece containing SQL query and its parameters. * Later this statement can be executed by {@link #executeUpdate} or by one of many selection methods. * Example: *
     * PreparedStatement stmt = prepareStatement("SELECT value FROM table WHERE id = ?", in(id, Long.class));
     * int result = singleRowQueryReturningInt(stmt);
     * 
* It is more convenient to use {@link sqlg3.annotations.Prepare} annotation to generate such queries than * to use this method manually. * * @param query query piece containing SQL query and its parameters */ public final PreparedStatement prepareStatement(QueryPiece query) throws SQLException { return doPrepareStatement(null, query.sql, query.data); } /** * Creates prepared statement with auto-generated keys from a piece containing SQL query and its parameters. * Later this statement can be executed by {@link #executeUpdate} or by one of many selection methods. * Example: *
     * PreparedStatement stmt = prepareStatementKey(new String[] {"id"}, "INSERT INTO table (name) values ('test')");
     * executeUpdate(stmt);
     * int generatedId = getGeneratedKeys()[0].intValue();
     * 
* It is more convenient to use {@link sqlg3.annotations.PrepareKey} annotation to generate such queries than * to use this method manually. * * @param autoKeys array of column names which are auto-generated. * Pass {@link #ALL_KEYS} to retrieve all generated columns (does not work on some DBs). * @param query query piece containing SQL query and its parameters */ public final PreparedStatement prepareStatementKey(String[] autoKeys, QueryPiece query) throws SQLException { return doPrepareStatement(autoKeys, query.sql, query.data); } /** * Creates CallableStatement for stored procedure (or PL/SQL block) call. * * @param sql stored procedure to call. * Syntax is the same as for {@link Connection#prepareCall(String)}: *
    *
  • { call proc_name(params) } or { ? = func_name(params) } *
  • begin ... end; *
*/ public final CallableStatement prepareCall(String sql, Parameter... params) throws SQLException { setSql(sql, params); return doPrepareAnyStatement(sql, params, Connection::prepareCall); } public final CallableStatement prepareCall(QueryPiece query) throws SQLException { return prepareCall(query.sql, query.data); } /** * 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); } /** * 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, 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 { Parameter.in(value, cls).set(ctx.global.mappers, st, index); } ///////////////////////////////// Sinlge and optional row 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, PreparedStatement stmt, boolean optional) throws SQLException { 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). * Closes statement after execution. * * @param cls class with user-defined mapping (see {@link RuntimeMapper}) */ public final T singleRowQueryReturning(Class cls, PreparedStatement stmt) throws SQLException { return singleOrOptionalRowQueryReturningT(cls, stmt, 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. * Closes statement after execution. */ public final int singleRowQueryReturningInt(PreparedStatement stmt) throws SQLException { Integer value = singleRowQueryReturning(Integer.class, stmt); 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. * Closes statement after execution. */ public final long singleRowQueryReturningLong(PreparedStatement stmt) throws SQLException { Long value = singleRowQueryReturning(Long.class, stmt); 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. * Closes statement after execution. */ public final double singleRowQueryReturningDouble(PreparedStatement stmt) throws SQLException { Double value = singleRowQueryReturning(Double.class, stmt); return value == null ? 0.0 : value.doubleValue(); } /** * Same as {@link #singleRowQueryReturning(Class, PreparedStatement)} but returns * null when no rows found. * * @param cls class with user-defined mapping (see {@link RuntimeMapper}) */ public final T optionalRowQueryReturning(Class cls, PreparedStatement stmt) throws SQLException { return singleOrOptionalRowQueryReturningT(cls, stmt, true); } ///////////////////////////////// Column statements ///////////////////////////////// /** * Executes select query returning single column of T. * Closes statement after execution. * * @param cls class with user-defined mapping (see {@link RuntimeMapper}) */ public final List columnOf(Class cls, PreparedStatement stmt) throws SQLException { 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; } /** * Executes select query returning single column of int. * Closes statement after execution. */ public final int[] columnOfInt(PreparedStatement stmt) throws SQLException { List list = columnOf(Integer.class, stmt); int[] ret = new int[list.size()]; for (int i = 0; i < list.size(); i++) { Integer value = list.get(i); ret[i] = value == null ? 0 : value.intValue(); } return ret; } /** * Executes select query returning single column of long. * Closes statement after execution. */ public final long[] columnOfLong(PreparedStatement stmt) throws SQLException { List list = columnOf(Long.class, stmt); long[] ret = new long[list.size()]; for (int i = 0; i < list.size(); i++) { Long value = list.get(i); ret[i] = value == null ? 0L : value.longValue(); } return ret; } /** * Executes select query returning single column of double. * Closes statement after execution. */ public final double[] columnOfDouble(PreparedStatement stmt) throws SQLException { List list = columnOf(Double.class, stmt); double[] ret = new double[list.size()]; for (int i = 0; i < list.size(); i++) { Double value = list.get(i); ret[i] = value == null ? 0.0 : value.doubleValue(); } return ret; } ///////////////////////////////// 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(PreparedStatement stmt, boolean optional, Class rowType) throws SQLException { 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. * Closes statement after execution. * * @param stmt SQL statement * @param rowType row type class or interface generated by preprocessor */ public final T singleRowQuery(PreparedStatement stmt, Class rowType) throws SQLException { return singleOrOptionalRowQuery(stmt, false, rowType); } /** * Same as {@link #singleRowQuery(PreparedStatement, Class)} but returns * null when no rows found. */ public final T optionalRowQuery(PreparedStatement stmt, Class rowType) throws SQLException { return singleOrOptionalRowQuery(stmt, 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. * Closes statement after execution. * * @param stmt SQL statement * @param rowType row type class or interface generated by preprocessor */ public final List multiRowQuery(PreparedStatement stmt, Class rowType) throws SQLException { 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(PreparedStatement stmt, Class rowType) throws SQLException { try (ResultSet rs = stmt.executeQuery()) { return metaRowQuery(rs, rowType); } } ///////////////////////////////// Executing DML ///////////////////////////////// /** * 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. *

* This method does not close statement, so it can be called multiple times. * You don't have to close statement manually if it was created with {@link #prepareStatement(String, Parameter...)} or the like, * because all statements created with {@link GBase} methods are closed automatically after business method exit. * * @param stmt SQL statement * @return number of modified database rows */ public static int executeUpdate(PreparedStatement stmt) throws SQLException { if (test != null) { test.checkSql(stmt); return 0; } else { return stmt.executeUpdate(); } } /** * Returns array of auto-generated keys for insert/update statement. List of auto-generated * columns is provided by {@link #prepareStatementKey} autoKeys * parameter or by {@link sqlg3.annotations.PrepareKey} annotation value. Number of elements in array is equal to the number * of auto-generated columns. */ public static Number[] 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(); Number[] ret = new Number[count]; for (int i = 0; i < count; i++) { ret[i] = (Number) rs.getObject(i + 1); } return ret; } } } public static int getGeneratedInt(PreparedStatement ps) throws SQLException { return getGeneratedKeys(ps)[0].intValue(); } public static long getGeneratedLong(PreparedStatement ps) throws SQLException { return getGeneratedKeys(ps)[0].longValue(); } ///////////////////////////////// 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(); } /** * Executes stored procedure or PL/SQL block. */ public final void executeCall(CallableStatement cs) throws SQLException { if (test != null) { test.checkSql(cs); } else { cs.execute(); Parameter.getOutParameters(ctx.global.mappers, cs, ctx.call.getParameters(cs)); } } /** * 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); } String sql = getProcCallSql(name, params); CallableStatement cs = prepareCall(sql, params); executeCall(cs); } ///////////////////////////////// Utility methods ///////////////////////////////// /** * Returns next number in sequence. * * @param sequence sequence name */ public final long getNextId(String sequence) throws SQLException { if (test != null) { test.checkSequenceExists(sequence); return 0; } setSql(sequence + ".NEXTVAL", null); return ctx.global.db.getNextId(getConnection(), sequence); } /** * Returns data access interface generated by preprocessor. * * @param iface interface class * @return data access interface implementation object working * in current transaction */ public final T getInterface(Class iface) { if (test != null) { return test.getNullInterface(iface); } else { return ctx.transaction.getInterface(iface, false, false); } } 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 GlobalContext getGlobal() { return ctx.global; } public final Object getUserObject() { return ctx.session.getUserObject(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy