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.getDbColumnKey(), 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 [" + dbColumnMap.keySet() + "]"; 504 throw new IllegalArgumentException(msg); 505 } 506 column.setPropertyName(propertyName); 507 } 508 } 509 510 /** 511 * Returns true if the Columns where supplied by parsing the sql select 512 * clause. 513 * <p> 514 * In the case where the columns where parsed then we can do extra checks on 515 * the column mapping such as, is the column a valid one in the sql and 516 * whether all the columns in the sql have been mapped. 517 * </p> 518 */ 519 public boolean isParsed() { 520 return parsed; 521 } 522 523 /** 524 * Return the number of columns in this column mapping. 525 */ 526 public int size() { 527 return dbColumnMap.size(); 528 } 529 530 /** 531 * Return the column mapping. 532 */ 533 protected Map<String, Column> mapping() { 534 return dbColumnMap; 535 } 536 537 /** 538 * Return the mapping by DB column. 539 */ 540 public Map<String, String> getMapping() { 541 return propertyMap; 542 } 543 544 /** 545 * Return the index position by bean property name. 546 */ 547 public int getIndexPosition(String property) { 548 Column c = propertyColumnMap.get(property); 549 return c == null ? -1 : c.getIndexPos(); 550 } 551 552 /** 553 * Return an iterator of the Columns. 554 */ 555 public Iterator<Column> getColumns() { 556 return dbColumnMap.values().iterator(); 557 } 558 559 /** 560 * Modify any column mappings with the given table alias to have the path prefix. 561 * <p> 562 * For example modify all mappings with table alias "c" to have the path prefix "customer". 563 * </p> 564 * <p> 565 * For the "Root type" you don't need to specify a tableAliasMapping. 566 * </p> 567 */ 568 public void tableAliasMapping(String tableAlias, String path) { 569 570 String startMatch = tableAlias+"."; 571 for (Map.Entry<String, Column> entry : dbColumnMap.entrySet()) { 572 if (entry.getKey().startsWith(startMatch)) { 573 entry.getValue().tableAliasMapping(path); 574 } 575 } 576 } 577 578 /** 579 * A Column of the RawSql that is mapped to a bean property (or ignored). 580 */ 581 public static class Column implements Serializable { 582 583 private static final long serialVersionUID = 1L; 584 private final int indexPos; 585 private final String dbColumn; 586 587 private final String dbAlias; 588 589 private String propertyName; 590 591 /** 592 * Construct a Column. 593 */ 594 public Column(int indexPos, String dbColumn, String dbAlias) { 595 this(indexPos, dbColumn, dbAlias, derivePropertyName(dbAlias, dbColumn)); 596 } 597 598 private Column(int indexPos, String dbColumn, String dbAlias, String propertyName) { 599 this.indexPos = indexPos; 600 this.dbColumn = dbColumn; 601 this.dbAlias = dbAlias; 602 if (propertyName == null && dbAlias != null) { 603 this.propertyName = dbAlias; 604 } else { 605 this.propertyName = propertyName; 606 } 607 } 608 609 protected static String derivePropertyName(String dbAlias, String dbColumn) { 610 if (dbAlias != null) { 611 return CamelCaseHelper.toCamelFromUnderscore(dbAlias); 612 } 613 int dotPos = dbColumn.indexOf('.'); 614 if (dotPos > -1) { 615 dbColumn = dbColumn.substring(dotPos + 1); 616 } 617 return CamelCaseHelper.toCamelFromUnderscore(dbColumn); 618 } 619 620 private void checkMapping() { 621 if (propertyName == null) { 622 String msg = "No propertyName defined (Column mapping) for dbColumn [" + dbColumn + "]"; 623 throw new IllegalStateException(msg); 624 } 625 } 626 627 @Override 628 public boolean equals(Object o) { 629 if (this == o) return true; 630 if (o == null || getClass() != o.getClass()) return false; 631 632 Column that = (Column) o; 633 if (indexPos != that.indexPos) return false; 634 if (!dbColumn.equals(that.dbColumn)) return false; 635 if (dbAlias != null ? !dbAlias.equals(that.dbAlias) : that.dbAlias != null) return false; 636 return propertyName != null ? propertyName.equals(that.propertyName) : that.propertyName == null; 637 } 638 639 @Override 640 public int hashCode() { 641 int result = indexPos; 642 result = 31 * result + dbColumn.hashCode(); 643 result = 31 * result + (dbAlias != null ? dbAlias.hashCode() : 0); 644 result = 31 * result + (propertyName != null ? propertyName.hashCode() : 0); 645 return result; 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 alias if specified otherwise DB column. 661 * This is used as the key for mapping a column to a logical property. 662 */ 663 public String getDbColumnKey() { 664 return (dbAlias != null) ? dbAlias : dbColumn; 665 } 666 667 /** 668 * Return the DB column name including table alias (if it has one). 669 */ 670 public String getDbColumn() { 671 return dbColumn; 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}