From a4095b4ae5904c7dcfe1d20490cf1cc6ddaea93b Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 12 Jun 2014 21:11:14 +1200 Subject: [PATCH] Fix for #142 - ENH: Add support for using RawSql with user supplied ResultSet --- src/main/java/com/avaje/ebean/RawSql.java | 54 ++++++++++++++- .../java/com/avaje/ebean/RawSqlBuilder.java | 29 +++++++- .../ebeaninternal/server/query/CQuery.java | 10 +++ .../server/query/CQueryBuilderRawSql.java | 7 +- .../rawsql/TestRawSqlPositionedParams.java | 4 +- .../tests/rawsql/TestRawSqlWithResultSet.java | 67 +++++++++++++++++++ 6 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 src/test/java/com/avaje/tests/rawsql/TestRawSqlWithResultSet.java diff --git a/src/main/java/com/avaje/ebean/RawSql.java b/src/main/java/com/avaje/ebean/RawSql.java index ffc703aec..4dc5211fb 100644 --- a/src/main/java/com/avaje/ebean/RawSql.java +++ b/src/main/java/com/avaje/ebean/RawSql.java @@ -1,6 +1,7 @@ package com.avaje.ebean; import java.io.Serializable; +import java.sql.ResultSet; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -135,18 +136,33 @@ import com.avaje.ebean.util.CamelCaseHelper; * Note that lazy loading also works with object graphs built with RawSql. *

* - * @author rbygrave - * */ public final class RawSql implements Serializable { private static final long serialVersionUID = 1L; + private final ResultSet resultSet; + private final Sql sql; private final ColumnMapping columnMapping; - protected RawSql(Sql sql, ColumnMapping columnMapping) { + /** + * Construct with a ResultSet and properties that the columns map to. + *

+ * The properties listed in the propertyNames must be in the same order as the columns in the + * resultSet. + *

+ * When a query executes this RawSql object then it will close the resultSet. + */ + public RawSql(ResultSet resultSet, String... propertyNames) { + this.resultSet = resultSet; + this.sql = null; + this.columnMapping = new ColumnMapping(propertyNames); + } + + protected RawSql(ResultSet resultSet, Sql sql, ColumnMapping columnMapping) { + this.resultSet = resultSet; this.sql = sql; this.columnMapping = columnMapping; } @@ -158,6 +174,14 @@ public final class RawSql implements Serializable { return sql; } + + /** + * Return the resultSet if this is a ResultSet based RawSql. + */ + public ResultSet getResultSet() { + return resultSet; + } + /** * Return the column mapping for the SQL columns to bean properties. */ @@ -169,6 +193,9 @@ public final class RawSql implements Serializable { * Return the hash for this query. */ public int queryHash() { + if (resultSet != null) { + return 31 * columnMapping.queryHash(); + } return 31 * sql.queryHash() + columnMapping.queryHash(); } @@ -329,6 +356,7 @@ public final class RawSql implements Serializable { private final LinkedHashMap dbColumnMap; private final Map propertyMap; + private final Map propertyColumnMap; private final boolean parsed; @@ -364,6 +392,26 @@ public final class RawSql implements Serializable { this.propertyColumnMap = null; this.dbColumnMap = new LinkedHashMap(); } + + /** + * Construct for ResultSet use. + */ + protected ColumnMapping(String... propertyNames) { + this.immutable = false; + this.parsed = false; + this.propertyMap = null; + //this.propertyColumnMap = null; + this.dbColumnMap = new LinkedHashMap(); + + int hc = 31; + int pos = 0; + for (String prop : propertyNames) { + hc = 31 * hc + prop.hashCode(); + dbColumnMap.put(prop, new Column(pos++, prop, null, prop)); + } + propertyColumnMap = dbColumnMap; + this.queryHashCode = hc; + } /** * Construct an immutable ColumnMapping based on collected information. diff --git a/src/main/java/com/avaje/ebean/RawSqlBuilder.java b/src/main/java/com/avaje/ebean/RawSqlBuilder.java index 7749829b2..b491083a7 100644 --- a/src/main/java/com/avaje/ebean/RawSqlBuilder.java +++ b/src/main/java/com/avaje/ebean/RawSqlBuilder.java @@ -1,5 +1,7 @@ package com.avaje.ebean; +import java.sql.ResultSet; + import com.avaje.ebean.RawSql.ColumnMapping; import com.avaje.ebean.RawSql.Sql; @@ -10,8 +12,6 @@ import com.avaje.ebean.RawSql.Sql; * named query. *

* - * @author rbygrave - * * @see RawSql */ public class RawSqlBuilder { @@ -21,10 +21,23 @@ public class RawSqlBuilder { */ public static final String IGNORE_COLUMN = "$$_IGNORE_COLUMN_$$"; + private final ResultSet resultSet; + private final Sql sql; private final ColumnMapping columnMapping; + /** + * Create and return a RawSql object based on the resultSet and list of properties the columns in + * the resultSet map to. + *

+ * The properties listed in the propertyNames must be in the same order as the columns in the + * resultSet. + */ + public static RawSql resultSet(ResultSet resultSet, String... propertyNames) { + return new RawSql(resultSet, propertyNames); + } + /** * Return an unparsed RawSqlBuilder. Unlike a parsed one this query can not be * modified - so no additional WHERE or HAVING expressions can be added to @@ -58,9 +71,17 @@ public class RawSqlBuilder { return new RawSqlBuilder(sql2, mapping); } + + private RawSqlBuilder(ResultSet resultSet, ColumnMapping columnMapping) { + this.resultSet = resultSet; + this.columnMapping = columnMapping; + this.sql = null; + } + private RawSqlBuilder(Sql sql, ColumnMapping columnMapping) { this.sql = sql; this.columnMapping = columnMapping; + this.resultSet = null; } /** @@ -92,7 +113,7 @@ public class RawSqlBuilder { * has been defined. */ public RawSql create() { - return new RawSql(sql, columnMapping.createImmutableCopy()); + return new RawSql(resultSet, sql, columnMapping.createImmutableCopy()); } /** @@ -101,4 +122,6 @@ public class RawSqlBuilder { protected Sql getSql() { return sql; } + + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java index 218940be3..68444a167 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQuery.java @@ -357,6 +357,16 @@ public class CQuery implements DbReadContext, CancelableQuery { SpiTransaction t = request.getTransaction(); Connection conn = t.getInternalConnection(); + if (query.isRawSql()) { + ResultSet suppliedResultSet = query.getRawSql().getResultSet(); + if (suppliedResultSet != null) { + // this is a user supplied ResultSet so use that + dataReader = queryPlan.createDataReader(suppliedResultSet); + bindLog = ""; + return true; + } + } + if (forwardOnlyHint) { // Use forward only hints for large resultset processing (Issue 56, MySql specific) pstmt = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilderRawSql.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilderRawSql.java index f9b42bd4d..9f5f31173 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilderRawSql.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilderRawSql.java @@ -25,7 +25,12 @@ public class CQueryBuilderRawSql implements Constants { * Build the full SQL Select statement for the request. */ public SqlLimitResponse buildSql(OrmQueryRequest request, CQueryPredicates predicates, RawSql.Sql rsql) { - + + if (rsql == null) { + // this is a ResultSet based RawSql query - just use some placeholder for the SQL + return new SqlLimitResponse("--ResultSetBasedRawSql", false); + } + if (!rsql.isParsed()){ String sql = rsql.getUnparsedSql(); BindParams bindParams = request.getQuery().getBindParams(); diff --git a/src/test/java/com/avaje/tests/rawsql/TestRawSqlPositionedParams.java b/src/test/java/com/avaje/tests/rawsql/TestRawSqlPositionedParams.java index cf6fca526..bb65bc0d9 100644 --- a/src/test/java/com/avaje/tests/rawsql/TestRawSqlPositionedParams.java +++ b/src/test/java/com/avaje/tests/rawsql/TestRawSqlPositionedParams.java @@ -35,13 +35,15 @@ public class TestRawSqlPositionedParams extends BaseTestCase { Assert.assertNotNull(list); } + @Test public void test_unparsed() { ResetBasicData.reset(); RawSql rawSql = RawSqlBuilder .unparsed("select r.id, r.name from o_customer r where r.id >= ? and r.name like ?") - .columnMapping("r.id", "id").columnMapping("r.name", "name").create(); + .columnMapping("r.id", "id") + .columnMapping("r.name", "name").create(); Query query = Ebean.find(Customer.class); query.setRawSql(rawSql); diff --git a/src/test/java/com/avaje/tests/rawsql/TestRawSqlWithResultSet.java b/src/test/java/com/avaje/tests/rawsql/TestRawSqlWithResultSet.java new file mode 100644 index 000000000..c84e906ef --- /dev/null +++ b/src/test/java/com/avaje/tests/rawsql/TestRawSqlWithResultSet.java @@ -0,0 +1,67 @@ +package com.avaje.tests.rawsql; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import org.junit.Test; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.FetchConfig; +import com.avaje.ebean.RawSql; +import com.avaje.ebean.Transaction; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.ResetBasicData; + +public class TestRawSqlWithResultSet extends BaseTestCase { + + @Test + public void test() throws SQLException { + + ResetBasicData.reset(); + + // Transaction supplies our jdbc Connection + Transaction txn = Ebean.beginTransaction(); + + PreparedStatement pstmt = null; + + try { + pstmt = txn.getConnection().prepareStatement("select id, name, billing_address_id from o_customer"); + + // ResultSet will be closed by Ebean + ResultSet resultSet = pstmt.executeQuery(); + + RawSql rawSql = new RawSql(resultSet, "id", "name", "billingAddress.id"); + + List list = Ebean.find(Customer.class) + .setRawSql(rawSql) + // also test a secondary query join + .fetch("billingAddress", new FetchConfig().query()) + .findList(); + + for (Customer customer : list) { + System.out.println("id:"+customer.getId()+" name:"+customer.getName()+" billingAddress:"+customer.getBillingAddress()); + } + + } finally { + close(pstmt); + txn.end(); + } + + } + + private static void close(Statement stmt) { + + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + +}