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 }