Coverage Summary for Class: Session (net.sf.persism)
| Class | Class, % | Method, % | Line, % |
|---|---|---|---|
| Session | 100% (1/1) | 100% (25/25) | 96.7% (405/419) |
1 package net.sf.persism; 2 3 import net.sf.persism.annotations.NotTable; 4 import net.sf.persism.annotations.View; 5 6 import java.lang.reflect.Method; 7 import java.sql.*; 8 import java.util.*; 9 10 import static net.sf.persism.Parameters.none; 11 import static net.sf.persism.Parameters.params; 12 import static net.sf.persism.SQL.sql; 13 import static net.sf.persism.SQL.where; 14 import static net.sf.persism.Util.isRecord; 15 16 /** 17 * Performs various read and write operations in the database. 18 * 19 * @author Dan Howard 20 * @since 1/8/2021 21 */ 22 public final class Session implements AutoCloseable { 23 24 private static final Log log = Log.getLogger(Session.class); 25 private static final Log blog = Log.getLogger("net.sf.persism.Benchmarks"); 26 private static final Log sqllog = Log.getLogger("net.sf.persism.SQL"); 27 28 final SessionHelper helper; 29 30 Connection connection; 31 MetaData metaData; 32 Reader reader; 33 Converter converter; 34 35 /** 36 * @param connection db connection 37 * @throws PersismException if something goes wrong 38 */ 39 public Session(Connection connection) throws PersismException { 40 this.connection = connection; 41 helper = new SessionHelper(this); 42 init(connection, null); 43 } 44 45 /** 46 * Constructor for Session where you want to specify the Session Key. 47 * 48 * @param connection db connection 49 * @param sessionKey Unique string to represent the connection URL if it is not available on the Connection metadata. 50 * This string should start with the jdbc url string to indicate the connection type. 51 * <code> 52 * <br> 53 * <br> jdbc:h2 = h2 54 * <br> jdbc:sqlserver = MS SQL 55 * <br> jdbc:oracle = Oracle 56 * <br> jdbc:sqlite = SQLite 57 * <br> jdbc:derby = Derby 58 * <br> jdbc:mysql = MySQL/MariaDB 59 * <br> jdbc:postgresql = PostgreSQL 60 * <br> jdbc:firebirdsql = Firebird (Jaybird) 61 * <br> jdbc:hsqldb = HSQLDB 62 * <br> jdbc:ucanaccess = MS Access 63 * <br> jdbc:informix = Informix 64 * </code> 65 * @throws PersismException if something goes wrong 66 */ 67 public Session(Connection connection, String sessionKey) throws PersismException { 68 this.connection = connection; 69 helper = new SessionHelper(this); 70 init(connection, sessionKey); 71 } 72 73 /** 74 * Close the connection 75 */ 76 @Override 77 public void close() { 78 if (connection != null) { 79 try { 80 connection.close(); 81 } catch (SQLException e) { 82 log.warn(e.getMessage(), e); 83 } 84 } 85 } 86 87 private void init(Connection connection, String sessionKey) { 88 // place any DB specific properties here. 89 try { 90 metaData = MetaData.getInstance(connection, sessionKey); 91 } catch (SQLException e) { 92 throw new PersismException(e.getMessage(), e); 93 } 94 95 converter = new Converter(); 96 reader = new Reader(this); 97 } 98 99 100 /** 101 * Fetch an object from the database by it's primary key(s). 102 * You should instantiate the object and set the primary key properties before calling this method. 103 * 104 * @param object Data object to read from the database. 105 * @return true if the object was found by the primary key. 106 * @throws PersismException if something goes wrong. 107 */ 108 public boolean fetch(Object object) throws PersismException { 109 Class<?> objectClass = object.getClass(); 110 111 // If we know this type it means it's a primitive type. This method cannot be used for primitives 112 boolean readPrimitive = JavaType.getType(objectClass) != null; 113 if (readPrimitive) { 114 throw new PersismException(Message.OperationNotSupportedForJavaType.message(objectClass, "FETCH")); 115 } 116 117 if (isRecord(objectClass)) { 118 throw new PersismException(Message.OperationNotSupportedForRecord.message(objectClass, "FETCH")); 119 } 120 121 if (objectClass.getAnnotation(View.class) != null) { 122 throw new PersismException(Message.OperationNotSupportedForView.message(objectClass, "FETCH")); 123 } 124 125 if (objectClass.getAnnotation(NotTable.class) != null) { 126 throw new PersismException(Message.OperationNotSupportedForNotTableQuery.message(objectClass, "FETCH")); 127 } 128 129 List<String> primaryKeys = metaData.getPrimaryKeys(objectClass, connection); 130 if (primaryKeys.size() == 0) { 131 throw new PersismException(Message.TableHasNoPrimaryKeys.message("FETCH", metaData.getTableInfo(objectClass).name())); 132 } 133 134 Map<String, PropertyInfo> properties = metaData.getTableColumnsPropertyInfo(objectClass, connection); 135 Parameters params = new Parameters(); 136 137 List<ColumnInfo> columnInfos = new ArrayList<>(properties.size()); 138 Map<String, ColumnInfo> cols = metaData.getColumns(objectClass, connection); 139 JDBCResult result = new JDBCResult(); 140 try { 141 for (String column : primaryKeys) { 142 PropertyInfo propertyInfo = properties.get(column); 143 params.add(propertyInfo.getValue(object)); 144 columnInfos.add(cols.get(column)); 145 } 146 assert params.size() == columnInfos.size(); 147 148 String sql = metaData.getDefaultSelectStatement(objectClass, connection); 149 log.debug("FETCH %s PARAMS: %s", sql, params); 150 for (int j = 0; j < params.size(); j++) { 151 if (params.get(j) != null) { 152 params.set(j, converter.convert(params.get(j), columnInfos.get(j).columnType.getJavaType(), columnInfos.get(j).columnName)); 153 } 154 } 155 156 helper.exec(result, sql, params.toArray()); 157 158 verifyPropertyInfoForQuery(objectClass, properties, result.rs); 159 160 if (result.rs.next()) { 161 reader.readObject(object, properties, result.rs); 162 helper.handleJoins(object, objectClass, sql, params); 163 return true; 164 } 165 return false; 166 167 } catch (Exception e) { 168 Util.rollback(connection); 169 throw new PersismException(e.getMessage(), e); 170 } finally { 171 Util.cleanup(result.st, result.rs); 172 } 173 } 174 175 /** 176 * Fetch object by primary key(s) 177 * 178 * @param objectClass Type to return (should be a POJO data class or a record) 179 * @param primaryKeyValues primary key values 180 * @param <T> Type 181 * @return Instance of object type T or NULL if not found 182 * @throws PersismException if you pass a Java primitive or other invalid type for objectClass or something else goes wrong. 183 */ 184 public <T> T fetch(Class<T> objectClass, Parameters primaryKeyValues) { 185 if (objectClass.getAnnotation(NotTable.class) != null) { 186 throw new PersismException(Message.OperationNotSupportedForNotTableQuery.message(objectClass, "FETCH w/o specifying the SQL")); 187 } 188 189 // View does not have any good way to know about primary keys 190 if (objectClass.getAnnotation(View.class) != null) { 191 throw new PersismException(Message.OperationNotSupportedForView.message(objectClass, "FETCH w/o specifying the SQL with @View")); 192 } 193 194 if (JavaType.getType(objectClass) != null) { 195 throw new PersismException(Message.OperationNotSupportedForJavaType.message(objectClass, "FETCH")); 196 } 197 primaryKeyValues.areKeys = true; 198 199 SQL sql = new SQL(metaData.getDefaultSelectStatement(objectClass, connection)); 200 return fetch(objectClass, sql, primaryKeyValues); 201 } 202 203 /** 204 * Fetch object by arbitrary SQL 205 * 206 * @param objectClass Type to return 207 * @param sql SQL query 208 * @param <T> Type 209 * @return Instance of object type T or NULL if not found 210 * @throws PersismException if something goes wrong. 211 */ 212 public <T> T fetch(Class<T> objectClass, SQL sql) { 213 return fetch(objectClass, sql, none()); 214 } 215 216 /** 217 * Fetch an object of the specified type from the database. The type can be a Data Object or a native Java Object or primitive. 218 * 219 * @param objectClass Type of returned value 220 * @param sql query - this would usually be a select OR a select of a single column if the type is a primitive. 221 * If this is a primitive type then this method will only look at the 1st column in the result. 222 * @param parameters parameters to the query. 223 * @param <T> Return type 224 * @return value read from the database of type T or null if not found 225 * @throws PersismException Well, this is a runtime exception, so it actually could be anything really. 226 */ 227 public <T> T fetch(Class<T> objectClass, SQL sql, Parameters parameters) { 228 // If we know this type it means it's a primitive type. Not a DAO so we use a different rule to read those 229 boolean isPOJO = JavaType.getType(objectClass) == null; 230 boolean isRecord = isPOJO && isRecord(objectClass); 231 232 helper.checkIfStoredProcOrSQL(objectClass, sql); 233 234 JDBCResult result = JDBCResult.DEFAULT; 235 try { 236 result = helper.executeQuery(objectClass, sql, parameters); 237 238 Map<String, PropertyInfo> properties = Collections.emptyMap(); 239 if (isPOJO) { 240 if (objectClass.getAnnotation(NotTable.class) == null) { 241 properties = metaData.getTableColumnsPropertyInfo(objectClass, connection); 242 } else { 243 properties = metaData.getQueryColumnsPropertyInfo(objectClass, result.rs); 244 } 245 } 246 247 if (result.rs.next()) { 248 if (isRecord) { 249 RecordInfo<T> recordInfo = new RecordInfo<>(objectClass, properties, result.rs); 250 var ret = reader.readRecord(recordInfo, result.rs); 251 helper.handleJoins(ret, objectClass, sql.toString(), parameters); 252 return ret; 253 254 } else if (isPOJO) { 255 var pojo = objectClass.getDeclaredConstructor().newInstance(); 256 verifyPropertyInfoForQuery(objectClass, properties, result.rs); 257 var ret = reader.readObject(pojo, properties, result.rs); 258 helper.handleJoins(ret, objectClass, sql.toString(), parameters); 259 return ret; 260 261 } else { 262 ResultSetMetaData rsmd = result.rs.getMetaData(); 263 //noinspection unchecked 264 return (T) reader.readColumn(result.rs, 1, rsmd.getColumnType(1), rsmd.getColumnLabel(1), objectClass); 265 } 266 } 267 268 return null; 269 270 } catch (Exception e) { 271 Util.rollback(connection); 272 throw new PersismException(e.getMessage(), e); 273 } finally { 274 Util.cleanup(result.st, result.rs); 275 } 276 } 277 278 /** 279 * Query to return all results. 280 * 281 * @param objectClass Type of returned value 282 * @param <T> Return type 283 * @return List of type T read from the database 284 * @throws PersismException Oof. 285 */ 286 public <T> List<T> query(Class<T> objectClass) { 287 if (objectClass.getAnnotation(NotTable.class) != null) { 288 throw new PersismException(Message.OperationNotSupportedForNotTableQuery.message(objectClass, "QUERY w/o specifying the SQL")); 289 } 290 291 if (JavaType.getType(objectClass) != null) { 292 throw new PersismException(Message.OperationNotSupportedForJavaType.message(objectClass, "QUERY w/o specifying the SQL")); 293 } 294 SQL sql = sql(metaData.getSelectStatement(objectClass, connection)); 295 return query(objectClass, sql, none()); 296 } 297 298 /** 299 * Query for any arbitrary SQL statement. 300 * 301 * @param objectClass Type of returned value 302 * @param sql SQL to use for Querying 303 * @param <T> Return type 304 * @return List of type T read from the database 305 * @throws PersismException He's dead Jim! 306 */ 307 public <T> List<T> query(Class<T> objectClass, SQL sql) { 308 return query(objectClass, sql, none()); 309 } 310 311 /** 312 * Query to return any results matching the primary key values provided. 313 * 314 * @param objectClass Type of returned value 315 * @param primaryKeyValues Parameters containing primary key values 316 * @param <T> Return type 317 * @return List of type T read from the database of any rows matching the primary keys. If you pass multiple primaries this will use WHERE IN(?,?,?) to find them. 318 * @throws PersismException Oh no. Not again. 319 */ 320 public <T> List<T> query(Class<T> objectClass, Parameters primaryKeyValues) { 321 322 // NotTable requires SQL - we don't know what SQL to use here. 323 if (objectClass.getAnnotation(NotTable.class) != null) { 324 throw new PersismException(Message.OperationNotSupportedForNotTableQuery.message(objectClass, "QUERY w/o specifying the SQL")); 325 } 326 327 // View does not have any good way to know about primary keys 328 if (objectClass.getAnnotation(View.class) != null && primaryKeyValues.size() > 0) { 329 throw new PersismException(Message.OperationNotSupportedForView.message(objectClass, "QUERY w/o specifying the SQL with @View since we don't have Primary Keys")); 330 } 331 332 // Requires a POJO or Record 333 if (JavaType.getType(objectClass) != null) { 334 throw new PersismException(Message.OperationNotSupportedForJavaType.message(objectClass, "QUERY")); 335 } 336 337 if (primaryKeyValues.size() == 0) { 338 return query(objectClass); // select all 339 } 340 341 List<String> primaryKeys = metaData.getPrimaryKeys(objectClass, connection); 342 if (primaryKeys.size() == 0) { 343 throw new PersismException(Message.TableHasNoPrimaryKeys.message("QUERY", metaData.getTableInfo(objectClass))); 344 } 345 346 primaryKeyValues.areKeys = true; 347 348 if (primaryKeyValues.size() == primaryKeys.size()) { 349 // single select 350 return query(objectClass, sql(metaData.getDefaultSelectStatement(objectClass, connection)), primaryKeyValues); 351 } 352 353 String query = metaData.getSelectStatement(objectClass, connection) + metaData.getPrimaryInClause(objectClass, primaryKeyValues.size(), connection); 354 SQL sql = sql(query); 355 return query(objectClass, sql, primaryKeyValues); 356 } 357 358 /** 359 * Query for a list of objects of the specified class using the specified SQL query and parameters. 360 * The type of the list can be Data Objects or native Java Objects or primitives. 361 * 362 * @param objectClass class of objects to return. 363 * @param sql query string to execute. 364 * @param parameters parameters to the query. 365 * @param <T> Return type 366 * @return a list of objects of the specified class using the specified SQL query and parameters. 367 * @throws PersismException If something goes wrong you get a big stack trace. 368 */ 369 public <T> List<T> query(Class<T> objectClass, SQL sql, Parameters parameters) { 370 371 helper.checkIfStoredProcOrSQL(objectClass, sql); 372 373 List<T> list = new ArrayList<>(32); 374 375 // If we know this type it means it's a primitive type. Not a DAO so we use a different rule to read those 376 boolean isPOJO = JavaType.getType(objectClass) == null; 377 boolean isRecord = isPOJO && isRecord(objectClass); 378 379 long now = System.currentTimeMillis(); 380 JDBCResult result = JDBCResult.DEFAULT; 381 try { 382 result = helper.executeQuery(objectClass, sql, parameters); 383 384 Map<String, PropertyInfo> properties = Collections.emptyMap(); 385 if (isPOJO) { 386 if (objectClass.getAnnotation(NotTable.class) == null) { 387 properties = metaData.getTableColumnsPropertyInfo(objectClass, connection); 388 } else { 389 properties = metaData.getQueryColumnsPropertyInfo(objectClass, result.rs); 390 } 391 } 392 393 if (isRecord) { 394 RecordInfo<T> recordInfo = new RecordInfo<>(objectClass, properties, result.rs); 395 while (result.rs.next()) { 396 var record = reader.readRecord(recordInfo, result.rs); 397 list.add(record); 398 } 399 } else if (isPOJO) { 400 verifyPropertyInfoForQuery(objectClass, properties, result.rs); 401 while (result.rs.next()) { 402 var pojo = objectClass.getDeclaredConstructor().newInstance(); 403 list.add(reader.readObject(pojo, properties, result.rs)); 404 } 405 } else { 406 ResultSetMetaData rsmd = result.rs.getMetaData(); 407 while (result.rs.next()) { 408 //noinspection unchecked 409 list.add((T) reader.readColumn(result.rs, 1, rsmd.getColumnType(1), rsmd.getColumnLabel(1), objectClass)); 410 } 411 } 412 413 //blog.debug("TIME TO READ " + objectClass + " " + (System.currentTimeMillis() - now) + " SIZE " + list.size()); 414 blog.debug("READ time: %s SIZE: %s %s", (System.currentTimeMillis() - now), list.size(), objectClass); 415 416 if (list.size() > 0) { 417 now = System.currentTimeMillis(); 418 helper.handleJoins(list, objectClass, sql.toString(), parameters); 419 } 420 421 if (blog.isDebugEnabled()) { 422 blog.debug("handleJoins TIME: " + (System.currentTimeMillis() - now) + " " + objectClass, new Throwable()); 423 } 424 425 } catch (Exception e) { 426 Util.rollback(connection); 427 throw new PersismException(e.getMessage(), e); 428 } finally { 429 Util.cleanup(result.st, result.rs); 430 } 431 432 return list; 433 } 434 435 private void verifyPropertyInfoForQuery(Class<?> objectClass, Map<String, PropertyInfo> properties, ResultSet rs) throws SQLException { 436 437 // Test if all properties have column mapping (skipping joins) and throw PersismException if not 438 // This block verifies that the object is fully initialized. 439 // Any properties not marked by NotColumn should have been set (or if they have a getter only) 440 // If not throw a PersismException 441 Collection<PropertyInfo> allProperties = MetaData.getPropertyInfo(objectClass).stream().filter(p -> !p.isJoin).toList(); 442 if (properties.values().size() < allProperties.size()) { 443 Set<PropertyInfo> missing = new HashSet<>(allProperties.size()); 444 missing.addAll(allProperties); 445 missing.removeAll(properties.values()); 446 447 StringBuilder sb = new StringBuilder(); 448 String sep = ""; 449 for (PropertyInfo prop : missing) { 450 sb.append(sep).append(prop.propertyName); 451 sep = ","; 452 } 453 454 throw new PersismException(Message.ObjectNotProperlyInitialized.message(objectClass, sb)); 455 } 456 457 ResultSetMetaData rsmd = rs.getMetaData(); 458 int columnCount = rsmd.getColumnCount(); 459 List<String> foundColumns = new ArrayList<>(columnCount); 460 461 for (int j = 1; j <= columnCount; j++) { 462 463 String columnName = rsmd.getColumnLabel(j); 464 PropertyInfo columnProperty = reader.getPropertyInfo(columnName, properties); 465 //ColumnInfo columnInfo = getMetaData().get 466 if (columnProperty != null) { 467 foundColumns.add(columnName); 468 } 469 } 470 471 // This tests for when a user writes their own SQL and forgets a column. 472 if (foundColumns.size() < properties.keySet().size()) { 473 474 Set<String> missing = new LinkedHashSet<>(columnCount); 475 missing.addAll(properties.keySet()); 476 foundColumns.forEach(missing::remove); 477 478 throw new PersismException(Message.ObjectNotProperlyInitializedByQuery.message(objectClass, foundColumns, missing)); 479 } 480 481 } 482 483 /* ****************************** Write methods ****************************************/ 484 485 /** 486 * Updates the data object in the database. 487 * 488 * @param object data object to update. 489 * @return Result object containing rows changed (usually 1 to indicate rows changed via JDBC) and the data object itself which may have been changed. 490 * @throws PersismException Indicating the upcoming robot uprising. 491 */ 492 public <T> Result<T> update(T object) throws PersismException { 493 Class<?> objectClass = object.getClass(); 494 495 helper.checkIfOkForWriteOperation(objectClass, "UPDATE"); 496 497 List<String> primaryKeys = metaData.getPrimaryKeys(objectClass, connection); 498 if (primaryKeys.size() == 0) { 499 throw new PersismException(Message.TableHasNoPrimaryKeys.message("UPDATE", metaData.getTableInfo(objectClass).name())); 500 } 501 502 PreparedStatement st = null; 503 try { 504 505 String updateStatement = null; 506 try { 507 updateStatement = metaData.getUpdateStatement(object, connection); 508 log.debug(updateStatement); 509 } catch (NoChangesDetectedForUpdateException e) { 510 log.info("No properties changed. No update required for Object: " + object + " class: " + objectClass.getName()); 511 return new Result<>(0, object); 512 } 513 514 st = connection.prepareStatement(updateStatement); 515 516 // These keys should always be in sorted order. 517 Map<String, PropertyInfo> allProperties = metaData.getTableColumnsPropertyInfo(objectClass, connection); 518 Map<String, PropertyInfo> changedProperties; 519 if (object instanceof Persistable<?> pojo) { 520 changedProperties = metaData.getChangedProperties(pojo, connection); 521 } else { 522 changedProperties = allProperties; 523 } 524 525 List<Object> params = new ArrayList<>(primaryKeys.size()); 526 List<ColumnInfo> columnInfos = new ArrayList<>(changedProperties.size()); 527 528 Map<String, ColumnInfo> columns = metaData.getColumns(objectClass, connection); 529 530 for (String column : changedProperties.keySet()) { 531 ColumnInfo columnInfo = columns.get(column); 532 533 if (primaryKeys.contains(column)) { 534 log.debug("Session update: skipping column %s", column); 535 } else { 536 Object value = allProperties.get(column).getValue(object); 537 params.add(value); 538 columnInfos.add(columnInfo); 539 } 540 } 541 542 for (String column : primaryKeys) { 543 params.add(allProperties.get(column).getValue(object)); 544 columnInfos.add(metaData.getColumns(objectClass, connection).get(column)); 545 } 546 assert params.size() == columnInfos.size(); 547 for (int j = 0; j < params.size(); j++) { 548 if (params.get(j) != null) { 549 params.set(j, converter.convert(params.get(j), columnInfos.get(j).columnType.getJavaType(), columnInfos.get(j).columnName)); 550 } 551 } 552 if (sqllog.isDebugEnabled()) { 553 sqllog.debug("%s params: %s", updateStatement, params); 554 } 555 helper.setParameters(st, params.toArray()); 556 int ret = st.executeUpdate(); 557 558 if (object instanceof Persistable<?> pojo) { 559 // Save this object state to later detect changed properties 560 pojo.saveReadState(); 561 } 562 563 return new Result<>(ret, object); 564 565 } catch (Exception e) { 566 Util.rollback(connection); 567 throw new PersismException(e.getMessage(), e); 568 569 } finally { 570 Util.cleanup(st, null); 571 } 572 } 573 574 /** 575 * Inserts the data object in the database refreshing with autoinc and other defaults that may exist. 576 * 577 * @param object the data object to insert. 578 * @param <T> Type of the returning data object in Result. 579 * @return Result object containing rows changed (usually 1 to indicate rows changed via JDBC) and the data object itself which may have been changed by auto-inc or column defaults. 580 * @throws PersismException When planet of the apes starts happening. 581 */ 582 public <T> Result<T> insert(T object) throws PersismException { 583 Class<?> objectClass = object.getClass(); 584 585 helper.checkIfOkForWriteOperation(objectClass, "INSERT"); 586 587 String insertStatement = metaData.getInsertStatement(object, connection); 588 589 PreparedStatement st = null; 590 ResultSet rs = null; 591 592 ConnectionType connectionType = metaData.getConnectionType(); 593 594 try { 595 // These keys should always be in sorted order. 596 Map<String, PropertyInfo> properties = metaData.getTableColumnsPropertyInfo(objectClass, connection); 597 Map<String, ColumnInfo> columns = metaData.getColumns(objectClass, connection); 598 599 List<String> generatedKeys = new ArrayList<>(1); 600 for (ColumnInfo column : columns.values()) { 601 if (column.autoIncrement) { 602 generatedKeys.add(column.columnName); 603 } else if (connectionType.supportsNonAutoIncGenerated() && column.primary && column.hasDefault && properties.get(column.columnName).getValue(object) == null) { 604 generatedKeys.add(column.columnName); 605 } 606 } 607 608 if (generatedKeys.size() > 0) { 609 String[] keyArray = generatedKeys.toArray(new String[0]); 610 st = connection.prepareStatement(insertStatement, keyArray); 611 } else { 612 st = connection.prepareStatement(insertStatement); 613 } 614 615 boolean refreshAfterInsert = false; 616 617 List<Object> params = new ArrayList<>(columns.size()); 618 List<ColumnInfo> columnInfos = new ArrayList<>(columns.size()); 619 620 for (ColumnInfo columnInfo : columns.values()) { 621 622 PropertyInfo propertyInfo = properties.get(columnInfo.columnName); 623 if (propertyInfo.getter == null) { 624 throw new PersismException(Message.ClassHasNoGetterForProperty.message(objectClass, propertyInfo.propertyName)); 625 } 626 if (!columnInfo.autoIncrement) { 627 628 if (columnInfo.hasDefault) { 629 // Do not include if this column has a default and no value has been 630 // set on it's associated property. 631 if (propertyInfo.getter.getReturnType().isPrimitive()) { 632 log.warnNoDuplicates(Message.PropertyShouldBeAnObjectType.message(propertyInfo.propertyName, columnInfo.columnName, objectClass)); 633 } 634 635 if (propertyInfo.getValue(object) == null) { 636 637 if (columnInfo.primary) { 638 // This is supported with PostgreSQL/MSSQL but otherwise throw this an exception 639 if (!connectionType.supportsNonAutoIncGenerated()) { 640 throw new PersismException(Message.NonAutoIncGeneratedNotSupported.message()); 641 } 642 } 643 644 refreshAfterInsert = true; 645 continue; 646 } 647 } 648 649 // if any column is read only it usually means there's a default to read back - we don't include in the INSERT or the params. 650 if (columnInfo.readOnly) { 651 refreshAfterInsert = true; 652 } else { 653 Object value = propertyInfo.getValue(object); 654 params.add(value); 655 columnInfos.add(columnInfo); 656 } 657 } 658 } 659 660 assert params.size() == columnInfos.size(); 661 662 for (int j = 0; j < params.size(); j++) { 663 ColumnInfo columnInfo = columnInfos.get(j); 664 if (params.get(j) != null) { 665 params.set(j, converter.convert(params.get(j), columnInfo.columnType.getJavaType(), columnInfo.columnName)); 666 } 667 } 668 669 if (sqllog.isDebugEnabled()) { 670 sqllog.debug("%s params: %s", insertStatement, params); 671 } 672 673 helper.setParameters(st, params.toArray()); 674 boolean insertReturnedResults = st.execute(); 675 int rowCount = st.getUpdateCount(); 676 677 List<Object> primaryKeyValues = new ArrayList<>(); 678 if (generatedKeys.size() > 0) { 679 if (insertReturnedResults) { 680 rs = st.getResultSet(); 681 } else { 682 rs = st.getGeneratedKeys(); 683 } 684 log.debug("insert return count after insert: %s", rowCount); 685 PropertyInfo propertyInfo; 686 for (String column : generatedKeys) { 687 if (rs.next()) { 688 689 propertyInfo = properties.get(column); 690 Method setter = propertyInfo.setter; 691 Object value; 692 if (setter != null) { 693 value = helper.getTypedValueReturnedFromGeneratedKeys(setter.getParameterTypes()[0], rs); 694 if (value == null) { 695 throw new PersismException("Could not retrieve value from column " + column + " for table " + metaData.getTableInfo(objectClass)); 696 } 697 value = converter.convert(value, setter.getParameterTypes()[0], column); 698 setter.invoke(object, value); 699 } else { 700 // Set read-only property by field ONLY FOR NON-RECORDS. 701 value = helper.getTypedValueReturnedFromGeneratedKeys(propertyInfo.field.getType(), rs); 702 if (value == null) { 703 throw new PersismException("Could not retrieve value from column " + column + " for table " + metaData.getTableInfo(objectClass)); 704 } 705 value = converter.convert(value, propertyInfo.field.getType(), column); 706 if (!isRecord(objectClass)) { 707 propertyInfo.field.setAccessible(true); 708 propertyInfo.field.set(object, value); 709 propertyInfo.field.setAccessible(false); 710 log.debug("insert %s generated %s", column, value); 711 } 712 } 713 714 primaryKeyValues.add(value); 715 } 716 } 717 } 718 719 // If it's a record we can't assign the autoinc so we need a refresh 720 if (generatedKeys.size() > 0 && isRecord(objectClass)) { 721 refreshAfterInsert = true; 722 } 723 724 Object returnObject = null; 725 if (refreshAfterInsert) { 726 // these 2 fetches need a fetchAfterInsert flag 727 // Read the full object back to update any properties which had defaults 728 if (isRecord(objectClass)) { 729 SQL sql = new SQL(metaData.getDefaultSelectStatement(objectClass, connection)); 730 returnObject = fetch(objectClass, sql, params(primaryKeyValues.toArray())); 731 } else { 732 fetch(object); 733 returnObject = object; 734 } 735 } else { 736 returnObject = object; 737 } 738 739 if (object instanceof Persistable<?> pojo) { 740 // Save this object new state to later detect changed properties 741 pojo.saveReadState(); 742 } 743 744 //noinspection unchecked 745 return new Result<>(rowCount, (T) returnObject); 746 } catch (Exception e) { 747 Util.rollback(connection); 748 throw new PersismException(e.getMessage(), e); 749 } finally { 750 Util.cleanup(st, rs); 751 } 752 } 753 754 /** 755 * Deletes the data object from the database. 756 * 757 * @param object data object to delete 758 * @return Result with usually 1 to indicate rows changed via JDBC. 759 * @throws PersismException If you mistakenly pass a Class rather than a data object, or other SQL Exception. 760 */ 761 public <T> Result<T> delete(T object) throws PersismException { 762 763 // Catch if user mistakenly passes a class to this method 764 if (object instanceof java.lang.Class c) { 765 throw new PersismException(Message.DeleteExpectsInstanceOfDataObjectNotAClass.message(c.getName())); 766 } 767 768 Class<?> objectClass = object.getClass(); 769 770 helper.checkIfOkForWriteOperation(objectClass, "DELETE"); 771 772 List<String> primaryKeys = metaData.getPrimaryKeys(objectClass, connection); 773 if (primaryKeys.size() == 0) { 774 throw new PersismException(Message.TableHasNoPrimaryKeys.message("DELETE", metaData.getTableInfo(objectClass).name())); 775 } 776 777 PreparedStatement st = null; 778 try { 779 String deleteStatement = metaData.getDefaultDeleteStatement(objectClass, connection); 780 log.debug(deleteStatement); 781 782 st = connection.prepareStatement(deleteStatement); 783 784 // These keys should always be in sorted order. 785 Map<String, PropertyInfo> columns = metaData.getTableColumnsPropertyInfo(objectClass, connection); 786 787 List<Object> params = new ArrayList<>(primaryKeys.size()); 788 List<ColumnInfo> columnInfos = new ArrayList<>(columns.size()); 789 for (String column : primaryKeys) { 790 params.add(columns.get(column).getValue(object)); 791 columnInfos.add(metaData.getColumns(objectClass, connection).get(column)); 792 } 793 794 for (int j = 0; j < params.size(); j++) { 795 if (params.get(j) != null) { 796 params.set(j, converter.convert(params.get(j), columnInfos.get(j).columnType.getJavaType(), columnInfos.get(j).columnName)); 797 } 798 } 799 800 if (sqllog.isDebugEnabled()) { 801 sqllog.debug("%s params: %s", deleteStatement, params); 802 } 803 804 helper.setParameters(st, params.toArray()); 805 int rows = st.executeUpdate(); 806 return new Result<>(rows, object); 807 808 } catch (Exception e) { 809 Util.rollback(connection); 810 throw new PersismException(e.getMessage(), e); 811 812 } finally { 813 Util.cleanup(st, null); 814 } 815 } 816 817 /** 818 * Deletes data from the database based on the specified WHERE clause 819 * 820 * @param objectClass class of data object where to delete from. 821 * @param whereClause WHERE clause condition. 822 * @return int rows affected 823 * @throws PersismException If something goes wrong in the Db. 824 */ 825 public int delete(Class<?> objectClass, SQL whereClause) { 826 helper.checkIfOkForWriteOperation(objectClass, "DELETE"); 827 return delete(objectClass, whereClause, none()); 828 } 829 830 /** 831 * Deletes data from the database based on the specified primary keys provided 832 * 833 * @param objectClass class of data object where to delete from. 834 * @param primaryKeyValues primary key values 835 * @return int rows affected 836 * @throws PersismException If something goes wrong in the Db OR if you accidentally call this with 0 parameters. 837 */ 838 public int delete(Class<?> objectClass, Parameters primaryKeyValues) { 839 // delete by primary keys 840 helper.checkIfOkForWriteOperation(objectClass, "DELETE"); 841 842 if (primaryKeyValues.size() == 0) { 843 // should fail here. We don't want to accidentally delete all 844 throw new PersismException(Message.CannotDeleteWithNoPrimaryKeys.message()); 845 } 846 847 List<String> primaryKeys = metaData.getPrimaryKeys(objectClass, connection); 848 if (primaryKeys.size() == 0) { 849 throw new PersismException(Message.TableHasNoPrimaryKeys.message("DELETE", metaData.getTableInfo(objectClass))); 850 } 851 852 primaryKeyValues.areKeys = true; 853 854 String deleteStatement = metaData.getDeleteStatement(objectClass, connection) + metaData.getPrimaryInClause(objectClass, primaryKeyValues.size(), connection); 855 if (sqllog.isDebugEnabled()) { 856 sqllog.debug("%s params: %s", deleteStatement, primaryKeyValues); 857 } 858 try (PreparedStatement st = connection.prepareStatement(deleteStatement)) { 859 helper.setParameters(st, primaryKeyValues.toArray()); 860 return st.executeUpdate(); 861 } catch (SQLException e) { 862 Util.rollback(connection); 863 throw new PersismException(e.getMessage(), e); 864 } 865 } 866 867 /** 868 * Deletes data from the database based on the specified WHERE clause and parameters 869 * 870 * @param objectClass class of data object where to delete from. 871 * @param whereClause WHERE clause condition. 872 * @param parameters parameters for the WHERE clause 873 * @return int rows affected 874 * @throws PersismException If something goes wrong in the Db. 875 */ 876 public int delete(Class<?> objectClass, SQL whereClause, Parameters parameters) { 877 // delete where. 878 helper.checkIfOkForWriteOperation(objectClass, "DELETE"); 879 if (whereClause.type != SQL.SQLType.Where) { 880 throw new PersismException(Message.DeleteCanOnlyUseWhereClause.message()); 881 } 882 883 String deleteStatement = metaData.getDeleteStatement(objectClass, connection) + " " + helper.parsePropertyNames(whereClause.sql, objectClass, connection); 884 if (sqllog.isDebugEnabled()) { 885 sqllog.debug("%s params: %s", deleteStatement, parameters); 886 } 887 888 try (PreparedStatement st = connection.prepareStatement(deleteStatement)) { 889 helper.setParameters(st, parameters.toArray()); 890 return st.executeUpdate(); 891 } catch (SQLException e) { 892 Util.rollback(connection); 893 throw new PersismException(e.getMessage(), e); 894 } 895 } 896 897 /** 898 * Function block of database operations to group together in one transaction. 899 * This method will set autocommit to false then execute the function, commit and set autocommit back to true. 900 * <pre>{@code 901 * session.withTransaction(() -> { 902 * Contact contact = getContactFromSomewhere(); 903 * 904 * contact.setIdentity(randomUUID); 905 * session.insert(contact); 906 * 907 * contact.setContactName("Wilma Flintstone"); 908 * 909 * session.update(contact); 910 * session.fetch(contact); 911 * }); 912 * }</pre> 913 * 914 * @param transactionBlock Block of operations expected to run as a single transaction. 915 * @throws PersismException in case of SQLException where the transaction is rolled back. 916 */ 917 public void withTransaction(Runnable transactionBlock) { 918 try { 919 connection.setAutoCommit(false); 920 transactionBlock.run(); 921 connection.commit(); 922 } catch (Exception e) { 923 Util.rollback(connection); 924 throw new PersismException(e.getMessage(), e); 925 } finally { 926 try { 927 connection.setAutoCommit(true); 928 } catch (SQLException e) { 929 log.warn(e.getMessage()); 930 } 931 } 932 } 933 934 935 MetaData getMetaData() { 936 return metaData; 937 } 938 939 Converter getConverter() { 940 return converter; 941 } 942 943 Connection getConnection() { 944 return connection; 945 } 946 947 // this is a maybe.... 948 static synchronized void clearMetaData() { 949 log.warn("Clearing meta data"); 950 MetaData.metaData.clear(); 951 log.warn("meta data cleared"); 952 } 953 954 }