javamybatismybatis-mapper

How to read nested optional object for an insert statement?


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?


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.

    1. Define a private getter.
    2. Use <bind> tag.
    3. Create a custom 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.