SqlQuery
With SqlQuery we can execute sql queries without any entity beans or dto beans. Instead we can:
- Read JDBC
ResultSet
directly - Read as
SqlRow
objects - Use
mapToScalar()
and read simple scalar values like Long, BigDecimal, OffsetDateTime etc - use
mapTo()
andRowMapper
to map and read objects
Example findOne with SqlRow
String sql = "select id, name, when_active from customer where id = :id"
SqlRow row = DB.sqlQuery(sql)
.setParameter("id", 42)
.findOne();
String name = row.getString("name");
Timestamp active = row.getTimestamp("when_active");
Example findEachRow with ResultSet
String sql = "select id, name, status from customer order by name desc";
DB.sqlQuery(sql)
.findEachRow((resultSet, rowNum) -> {
long id = resultSet.getLong(1);
String name = resultSet.getString(2);
...
});
Example mapToScalar()
String sql = "select mysequence.nextval";
Long nextVal = DB.sqlQuery(sql)
.mapToScalar(Long.class)
.findOne();
Binding positioned parameters using ?
Positioned parameters use ?
as the placeholder.
String sql = "select id, name, when_active from customer where status = ? and when_created > ?";
List<SqlRow> rows = DB.sqlQuery(sql)
.setParameter(1, "NEW")
.setParameter(2, lastWeek)
.findList();
We can omit the index positions like the next example.
String sql = "select id, name, when_active from customer where status = ? and when_created > ?";
List<SqlRow> rows = DB.sqlQuery(sql)
.setParameter("NEW")
.setParameter(lastWeek)
.findList();
We can alternatively set multiple positioned parameters using setParameters()
.
String sql = "select id, name, when_active from customer where status = ? and when_created > ?";
List<SqlRow> rows = DB.sqlQuery(sql)
.setParameters("NEW", lastWeek)
.findList();
Binding named parameters using :name
Named parameters use the form :foo
as the placeholder. Named parameters have the
advantages that we can use them to bind collections into an IN
expression and
that the same parameter can appear multiple times in the SQL.
Binding using named parameters
String sql =
"select id, name, when_active " +
"from customer " +
"where status = :status and when_created > :created";
List<SqlRow> rows = DB.sqlQuery(sql)
.setParameter("status", "NEW")
.setParameter("created", lastWeek)
.findList();
Binding collections
To bind collections of values into an IN clause we need to used named parameters or indexed positioned parameters.
Named parameter example (:names)
String sql = "select c.id, c.name from customer c where c.name in (:names)";
List<SqlRow> rows = DB.sqlQuery(sql)
.setParameter("names", asList("Rob", "Fiona", "Jack"))
.findList();
Index parameter example - ?2
String sql = "select c.id, c.name from customer c " +
"where c.status = ? and c.name in (?2) and c.when_created > ?";
List<SqlRow> rows = DB.sqlQuery(sql)
.setParameter("NEW")
.setParameter(asList("Rob", "Fiona", "Jack"))
.setParameter(lastWeek)
.findList();
Postgres ANY
When using Postgres we can use Postgres ANY
in SqlQuery with positioned parameters.
The benefit of using Postgres ANY
is that we end up using a single SQL query regardless of the number
of parameters - a single JDBC PreparedStatement and the database has a single SQL query to parse. This results in
the database doing less hard parsing and better use of PreparedStatement cache, less query plans to cache, less memory consumed.
Example
When using index positioned parameters and binding a collection that will bind as a JDBC ARRAY.
List<Integer> ids = List.of(1, 2);
List<SqlRow> list = DB.sqlQuery("select id, name from o_customer where id = any(?)")
.setParameter(1, ids) // bind as JDBC ARRAY
.findList();
When using Postgres we would often prefer to use that over using an IN
clause
like the example below. The reason we prefer ANY
is that it does not matter how
many elements we have in the ARRAY we end up with a single SQL statement. This gives us a single
query plan, single PrepareStatement and on the database end a single query statement to parse regardless
of the number of id values we are binding.
In comparison to using an IN
clause, if we have 3 ids that is a different SQL query to one
that has 4 ids for example - we end up with more distinct PreparedStatements, potentially a lot more depending
on the number of id values we have.
List<SqlRow> list = DB.sqlQuery("select id, name from o_customer where id in (:idList)")
.setParameter("idList", ids) // bind parameter expansion
.findList();
Using the IN
clause will result in a different SQL query when we have different numbers of
id values. With 4 id values supplied then in (:idList)
is turned into SQL in (?, ?, ?, ?)
.
firstRow / maxRows
We can specify firstRow and maxRows on a SqlQuery and Ebean will modify the SQL based on the database platform to add the appropriate row limit clause.
String sql = "select id, name, when_active from customer where status = ?";
List<SqlRow> rows = DB.sqlQuery(sql)
.setParameter(1, "NEW")
.setFirstRow(10)
.setMaxRows(10)
.findList();
mapToScalar() - single column scalar result
Use mapToScalar()
when the query has a single column in the select clause.
This specifies the type to use to read the result.
String sql = "select mysequence.nextval";
Long nextVal = DB.sqlQuery(sql)
.mapToScalar(Long.class)
.findOne();
String sql = "select max(unit_price) from order_lines where order_qty > ?";
BigDecimal maxPrice = DB.sqlQuery(sql)
.setParameter(42)
.mapToScalar(BigDecimal.class)
.findOne();
String sql = "select max(order_date) from orders where customer_id = ?";
OffsetDateTime maxDate = DB.sqlQuery(sql)
.setParameter(42)
.mapToScalar(OffsetDateTime.class)
.findOne();
All scalar types that Ebean supports can be used - refer to mapping types.
mapTo() - RowMapper
We can implement a RowMapper to convert a ResultSet into a dto bean.
Note that this similar to DtoQuery except that the mapping is here is explicit and with DtoQuery the mapping is effectively automatic (no effort) based on naming convention mapping. In that sense the expectation is that people will use DtoQuery first and only use this explicit RowMapping when necessary.
static class CustomerDtoMapper implements RowMapper<CustomerDto> {
@Override
public CustomerDto map(ResultSet resultSet, int rowNum) throws SQLException {
long id = resultSet.getLong(1);
String name = resultSet.getString(2);
String status = resultSet.getString(3);
return new CustomerDto(id, name, status);
}
}
...
static final CustomerDtoMapper CUSTOMER_MAPPER = new CustomerDtoMapper()
Then we can use it to return DTO beans.
String sql = "select id, name, status from customer where name = ?";
List<CustomerDto> rob = DB.sqlQuery(sql)
.setParameter("Rob")
.mapTo(CUSTOMER_MAPPER)
.findList();