Start PreparedStatement binding documentation, fix some omissions/inconsistencies between QueryMapper and JdbcMapper

This commit is contained in:
Travis Burtrum 2019-02-11 00:55:24 -05:00
parent 73729f5622
commit 29ec477334
3 changed files with 149 additions and 6 deletions

View File

@ -25,12 +25,12 @@ public class PreparedStatementUtil {
public static void setObject(final PreparedStatement ps, final int index, final Object o) throws SQLException { 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 // 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); ps.setObject(index, o);
// java.util.Date support, put it in a Timestamp // java.util.Date support, put it in a Timestamp
else if (o instanceof java.util.Date) 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); 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) else if (o instanceof Instant)
ps.setObject(index, java.sql.Timestamp.from((Instant)o)); ps.setObject(index, java.sql.Timestamp.from((Instant)o));
else if (o instanceof LocalDateTime) else if (o instanceof LocalDateTime)
@ -73,6 +73,8 @@ public class PreparedStatementUtil {
ps.setArray(index, (java.sql.Array) o); ps.setArray(index, (java.sql.Array) o);
else if (o instanceof Enum) else if (o instanceof Enum)
ps.setObject(index, ((Enum)o).name()); ps.setObject(index, ((Enum)o).name());
else if (o instanceof java.sql.Ref)
ps.setRef(index, (java.sql.Ref) o);
else else
ps.setObject(index, o); // probably won't get here ever, but just in case... ps.setObject(index, o); // probably won't get here ever, but just in case...
/* /*

View File

@ -65,8 +65,8 @@ public class JdbcMapperProcessor extends AbstractProcessor {
return messager; return messager;
} }
static TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType, connectionType, jdbcMapperType, static TypeMirror sqlExceptionType, stringType, numberType, booleanType, utilDateType, readerType, clobType, connectionType, jdbcMapperType,
byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType, iterableType, bindableType, calendarType, cleanerType, enumType; byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, refType, collectionType, iterableType, bindableType, calendarType, cleanerType, enumType;
//IFJAVA8_START //IFJAVA8_START
static TypeMirror streamType, instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType; static TypeMirror streamType, instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType;
//IFJAVA8_END //IFJAVA8_END
@ -99,6 +99,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
sqlExceptionType = elements.getTypeElement(SQLException.class.getCanonicalName()).asType(); sqlExceptionType = elements.getTypeElement(SQLException.class.getCanonicalName()).asType();
stringType = elements.getTypeElement(String.class.getCanonicalName()).asType(); stringType = elements.getTypeElement(String.class.getCanonicalName()).asType();
numberType = elements.getTypeElement(Number.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(); utilDateType = elements.getTypeElement(java.util.Date.class.getCanonicalName()).asType();
readerType = elements.getTypeElement(Reader.class.getCanonicalName()).asType(); readerType = elements.getTypeElement(Reader.class.getCanonicalName()).asType();
clobType = elements.getTypeElement(Clob.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 = elements.getTypeElement(byte.class.getCanonicalName()).asType();
byteArrayType = types.getArrayType(types.getPrimitiveType(TypeKind.BYTE)); byteArrayType = types.getArrayType(types.getPrimitiveType(TypeKind.BYTE));
sqlArrayType = elements.getTypeElement(java.sql.Array.class.getCanonicalName()).asType(); 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)); collectionType = types.getDeclaredType(elements.getTypeElement(Collection.class.getCanonicalName()), types.getWildcardType(null, null));
iterableType = types.getDeclaredType(elements.getTypeElement(Iterable.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 // we are going to put most common ones up top so it should execute faster normally
// todo: avoid string concat here // todo: avoid string concat here
if (method == null) 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"; method = "Object";
// java.util.Date support, put it in a Timestamp // java.util.Date support, put it in a Timestamp
} else if (types.isAssignable(o, utilDateType)) { } else if (types.isAssignable(o, utilDateType)) {
@ -1038,6 +1040,8 @@ public class JdbcMapperProcessor extends AbstractProcessor {
} else if (types.isAssignable(o, enumType)) { } else if (types.isAssignable(o, enumType)) {
method = "Object"; method = "Object";
variableName = variableName + " == null ? null : " + variableName + ".name()"; variableName = variableName + " == null ? null : " + variableName + ".name()";
} else if (types.isAssignable(o, refType)) {
method = "Ref";
} else { } else {
// shouldn't get here ever, if we do the types should be more specific // 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); processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL could not properly infer PreparedStatement bind call for param", param);

137
readme.md
View File

@ -429,6 +429,143 @@ String s = rs.getString(index);
return s == null ? null : ZoneOffset.of(s); 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 TODO
---- ----