001package com.avaje.ebean; 002 003import java.io.Serializable; 004import java.sql.ResultSet; 005import java.util.*; 006 007import com.avaje.ebean.util.CamelCaseHelper; 008 009/** 010 * Used to build object graphs based on a raw SQL statement (rather than 011 * generated by Ebean). 012 * <p> 013 * If you don't want to build object graphs you can use {@link SqlQuery} instead 014 * which returns {@link SqlRow} objects rather than entity beans. 015 * </p> 016 * <p> 017 * <b>Unparsed RawSql:</b> 018 * </p> 019 * <p> 020 * When RawSql is created via {@link RawSqlBuilder#unparsed(String)} then Ebean can not 021 * modify the SQL at all. It can't add any extra expressions into the SQL. 022 * </p> 023 * <p> 024 * <b>Parsed RawSql:</b> 025 * </p> 026 * <p> 027 * When RawSql is created via {@link RawSqlBuilder#parse(String)} then Ebean will parse the 028 * SQL and find places in the SQL where it can add extra where expressions, add 029 * extra having expressions or replace the order by clause. If you want to 030 * explicitly tell Ebean where these insertion points are you can place special 031 * strings into your SQL ({@code ${where}} or {@code ${andWhere}} and {@code ${having}} or 032 * {@code ${andHaving})}. 033 * </p> 034 * <p> 035 * If the SQL already includes a WHERE clause put in {@code ${andWhere}} in the location 036 * you want Ebean to add any extra where expressions. If the SQL doesn't have a 037 * WHERE clause put {@code ${where}} in instead. Similarly you can put in {@code ${having}} or 038 * {@code ${andHaving}} where you want Ebean put add extra having expressions. 039 * </p> 040 * <p> 041 * <b>Aggregates:</b> 042 * </p> 043 * <p> 044 * Often RawSql will be used with Aggregate functions (sum, avg, max etc). The 045 * follow example shows an example based on Total Order Amount - 046 * sum(d.order_qty*d.unit_price). 047 * </p> 048 * <p> 049 * We can use a OrderAggregate bean that has a @Sql to indicate it is based 050 * on RawSql and not based on a real DB Table or DB View. It has some properties 051 * to hold the values for the aggregate functions (sum etc) and a @OneToOne 052 * to Order. 053 * </p> 054 * 055 * <h3>Example OrderAggregate</h3> 056 * 057 * <pre>{@code 058 * ... 059 * // @Sql indicates to that this bean 060 * // is based on RawSql rather than a table 061 * 062 * @Entity 063 * @Sql 064 * public class OrderAggregate { 065 * 066 * @OneToOne 067 * Order order; 068 * 069 * Double totalAmount; 070 * 071 * Double totalItems; 072 * 073 * // getters and setters 074 * ... 075 * 076 * }</pre> 077 * 078 * <h3>Example 1:</h3> 079 * 080 * <pre>{@code 081 * 082 * String sql = " select order_id, o.status, c.id, c.name, sum(d.order_qty*d.unit_price) as totalAmount" 083 * + " from o_order o" 084 * + " join o_customer c on c.id = o.kcustomer_id " 085 * + " join o_order_detail d on d.order_id = o.id " + " group by order_id, o.status "; 086 * 087 * RawSql rawSql = RawSqlBuilder.parse(sql) 088 * // map the sql result columns to bean properties 089 * .columnMapping("order_id", "order.id") 090 * .columnMapping("o.status", "order.status") 091 * .columnMapping("c.id", "order.customer.id") 092 * .columnMapping("c.name", "order.customer.name") 093 * // we don't need to map this one due to the sql column alias 094 * // .columnMapping("sum(d.order_qty*d.unit_price)", "totalAmount") 095 * .create(); 096 * 097 * List<OrderAggregate> list = Ebean.find(OrderAggregate.class) 098 * .setRawSql(rawSql) 099 * .where().gt("order.id", 0) 100 * .having().gt("totalAmount", 20) 101 * .findList(); 102 * 103 * 104 * }</pre> 105 * 106 * <h3>Example 2:</h3> 107 * 108 * <p> 109 * The following example uses a FetchConfig().query() so that after the initial 110 * RawSql query is executed Ebean executes a secondary query to fetch the 111 * associated order status, orderDate along with the customer name. 112 * </p> 113 * 114 * <pre>{@code 115 * 116 * String sql = " select order_id, 'ignoreMe', sum(d.order_qty*d.unit_price) as totalAmount " 117 * + " from o_order_detail d" 118 * + " group by order_id "; 119 * 120 * RawSql rawSql = RawSqlBuilder.parse(sql) 121 * .columnMapping("order_id", "order.id") 122 * .columnMappingIgnore("'ignoreMe'") 123 * .create(); 124 * 125 * List<OrderAggregate> orders = Ebean.find(OrderAggregate.class) 126 * .setRawSql(rawSql) 127 * .fetch("order", "status,orderDate", new FetchConfig().query()) 128 * .fetch("order.customer", "name") 129 * .where().gt("order.id", 0) 130 * .having().gt("totalAmount", 20) 131 * .order().desc("totalAmount") 132 * .setMaxRows(10) 133 * .findList(); 134 * 135 * }</pre> 136 * 137 * 138 * <h3>Example 3: tableAliasMapping</h3> 139 * <p> 140 * Instead of mapping each column you can map each table alias to a path using tableAliasMapping(). 141 * </p> 142 * <pre>{@code 143 * 144 * String rs = "select o.id, o.status, c.id, c.name, "+ 145 * " d.id, d.order_qty, p.id, p.name " + 146 * "from o_order o join o_customer c on c.id = o.kcustomer_id " + 147 * "join o_order_detail d on d.order_id = o.id " + 148 * "join o_product p on p.id = d.product_id " + 149 * "where o.id <= :maxOrderId and p.id = :productId "+ 150 * "order by o.id, d.id asc"; 151 * 152 * RawSql rawSql = RawSqlBuilder.parse(rs) 153 * .tableAliasMapping("c", "customer") 154 * .tableAliasMapping("d", "details") 155 * .tableAliasMapping("p", "details.product") 156 * .create(); 157 * 158 * List<Order> ordersFromRaw = Ebean.find(Order.class) 159 * .setRawSql(rawSql) 160 * .setParameter("maxOrderId", 2) 161 * .setParameter("productId", 1) 162 * .findList(); 163 * 164 * }</pre> 165 * 166 * 167 * <p> 168 * Note that lazy loading also works with object graphs built with RawSql. 169 * </p> 170 * 171 */ 172public final class RawSql implements Serializable { 173 174 private static final long serialVersionUID = 1L; 175 176 private final ResultSet resultSet; 177 178 private final Sql sql; 179 180 private final ColumnMapping columnMapping; 181 182 /** 183 * Construct with a ResultSet and properties that the columns map to. 184 * <p> 185 * The properties listed in the propertyNames must be in the same order as the columns in the 186 * resultSet. 187 * <p> 188 * When a query executes this RawSql object then it will close the resultSet. 189 */ 190 public RawSql(ResultSet resultSet, String... propertyNames) { 191 this.resultSet = resultSet; 192 this.sql = null; 193 this.columnMapping = new ColumnMapping(propertyNames); 194 } 195 196 protected RawSql(ResultSet resultSet, Sql sql, ColumnMapping columnMapping) { 197 this.resultSet = resultSet; 198 this.sql = sql; 199 this.columnMapping = columnMapping; 200 } 201 202 /** 203 * Return the Sql either unparsed or in parsed (broken up) form. 204 */ 205 public Sql getSql() { 206 return sql; 207 } 208 209 210 /** 211 * Return the resultSet if this is a ResultSet based RawSql. 212 */ 213 public ResultSet getResultSet() { 214 return resultSet; 215 } 216 217 /** 218 * Return the column mapping for the SQL columns to bean properties. 219 */ 220 public ColumnMapping getColumnMapping() { 221 return columnMapping; 222 } 223 224 /** 225 * Return the hash for this query. 226 */ 227 public int queryHash() { 228 if (resultSet != null) { 229 return 31 * columnMapping.queryHash(); 230 } 231 return 31 * sql.queryHash() + columnMapping.queryHash(); 232 } 233 234 /** 235 * Represents the sql part of the query. For parsed RawSql the sql is broken 236 * up so that Ebean can insert extra WHERE and HAVING expressions into the 237 * SQL. 238 */ 239 public static final class Sql implements Serializable { 240 241 private static final long serialVersionUID = 1L; 242 243 private final boolean parsed; 244 245 private final String unparsedSql; 246 247 private final String preFrom; 248 249 private final String preWhere; 250 251 private final boolean andWhereExpr; 252 253 private final String preHaving; 254 255 private final boolean andHavingExpr; 256 257 private final String orderByPrefix; 258 259 private final String orderBy; 260 261 private final boolean distinct; 262 263 private final int queryHashCode; 264 265 /** 266 * Construct for unparsed SQL. 267 */ 268 protected Sql(String unparsedSql) { 269 this.queryHashCode = unparsedSql.hashCode(); 270 this.parsed = false; 271 this.unparsedSql = unparsedSql; 272 this.preFrom = null; 273 this.preHaving = null; 274 this.preWhere = null; 275 this.andHavingExpr = false; 276 this.andWhereExpr = false; 277 this.orderByPrefix = null; 278 this.orderBy = null; 279 this.distinct = false; 280 } 281 282 /** 283 * Construct for parsed SQL. 284 */ 285 protected Sql(int queryHashCode, String preFrom, String preWhere, boolean andWhereExpr, 286 String preHaving, boolean andHavingExpr, String orderByPrefix, String orderBy, boolean distinct) { 287 288 this.queryHashCode = queryHashCode; 289 this.parsed = true; 290 this.unparsedSql = null; 291 this.preFrom = preFrom; 292 this.preHaving = preHaving; 293 this.preWhere = preWhere; 294 this.andHavingExpr = andHavingExpr; 295 this.andWhereExpr = andWhereExpr; 296 this.orderByPrefix = orderByPrefix; 297 this.orderBy = orderBy; 298 this.distinct = distinct; 299 } 300 301 /** 302 * Return a hash for this query. 303 */ 304 public int queryHash() { 305 return queryHashCode; 306 } 307 308 public String toString() { 309 if (!parsed) { 310 return "unparsed[" + unparsedSql + "]"; 311 } 312 return "select[" + preFrom + "] preWhere[" + preWhere + "] preHaving[" + preHaving 313 + "] orderBy[" + orderBy + "]"; 314 } 315 316 public boolean isDistinct() { 317 return distinct; 318 } 319 320 /** 321 * Return true if the SQL is left completely unmodified. 322 * <p> 323 * This means Ebean can't add WHERE or HAVING expressions into the query - 324 * it will be left completely unmodified. 325 * </p> 326 */ 327 public boolean isParsed() { 328 return parsed; 329 } 330 331 /** 332 * Return the SQL when it is unparsed. 333 */ 334 public String getUnparsedSql() { 335 return unparsedSql; 336 } 337 338 /** 339 * Return the SQL prior to FROM clause. 340 */ 341 public String getPreFrom() { 342 return preFrom; 343 } 344 345 /** 346 * Return the SQL prior to WHERE clause. 347 */ 348 public String getPreWhere() { 349 return preWhere; 350 } 351 352 /** 353 * Return true if there is already a WHERE clause and any extra where 354 * expressions start with AND. 355 */ 356 public boolean isAndWhereExpr() { 357 return andWhereExpr; 358 } 359 360 /** 361 * Return the SQL prior to HAVING clause. 362 */ 363 public String getPreHaving() { 364 return preHaving; 365 } 366 367 /** 368 * Return true if there is already a HAVING clause and any extra having 369 * expressions start with AND. 370 */ 371 public boolean isAndHavingExpr() { 372 return andHavingExpr; 373 } 374 375 /** 376 * Return the 'order by' keywords. 377 * This can contain additional keywords, for example 'order siblings by' as Oracle syntax. 378 */ 379 public String getOrderByPrefix() { 380 return (orderByPrefix == null) ? "order by" : orderByPrefix; 381 } 382 383 /** 384 * Return the SQL ORDER BY clause. 385 */ 386 public String getOrderBy() { 387 return orderBy; 388 } 389 390 } 391 392 /** 393 * Defines the column mapping for raw sql DB columns to bean properties. 394 */ 395 public static final class ColumnMapping implements Serializable { 396 397 private static final long serialVersionUID = 1L; 398 399 private final LinkedHashMap<String, Column> dbColumnMap; 400 401 private final Map<String, String> propertyMap; 402 403 private final Map<String, Column> propertyColumnMap; 404 405 private final boolean parsed; 406 407 private final boolean immutable; 408 409 private final int queryHashCode; 410 411 /** 412 * Construct from parsed sql where the columns have been identified. 413 */ 414 protected ColumnMapping(List<Column> columns) { 415 this.queryHashCode = 0; 416 this.immutable = false; 417 this.parsed = true; 418 this.propertyMap = null; 419 this.propertyColumnMap = null; 420 this.dbColumnMap = new LinkedHashMap<String, Column>(); 421 for (int i = 0; i < columns.size(); i++) { 422 Column c = columns.get(i); 423 dbColumnMap.put(c.getDbColumn(), c); 424 } 425 } 426 427 /** 428 * Construct for unparsed sql. 429 */ 430 protected ColumnMapping() { 431 this.queryHashCode = 0; 432 this.immutable = false; 433 this.parsed = false; 434 this.propertyMap = null; 435 this.propertyColumnMap = null; 436 this.dbColumnMap = new LinkedHashMap<String, Column>(); 437 } 438 439 /** 440 * Construct for ResultSet use. 441 */ 442 protected ColumnMapping(String... propertyNames) { 443 this.immutable = false; 444 this.parsed = false; 445 this.propertyMap = null; 446 //this.propertyColumnMap = null; 447 this.dbColumnMap = new LinkedHashMap<String, Column>(); 448 449 int hc = 31; 450 int pos = 0; 451 for (String prop : propertyNames) { 452 hc = 31 * hc + prop.hashCode(); 453 dbColumnMap.put(prop, new Column(pos++, prop, null, prop)); 454 } 455 propertyColumnMap = dbColumnMap; 456 this.queryHashCode = hc; 457 } 458 459 /** 460 * Construct an immutable ColumnMapping based on collected information. 461 */ 462 protected ColumnMapping(boolean parsed, LinkedHashMap<String, Column> dbColumnMap) { 463 this.immutable = true; 464 this.parsed = parsed; 465 this.dbColumnMap = dbColumnMap; 466 467 int hc = ColumnMapping.class.getName().hashCode(); 468 469 HashMap<String, Column> pcMap = new HashMap<String, Column>(); 470 HashMap<String, String> pMap = new HashMap<String, String>(); 471 472 for (Column c : dbColumnMap.values()) { 473 pMap.put(c.getPropertyName(), c.getDbColumn()); 474 pcMap.put(c.getPropertyName(), c); 475 hc = 31 * hc + ((c.getPropertyName() == null) ? 0 : c.getPropertyName().hashCode()); 476 hc = 31 * hc + ((c.getDbColumn() == null) ? 0 : c.getDbColumn().hashCode()); 477 } 478 this.propertyMap = Collections.unmodifiableMap(pMap); 479 this.propertyColumnMap = Collections.unmodifiableMap(pcMap); 480 this.queryHashCode = hc; 481 } 482 483 /** 484 * Return true if the property is mapped. 485 */ 486 public boolean contains(String property) { 487 return this.propertyColumnMap.containsKey(property); 488 } 489 490 /** 491 * Creates an immutable copy of this ColumnMapping. 492 * 493 * @throws IllegalStateException 494 * when a propertyName has not been defined for a column. 495 */ 496 protected ColumnMapping createImmutableCopy() { 497 498 for (Column c : dbColumnMap.values()) { 499 c.checkMapping(); 500 } 501 502 return new ColumnMapping(parsed, dbColumnMap); 503 } 504 505 protected void columnMapping(String dbColumn, String propertyName) { 506 507 if (immutable) { 508 throw new IllegalStateException("Should never happen"); 509 } 510 if (!parsed) { 511 int pos = dbColumnMap.size(); 512 dbColumnMap.put(dbColumn, new Column(pos, dbColumn, null, propertyName)); 513 } else { 514 Column column = dbColumnMap.get(dbColumn); 515 if (column == null) { 516 String msg = "DB Column [" + dbColumn + "] not found in mapping. Expecting one of [" 517 + dbColumnMap.keySet() + "]"; 518 throw new IllegalArgumentException(msg); 519 } 520 column.setPropertyName(propertyName); 521 } 522 } 523 524 /** 525 * Return the query hash for this column mapping. 526 */ 527 public int queryHash() { 528 if (queryHashCode == 0) { 529 throw new RuntimeException("Bug: queryHashCode == 0"); 530 } 531 return queryHashCode; 532 } 533 534 /** 535 * Returns true if the Columns where supplied by parsing the sql select 536 * clause. 537 * <p> 538 * In the case where the columns where parsed then we can do extra checks on 539 * the column mapping such as, is the column a valid one in the sql and 540 * whether all the columns in the sql have been mapped. 541 * </p> 542 */ 543 public boolean isParsed() { 544 return parsed; 545 } 546 547 /** 548 * Return the number of columns in this column mapping. 549 */ 550 public int size() { 551 return dbColumnMap.size(); 552 } 553 554 /** 555 * Return the column mapping. 556 */ 557 protected Map<String, Column> mapping() { 558 return dbColumnMap; 559 } 560 561 /** 562 * Return the mapping by DB column. 563 */ 564 public Map<String, String> getMapping() { 565 return propertyMap; 566 } 567 568 /** 569 * Return the index position by bean property name. 570 */ 571 public int getIndexPosition(String property) { 572 Column c = propertyColumnMap.get(property); 573 return c == null ? -1 : c.getIndexPos(); 574 } 575 576 /** 577 * Return an iterator of the Columns. 578 */ 579 public Iterator<Column> getColumns() { 580 return dbColumnMap.values().iterator(); 581 } 582 583 /** 584 * Modify any column mappings with the given table alias to have the path prefix. 585 * <p> 586 * For example modify all mappings with table alias "c" to have the path prefix "customer". 587 * </p> 588 */ 589 public void tableAliasMapping(String tableAlias, String path) { 590 591 String startMatch = tableAlias+"."; 592 for (Map.Entry<String, Column> entry : dbColumnMap.entrySet()) { 593 if (entry.getKey().startsWith(startMatch)) { 594 entry.getValue().tableAliasMapping(path); 595 } 596 } 597 } 598 599 /** 600 * A Column of the RawSql that is mapped to a bean property (or ignored). 601 */ 602 public static class Column implements Serializable { 603 604 private static final long serialVersionUID = 1L; 605 private final int indexPos; 606 private final String dbColumn; 607 608 private final String dbAlias; 609 610 private String propertyName; 611 612 /** 613 * Construct a Column. 614 */ 615 public Column(int indexPos, String dbColumn, String dbAlias) { 616 this(indexPos, dbColumn, dbAlias, derivePropertyName(dbAlias, dbColumn)); 617 } 618 619 private Column(int indexPos, String dbColumn, String dbAlias, String propertyName) { 620 this.indexPos = indexPos; 621 this.dbColumn = dbColumn; 622 this.dbAlias = dbAlias; 623 if (propertyName == null && dbAlias != null) { 624 this.propertyName = dbAlias; 625 } else { 626 this.propertyName = propertyName; 627 } 628 } 629 630 private static String derivePropertyName(String dbAlias, String dbColumn) { 631 if (dbAlias != null) { 632 return dbAlias; 633 } 634 int dotPos = dbColumn.indexOf('.'); 635 if (dotPos > -1) { 636 dbColumn = dbColumn.substring(dotPos + 1); 637 } 638 return CamelCaseHelper.toCamelFromUnderscore(dbColumn); 639 } 640 641 private void checkMapping() { 642 if (propertyName == null) { 643 String msg = "No propertyName defined (Column mapping) for dbColumn [" + dbColumn + "]"; 644 throw new IllegalStateException(msg); 645 } 646 } 647 648 public String toString() { 649 return dbColumn + "->" + propertyName; 650 } 651 652 /** 653 * Return the index position of this column. 654 */ 655 public int getIndexPos() { 656 return indexPos; 657 } 658 659 /** 660 * Return the DB column name including table alias (if it has one). 661 */ 662 public String getDbColumn() { 663 return dbColumn; 664 } 665 666 /** 667 * Return the DB column alias (if it has one). 668 */ 669 public String getDbAlias() { 670 return dbAlias; 671 } 672 673 /** 674 * Return the bean property this column is mapped to. 675 */ 676 public String getPropertyName() { 677 return propertyName; 678 } 679 680 /** 681 * Set the property name mapped to this db column. 682 */ 683 private void setPropertyName(String propertyName) { 684 this.propertyName = propertyName; 685 } 686 687 /** 688 * Prepend the path to the property name. 689 * <p/> 690 * For example if path is "customer" then "name" becomes "customer.name". 691 */ 692 public void tableAliasMapping(String path) { 693 if (path != null) { 694 propertyName = path + "." + propertyName; 695 } 696 } 697 } 698 } 699}