From 29ec4773349742b21b1b97868fcae2f678e3560f Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Mon, 11 Feb 2019 00:55:24 -0500 Subject: [PATCH] Start PreparedStatement binding documentation, fix some omissions/inconsistencies between QueryMapper and JdbcMapper --- .../jdbc/util/PreparedStatementUtil.java | 6 +- .../jdbc/codegen/JdbcMapperProcessor.java | 10 +- readme.md | 139 +++++++++++++++++- 3 files changed, 149 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/com/moparisthebest/jdbc/util/PreparedStatementUtil.java b/common/src/main/java/com/moparisthebest/jdbc/util/PreparedStatementUtil.java index 5b30575..f863713 100644 --- a/common/src/main/java/com/moparisthebest/jdbc/util/PreparedStatementUtil.java +++ b/common/src/main/java/com/moparisthebest/jdbc/util/PreparedStatementUtil.java @@ -25,12 +25,12 @@ public class PreparedStatementUtil { public static void setObject(final PreparedStatement ps, final int index, final Object o) throws SQLException { // we are going to put most common ones up top so it should execute faster normally - if (o == null || o instanceof String || o instanceof Number) + if (o == null || o instanceof String || o instanceof Number || o instanceof Boolean) ps.setObject(index, o); // java.util.Date support, put it in a Timestamp else if (o instanceof java.util.Date) ps.setObject(index, o.getClass().equals(java.util.Date.class) ? new java.sql.Timestamp(((java.util.Date)o).getTime()) : o); - //IFJAVA8_START// todo: other java.time types + //IFJAVA8_START// todo: other java.time types, Year, ZoneId, ZoneOffset else if (o instanceof Instant) ps.setObject(index, java.sql.Timestamp.from((Instant)o)); else if (o instanceof LocalDateTime) @@ -73,6 +73,8 @@ public class PreparedStatementUtil { ps.setArray(index, (java.sql.Array) o); else if (o instanceof Enum) ps.setObject(index, ((Enum)o).name()); + else if (o instanceof java.sql.Ref) + ps.setRef(index, (java.sql.Ref) o); else ps.setObject(index, o); // probably won't get here ever, but just in case... /* diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java index 640dd7d..22a4f40 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java @@ -65,8 +65,8 @@ public class JdbcMapperProcessor extends AbstractProcessor { return messager; } - static TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType, connectionType, jdbcMapperType, - byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType, iterableType, bindableType, calendarType, cleanerType, enumType; + static TypeMirror sqlExceptionType, stringType, numberType, booleanType, utilDateType, readerType, clobType, connectionType, jdbcMapperType, + byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, refType, collectionType, iterableType, bindableType, calendarType, cleanerType, enumType; //IFJAVA8_START static TypeMirror streamType, instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType; //IFJAVA8_END @@ -99,6 +99,7 @@ public class JdbcMapperProcessor extends AbstractProcessor { sqlExceptionType = elements.getTypeElement(SQLException.class.getCanonicalName()).asType(); stringType = elements.getTypeElement(String.class.getCanonicalName()).asType(); numberType = elements.getTypeElement(Number.class.getCanonicalName()).asType(); + booleanType = elements.getTypeElement(Boolean.class.getCanonicalName()).asType(); utilDateType = elements.getTypeElement(java.util.Date.class.getCanonicalName()).asType(); readerType = elements.getTypeElement(Reader.class.getCanonicalName()).asType(); clobType = elements.getTypeElement(Clob.class.getCanonicalName()).asType(); @@ -124,6 +125,7 @@ public class JdbcMapperProcessor extends AbstractProcessor { //byteArrayType = elements.getTypeElement(byte.class.getCanonicalName()).asType(); byteArrayType = types.getArrayType(types.getPrimitiveType(TypeKind.BYTE)); sqlArrayType = elements.getTypeElement(java.sql.Array.class.getCanonicalName()).asType(); + refType = elements.getTypeElement(java.sql.Ref.class.getCanonicalName()).asType(); collectionType = types.getDeclaredType(elements.getTypeElement(Collection.class.getCanonicalName()), types.getWildcardType(null, null)); iterableType = types.getDeclaredType(elements.getTypeElement(Iterable.class.getCanonicalName()), types.getWildcardType(null, null)); @@ -987,7 +989,7 @@ public class JdbcMapperProcessor extends AbstractProcessor { // we are going to put most common ones up top so it should execute faster normally // todo: avoid string concat here if (method == null) - if (o.getKind().isPrimitive() || types.isAssignable(o, stringType) || types.isAssignable(o, numberType)) { + if (o.getKind().isPrimitive() || types.isAssignable(o, stringType) || types.isAssignable(o, numberType) || types.isAssignable(o, booleanType)) { method = "Object"; // java.util.Date support, put it in a Timestamp } else if (types.isAssignable(o, utilDateType)) { @@ -1038,6 +1040,8 @@ public class JdbcMapperProcessor extends AbstractProcessor { } else if (types.isAssignable(o, enumType)) { method = "Object"; variableName = variableName + " == null ? null : " + variableName + ".name()"; + } else if (types.isAssignable(o, refType)) { + method = "Ref"; } else { // shouldn't get here ever, if we do the types should be more specific processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL could not properly infer PreparedStatement bind call for param", param); diff --git a/readme.md b/readme.md index 0a974e5..c6d0eda 100644 --- a/readme.md +++ b/readme.md @@ -429,6 +429,143 @@ String s = rs.getString(index); return s == null ? null : ZoneOffset.of(s); ``` +Object to Column (PreparedStatement) Mapping +------------------------ + +This explains how specific java types map to specific PreparedStatement calls, this can be different between JdbcMapper and QueryMapper because of the +different information available. With JdbcMapper we have type information regardless of the value, so a String is a String even if you send in null. With +QueryMapper if the value is null, we have no idea if that was supposed to be a Date or a String or what. + +If you are thinking 'shut up and show me the code already' refer to [PreparedStatementUtil.java](https://github.com/moparisthebest/JdbcMapper/blob/master/common/src/main/java/com/moparisthebest/jdbc/util/PreparedStatementUtil.java#L26) for the runtime mapping, and [JdbcMapperProcessor.java](https://github.com/moparisthebest/JdbcMapper/blob/master/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java#L918) for the compile-time mapping, which should end up being identical where possible. + +For the purposes of this mapping, consider 'ps' an instance of PreparedStatement, 'index' an int index of a PreparedStatement column, and 'o' as the Object being mapped to the PreparedStatement column. + +### Misc Objects +##### String / Number / Boolean / primitives +```java +ps.setObject(index, o); +``` +##### null +This only applies at runtime, in which case we don't have a type, we always have a type at compile-time. +```java +ps.setObject(index, o); +``` +##### java.lang.Enum (any enum) +```java +ps.setObject(index, o.name()); +``` +##### byte[] +```java +ps.setBlob(index, new ByteArrayInputStream(o)); +``` +##### java.sql.Ref +```java +ps.setRef(index, o); +``` +##### java.sql.Blob / java.io.InputStream +```java +ps.setBlob(index, o); +``` +##### String as Blob +Where `s` is the String, and `charset` is the character set to convert the String to bytes with, +if not provided, charset defaults to UTF-8: +```java +ps.setBlob(index, s == null ? null : new ByteArrayInputStream(s.getBytes(charset))); +``` +At runtime using QueryMapper, you signal you want this by wrapping s with `PreparedStatementUtil.wrapBlob(s)` or `PreparedStatementUtil.wrapBlob(s, charset)` +At compile-time using JdbcMapper, you signal you want this in the SQL like `{blob:s}` or `{blob:utf-8:s}` any charset supported by your java works +##### java.io.File +```java +try { + ps.setBlob(index, new FileInputStream(o)); // todo: does this close this or leak a file descriptor? +} catch (FileNotFoundException e) { + throw new SQLException("File to Blob FileNotFoundException", e); +} +``` +This will likely change in the near future to read file to byte[] and behave like byte[] from above, since we probably +can't count on the FileInputStream being properly closed... +##### java.sql.Clob / java.io.Reader +```java +ps.setClob(index, o); +``` +##### String as Clob +Where `s` is the String: +```java +ps.setClob(index, s == null ? null : new StringReader(s)); +``` +At runtime using QueryMapper, you signal you want this by wrapping s with `PreparedStatementUtil.wrapClob(s)` +At compile-time using JdbcMapper, you signal you want this in the SQL like `{clob:s}` +##### java.sql.Array +```java +ps.setRef(index, o); +``` +##### * +If nothing else fits, we call setObject and cross our fingers with QueryMapper at runtime, this is a compile-time error +with JdbcMapper. +```java +ps.setObject(index, o); +``` +### Date/Time Objects +##### exactly java.util.Date +```java +ps.setObject(index, new java.sql.Timestamp(o.getTime()); +``` +##### instanceof java.util.Date, but not exactly java.util.Date +so from stdlib this includes java.sql.Date, java.sql.Timestamp, and java.sql.Time +```java +ps.setObject(index, o); +``` +##### java.time.Instant +```java +ps.setObject(index, java.sql.Timestamp.from(o); +``` +##### java.time.LocalDateTime +```java +ps.setObject(index, java.sql.Timestamp.valueOf(o)); +``` +##### java.time.LocalDate +```java +ps.setObject(index, java.sql.Date.valueOf(o)); +``` +##### java.time.LocalTime +```java +ps.setObject(index, java.sql.Time.valueOf(o)); +``` +##### java.time.ZonedDateTime +```java +ps.setObject(index, java.sql.Timestamp.from(o.toInstant())); +``` +##### java.time.OffsetDateTime +```java +ps.setObject(index, java.sql.Timestamp.from(o.toInstant())); +``` +##### java.time.OffsetTime +```java +ps.setObject(index, java.sql.Time.valueOf(o.toLocalTime())); +``` +##### java.time.Year +done this way instead of Year.of(int) because usually int->string database coercion is allowed and the other way is not +```java +// todo +``` +##### java.time.ZoneId +```java +// todo +``` +##### java.time.ZoneOffset +```java +// todo +``` +### Special objects +##### InLists +```java +// todo +``` +##### Bindable / SqlBuilder +```java +// todo +``` + TODO ---- @@ -439,4 +576,4 @@ TODO * CompilingResultSetMapper fails on inner class like 'public static class Bla {' * Support Optional for all T instead of null * change boolean to be consistent with other primitives? - * make sure 'fallback to resultSet.toObject()' never happens at compile-time with JdbcMapper \ No newline at end of file + * make sure 'fallback to resultSet.toObject()' never happens at compile-time with JdbcMapper