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 }