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 * Return the key; 211 */ 212 public Key getKey() { 213 boolean parsed = sql != null && sql.parsed; 214 String unParsedSql = (sql == null) ? "" : sql.unparsedSql; 215 return new Key(parsed, unParsedSql, columnMapping); 216 } 217 218 /** 219 * Return the resultSet if this is a ResultSet based RawSql. 220 */ 221 public ResultSet getResultSet() { 222 return resultSet; 223 } 224 225 /** 226 * Return the column mapping for the SQL columns to bean properties. 227 */ 228 public ColumnMapping getColumnMapping() { 229 return columnMapping; 230 } 231 232 /** 233 * Represents the sql part of the query. For parsed RawSql the sql is broken 234 * up so that Ebean can insert extra WHERE and HAVING expressions into the 235 * SQL. 236 */ 237 public static final class Sql implements Serializable { 238 239 private static final long serialVersionUID = 1L; 240 241 private final boolean parsed; 242 243 private final String unparsedSql; 244 245 private final String preFrom; 246 247 private final String preWhere; 248 249 private final boolean andWhereExpr; 250 251 private final String preHaving; 252 253 private final boolean andHavingExpr; 254 255 private final String orderByPrefix; 256 257 private final String orderBy; 258 259 private final boolean distinct; 260 261 /** 262 * Construct for unparsed SQL. 263 */ 264 protected Sql(String unparsedSql) { 265 this.parsed = false; 266 this.unparsedSql = unparsedSql; 267 this.preFrom = null; 268 this.preHaving = null; 269 this.preWhere = null; 270 this.andHavingExpr = false; 271 this.andWhereExpr = false; 272 this.orderByPrefix = null; 273 this.orderBy = null; 274 this.distinct = false; 275 } 276 277 /** 278 * Construct for parsed SQL. 279 */ 280 protected Sql(String unparsedSql, String preFrom, String preWhere, boolean andWhereExpr, 281 String preHaving, boolean andHavingExpr, String orderByPrefix, String orderBy, boolean distinct) { 282 283 this.unparsedSql = unparsedSql; 284 this.parsed = true; 285 this.preFrom = preFrom; 286 this.preHaving = preHaving; 287 this.preWhere = preWhere; 288 this.andHavingExpr = andHavingExpr; 289 this.andWhereExpr = andWhereExpr; 290 this.orderByPrefix = orderByPrefix; 291 this.orderBy = orderBy; 292 this.distinct = distinct; 293 } 294 295 public String toString() { 296 if (!parsed) { 297 return "unparsed[" + unparsedSql + "]"; 298 } 299 return "select[" + preFrom + "] preWhere[" + preWhere + "] preHaving[" + preHaving 300 + "] orderBy[" + orderBy + "]"; 301 } 302 303 public boolean isDistinct() { 304 return distinct; 305 } 306 307 /** 308 * Return true if the SQL is left completely unmodified. 309 * <p> 310 * This means Ebean can't add WHERE or HAVING expressions into the query - 311 * it will be left completely unmodified. 312 * </p> 313 */ 314 public boolean isParsed() { 315 return parsed; 316 } 317 318 /** 319 * Return the SQL when it is unparsed. 320 */ 321 public String getUnparsedSql() { 322 return unparsedSql; 323 } 324 325 /** 326 * Return the SQL prior to FROM clause. 327 */ 328 public String getPreFrom() { 329 return preFrom; 330 } 331 332 /** 333 * Return the SQL prior to WHERE clause. 334 */ 335 public String getPreWhere() { 336 return preWhere; 337 } 338 339 /** 340 * Return true if there is already a WHERE clause and any extra where 341 * expressions start with AND. 342 */ 343 public boolean isAndWhereExpr() { 344 return andWhereExpr; 345 } 346 347 /** 348 * Return the SQL prior to HAVING clause. 349 */ 350 public String getPreHaving() { 351 return preHaving; 352 } 353 354 /** 355 * Return true if there is already a HAVING clause and any extra having 356 * expressions start with AND. 357 */ 358 public boolean isAndHavingExpr() { 359 return andHavingExpr; 360 } 361 362 /** 363 * Return the 'order by' keywords. 364 * This can contain additional keywords, for example 'order siblings by' as Oracle syntax. 365 */ 366 public String getOrderByPrefix() { 367 return (orderByPrefix == null) ? "order by" : orderByPrefix; 368 } 369 370 /** 371 * Return the SQL ORDER BY clause. 372 */ 373 public String getOrderBy() { 374 return orderBy; 375 } 376 377 } 378 379 /** 380 * Defines the column mapping for raw sql DB columns to bean properties. 381 */ 382 public static final class ColumnMapping implements Serializable { 383 384 private static final long serialVersionUID = 1L; 385 386 private final LinkedHashMap<String, Column> dbColumnMap; 387 388 private final Map<String, String> propertyMap; 389 390 private final Map<String, Column> propertyColumnMap; 391 392 private final boolean parsed; 393 394 private final boolean immutable; 395 396 /** 397 * Construct from parsed sql where the columns have been identified. 398 */ 399 protected ColumnMapping(List<Column> columns) { 400 this.immutable = false; 401 this.parsed = true; 402 this.propertyMap = null; 403 this.propertyColumnMap = null; 404 this.dbColumnMap = new LinkedHashMap<String, Column>(); 405 for (int i = 0; i < columns.size(); i++) { 406 Column c = columns.get(i); 407 dbColumnMap.put(c.getDbColumn(), c); 408 } 409 } 410 411 /** 412 * Construct for unparsed sql. 413 */ 414 protected ColumnMapping() { 415 this.immutable = false; 416 this.parsed = false; 417 this.propertyMap = null; 418 this.propertyColumnMap = null; 419 this.dbColumnMap = new LinkedHashMap<String, Column>(); 420 } 421 422 /** 423 * Construct for ResultSet use. 424 */ 425 protected ColumnMapping(String... propertyNames) { 426 this.immutable = false; 427 this.parsed = false; 428 this.propertyMap = null; 429 this.dbColumnMap = new LinkedHashMap<String, Column>(); 430 431 int pos = 0; 432 for (String prop : propertyNames) { 433 dbColumnMap.put(prop, new Column(pos++, prop, null, prop)); 434 } 435 propertyColumnMap = dbColumnMap; 436 } 437 438 /** 439 * Construct an immutable ColumnMapping based on collected information. 440 */ 441 protected ColumnMapping(boolean parsed, LinkedHashMap<String, Column> dbColumnMap) { 442 this.immutable = true; 443 this.parsed = parsed; 444 this.dbColumnMap = dbColumnMap; 445 446 HashMap<String, Column> pcMap = new HashMap<String, Column>(); 447 HashMap<String, String> pMap = new HashMap<String, String>(); 448 449 for (Column c : dbColumnMap.values()) { 450 pMap.put(c.getPropertyName(), c.getDbColumn()); 451 pcMap.put(c.getPropertyName(), c); 452 } 453 this.propertyMap = Collections.unmodifiableMap(pMap); 454 this.propertyColumnMap = Collections.unmodifiableMap(pcMap); 455 } 456 457 @Override 458 public boolean equals(Object o) { 459 if (this == o) return true; 460 if (o == null || getClass() != o.getClass()) return false; 461 ColumnMapping that = (ColumnMapping) o; 462 return dbColumnMap.equals(that.dbColumnMap); 463 } 464 465 @Override 466 public int hashCode() { 467 return dbColumnMap.hashCode(); 468 } 469 470 /** 471 * Return true if the property is mapped. 472 */ 473 public boolean contains(String property) { 474 return this.propertyColumnMap.containsKey(property); 475 } 476 477 /** 478 * Creates an immutable copy of this ColumnMapping. 479 * 480 * @throws IllegalStateException 481 * when a propertyName has not been defined for a column. 482 */ 483 protected ColumnMapping createImmutableCopy() { 484 485 for (Column c : dbColumnMap.values()) { 486 c.checkMapping(); 487 } 488 489 return new ColumnMapping(parsed, dbColumnMap); 490 } 491 492 protected void columnMapping(String dbColumn, String propertyName) { 493 494 if (immutable) { 495 throw new IllegalStateException("Should never happen"); 496 } 497 if (!parsed) { 498 int pos = dbColumnMap.size(); 499 dbColumnMap.put(dbColumn, new Column(pos, dbColumn, null, propertyName)); 500 } else { 501 Column column = dbColumnMap.get(dbColumn); 502 if (column == null) { 503 String msg = "DB Column [" + dbColumn + "] not found in mapping. Expecting one of [" 504 + dbColumnMap.keySet() + "]"; 505 throw new IllegalArgumentException(msg); 506 } 507 column.setPropertyName(propertyName); 508 } 509 } 510 511 /** 512 * Returns true if the Columns where supplied by parsing the sql select 513 * clause. 514 * <p> 515 * In the case where the columns where parsed then we can do extra checks on 516 * the column mapping such as, is the column a valid one in the sql and 517 * whether all the columns in the sql have been mapped. 518 * </p> 519 */ 520 public boolean isParsed() { 521 return parsed; 522 } 523 524 /** 525 * Return the number of columns in this column mapping. 526 */ 527 public int size() { 528 return dbColumnMap.size(); 529 } 530 531 /** 532 * Return the column mapping. 533 */ 534 protected Map<String, Column> mapping() { 535 return dbColumnMap; 536 } 537 538 /** 539 * Return the mapping by DB column. 540 */ 541 public Map<String, String> getMapping() { 542 return propertyMap; 543 } 544 545 /** 546 * Return the index position by bean property name. 547 */ 548 public int getIndexPosition(String property) { 549 Column c = propertyColumnMap.get(property); 550 return c == null ? -1 : c.getIndexPos(); 551 } 552 553 /** 554 * Return an iterator of the Columns. 555 */ 556 public Iterator<Column> getColumns() { 557 return dbColumnMap.values().iterator(); 558 } 559 560 /** 561 * Modify any column mappings with the given table alias to have the path prefix. 562 * <p> 563 * For example modify all mappings with table alias "c" to have the path prefix "customer". 564 * </p> 565 * <p> 566 * For the "Root type" you don't need to specify a tableAliasMapping. 567 * </p> 568 */ 569 public void tableAliasMapping(String tableAlias, String path) { 570 571 String startMatch = tableAlias+"."; 572 for (Map.Entry<String, Column> entry : dbColumnMap.entrySet()) { 573 if (entry.getKey().startsWith(startMatch)) { 574 entry.getValue().tableAliasMapping(path); 575 } 576 } 577 } 578 579 /** 580 * A Column of the RawSql that is mapped to a bean property (or ignored). 581 */ 582 public static class Column implements Serializable { 583 584 private static final long serialVersionUID = 1L; 585 private final int indexPos; 586 private final String dbColumn; 587 588 private final String dbAlias; 589 590 private String propertyName; 591 592 /** 593 * Construct a Column. 594 */ 595 public Column(int indexPos, String dbColumn, String dbAlias) { 596 this(indexPos, dbColumn, dbAlias, derivePropertyName(dbAlias, dbColumn)); 597 } 598 599 private Column(int indexPos, String dbColumn, String dbAlias, String propertyName) { 600 this.indexPos = indexPos; 601 this.dbColumn = dbColumn; 602 this.dbAlias = dbAlias; 603 if (propertyName == null && dbAlias != null) { 604 this.propertyName = dbAlias; 605 } else { 606 this.propertyName = propertyName; 607 } 608 } 609 610 private static String derivePropertyName(String dbAlias, String dbColumn) { 611 if (dbAlias != null) { 612 return dbAlias; 613 } 614 int dotPos = dbColumn.indexOf('.'); 615 if (dotPos > -1) { 616 dbColumn = dbColumn.substring(dotPos + 1); 617 } 618 return CamelCaseHelper.toCamelFromUnderscore(dbColumn); 619 } 620 621 private void checkMapping() { 622 if (propertyName == null) { 623 String msg = "No propertyName defined (Column mapping) for dbColumn [" + dbColumn + "]"; 624 throw new IllegalStateException(msg); 625 } 626 } 627 628 @Override 629 public boolean equals(Object o) { 630 if (this == o) return true; 631 if (o == null || getClass() != o.getClass()) return false; 632 633 Column that = (Column) o; 634 if (indexPos != that.indexPos) return false; 635 if (!dbColumn.equals(that.dbColumn)) return false; 636 if (dbAlias != null ? !dbAlias.equals(that.dbAlias) : that.dbAlias != null) return false; 637 return propertyName != null ? propertyName.equals(that.propertyName) : that.propertyName == null; 638 } 639 640 @Override 641 public int hashCode() { 642 int result = indexPos; 643 result = 31 * result + dbColumn.hashCode(); 644 result = 31 * result + (dbAlias != null ? dbAlias.hashCode() : 0); 645 result = 31 * result + (propertyName != null ? propertyName.hashCode() : 0); 646 return result; 647 } 648 649 public String toString() { 650 return dbColumn + "->" + propertyName; 651 } 652 653 /** 654 * Return the index position of this column. 655 */ 656 public int getIndexPos() { 657 return indexPos; 658 } 659 660 /** 661 * Return the DB column name including table alias (if it has one). 662 */ 663 public String getDbColumn() { 664 return dbColumn; 665 } 666 667 /** 668 * Return the DB column alias (if it has one). 669 */ 670 public String getDbAlias() { 671 return dbAlias; 672 } 673 674 /** 675 * Return the bean property this column is mapped to. 676 */ 677 public String getPropertyName() { 678 return propertyName; 679 } 680 681 /** 682 * Set the property name mapped to this db column. 683 */ 684 private void setPropertyName(String propertyName) { 685 this.propertyName = propertyName; 686 } 687 688 /** 689 * Prepend the path to the property name. 690 * <p/> 691 * For example if path is "customer" then "name" becomes "customer.name". 692 */ 693 public void tableAliasMapping(String path) { 694 if (path != null) { 695 propertyName = path + "." + propertyName; 696 } 697 } 698 } 699 } 700 701 /** 702 * A key for the RawSql object using for the query plan. 703 */ 704 public static final class Key { 705 706 private final boolean parsed; 707 private final ColumnMapping columnMapping; 708 private final String unParsedSql; 709 710 Key(boolean parsed, String unParsedSql, ColumnMapping columnMapping) { 711 this.parsed = parsed; 712 this.unParsedSql = unParsedSql; 713 this.columnMapping = columnMapping; 714 } 715 716 @Override 717 public boolean equals(Object o) { 718 if (this == o) return true; 719 if (o == null || getClass() != o.getClass()) return false; 720 721 Key that = (Key) o; 722 return parsed == that.parsed 723 && columnMapping.equals(that.columnMapping) 724 && unParsedSql.equals(that.unParsedSql); 725 } 726 727 @Override 728 public int hashCode() { 729 int result = (parsed ? 1 : 0); 730 result = 31 * result + columnMapping.hashCode(); 731 result = 31 * result + unParsedSql.hashCode(); 732 return result; 733 } 734 } 735}