How to read nested optional object for an insert statement?
I have the following classes;
public class MyObj {
private String myField;
private MyChildObj myChild;
public (String myField, MyChildObj myChild) {
this.myField = myField;
this.myChild = myChild;
}
public String getMyField() {
return this.myField;
}
public Optinal<MyChildObj> getMyChildObj() {
return Optional.ofNullable(this.myChild);
}
}
public class MyChildObj {
private String myField2;
public (String myField2) {
this.myField2 = myField2;
}
public String getMyField2() {
return this.myField2;
}
}
And a mapper like this:
public interface MyMapper {
@Insert("INSERT INTO `Entity` VALUES(" +
"#{entity.myField}," +
"#{entity.myChild.myField2})")
public int insertEntity(@Param("entity") MyObj entity);
With this I get an error like
There is no getter for property named 'myField2' in 'class java.util.Optional'
Do I need a separate TypeHandler for this or is there another solution?
In your example, the getter name is different from the field name.
private MyChildObj myChild;
public Optional<MyChildObj> getMyChildObj() {
return Optional.ofNullable(this.myChild);
}
In this case, you can access the field directly in a mapper statement because MyBatis can access private field/method [1].
@Insert({
"INSERT INTO `Entity` VALUES(",
"#{entity.myField},",
"#{entity.myChild.myField2})"})
public int insertEntity(@Param("entity") MyObj entity);
When the getter has the same name as the field, there needs to be some extra work.
private MyChildObj myChildObj;
public Optional<MyChildObj> getMyChildObj() {
return Optional.ofNullable(this.myChildObj);
}
I'll explain the following three solutions.
<bind>
tag.ObjectWrapper
.The first one is pretty straight-forward.
Just add a private getter method...
@SuppressWarnings("unused")
private MyChildObj getMyChildObjPrivate() {
return myChildObj;
}
...and use it in a mapper statement.
@Insert({
"INSERT INTO `Entity` VALUES(",
"#{entity.myField},",
"#{entity.myChildObjPrivate.myField2})"})
public int insertEntity(@Param("entity") MyObj entity);
The second solution is a little bit verbose.
In a <bind>
tag, the expression of value
attribute is evaluated by OGNL, so you can write something like the following.
@Insert({
"<script>",
"<bind name='x'",
" value='entity.myChildObj.isEmpty() ? null :",
" entity.myChildObj.get().myField2'/>",
"INSERT INTO users VALUES(",
"#{entity.myField},",
"#{x})",
"</script>"})
int insertEntity(@Param("entity") MyObj entity);
The third solution is to create a custom ObjectWrapper
.
The following implementation adds a pseudo property orNull
to Optional
objects (note: it's not well-tested).
import java.util.List;
import java.util.Optional;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
public class MyObjectWrapperFactory implements ObjectWrapperFactory {
private static String[] pseudoProperties = new String[] {
"orNull"
};
@Override
public boolean hasWrapperFor(Object object) {
return object instanceof Optional;
}
@Override
public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
return new OptionalWrapper((Optional<?>) object);
}
private static class OptionalWrapper implements ObjectWrapper {
Optional<?> optional;
OptionalWrapper(Optional<?> optional) {
this.optional = optional;
}
@Override
public Object get(PropertyTokenizer prop) {
String name = prop.getName();
return switch (name) {
case "orNull" -> optional.orElse(null);
default -> throw new IllegalArgumentException(
"Invalid pseudo property name for Optional: '" + name
+ "'; Valid names are ['orNull'].");
};
}
@Override
public void set(PropertyTokenizer prop, Object value) {
throw new UnsupportedOperationException();
}
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
return hasPseudoProperty(name) ? name : null;
}
@Override
public String[] getGetterNames() {
return pseudoProperties;
}
@Override
public String[] getSetterNames() {
return null;
}
@Override
public Class<?> getSetterType(String name) {
return null;
}
@Override
public Class<?> getGetterType(String name) {
return null;
}
@Override
public boolean hasSetter(String name) {
return false;
}
@Override
public boolean hasGetter(String name) {
return hasPseudoProperty(name);
}
@Override
public MetaObject instantiatePropertyValue(String name, PropertyTokenizer prop, ObjectFactory objectFactory) {
throw new UnsupportedOperationException();
}
@Override
public boolean isCollection() {
return false;
}
@Override
public void add(Object element) {
throw new UnsupportedOperationException();
}
@Override
public <E> void addAll(List<E> list) {
throw new UnsupportedOperationException();
}
private static boolean hasPseudoProperty(String name) {
for (int i = 0; i < pseudoProperties.length; i++) {
if (pseudoProperties[i].equals(name)) {
return true;
}
}
return false;
}
}
}
To register the custom ObjectWrapperFactory
using MyBatis' XML configuration:
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<objectWrapperFactory type="pkg.MyObjectWrapperFactory"/>
...
</configuration>
If you are using mybatis-spring-boot, add the following line to the application.properties
:
mybatis.configuration.object-wrapper-factory=pkg.MyObjectWrapperFactory
The pseudo-property orNull
returns null
when the Optional
is empty.
@Insert({
"INSERT INTO `Entity` VALUES(",
"#{entity.myField},",
"#{entity.myChildObj.orNull.myField2})"})
public int insertEntity(@Param("entity") MyObj entity);
[1] With JPMS, you may have to open your module to MyBatis.