Holger Veltrup
Holger Veltrup

Reputation: 51

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?

Upvotes: 0

Views: 168

Answers (1)

ave
ave

Reputation: 3614

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.

Upvotes: 0

Related Questions