How to execute SQL in Mapper interface of Mybatis

Posted by verN on Fri, 08 Nov 2019 10:49:44 +0100

Preface

In many ORM frameworks, Mybatis is used by more and more Internet companies. The main reason is that Mybatis is easy to use and flexible to operate. This series is going to ask questions to learn more about Mybatis from the source layer.

Put questions to

The most common way to use Mybatis is to get a Mapper interface object, and then map it to the statement in the configuration file through the method name of the interface. The approximate code format is as follows:

public class BlogMain {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config-sourceCode.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession session = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper = session.getMapper(BlogMapper.class);
            // conventional method
            System.out.println(mapper.selectBlog(101));
            // Method of Object
            System.out.println(mapper.hashCode());
            // public default method
            System.out.println(mapper.defaultValue());
            // Methods in parent interface
            System.out.println(mapper.selectParent(101));
        } finally {
            session.close();
        }
    }

In addition to using the normal interface method selectBlog, the above methods also use totally different types: internal methods of Object, default methods of interface, and methods in the parent class. Of course, Mybatis can handle it well. Then how does Mybatis help us execute sql every time we call the interface method? Next, we will analyze it and look at it together How to deal with these special methods.

guess

Through the use of dynamic proxy, a proxy class is generated, and then mapped by the method name in Mapper and the statement name in the configuration file, and then sql is executed according to the statement type.

Analysis

First, analyze the getMapper operation, and then analyze how the relevant methods in Mapper call the relevant sql;

1. Perform getMapper analysis

In the above code, use openSession to create a DefaultSqlSession class. This class includes operations such as adding, deleting, modifying and querying sql, as well as the getMapper method:

  private final Configuration configuration;
  
  @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }

Configuration here is the key, and also a core class of Mybatis. It can be understood as a mapping class of our configuration file mybatis-config.xml. Go on:

  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

MapperRegistry is introduced here. All mappers are registered in this class and stored in the form of key value. Key corresponds to xx.xx.xxMapper, while value stores Mapper's proxy class, as shown in the MapperRegistry code

  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
  
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

It can be seen that every time getMapper gets a MapperProxyFactory class from knownMappers. As for when to add data to knownMappers, when parsing the mybatis-config.xml configuration file and the mappers tag, it is as follows:

<mappers>
    <mapper resource="mapper/BlogMapper.xml" />
</mappers>

Continue to parse the BlogMapper.xml, and use the namespace in the BlogMapper.xml as the key, as shown below:

<mapper namespace="com.mybatis.mapper.BlogMapper">
    <select id="selectBlog" parameterType="long" resultType="blog">
        select * from blog where id = #{id}
    </select>
</mapper>

namespace is required. This value is the key of knownMappers in MapperRegistry. Value is MapperProxyFactory, a proxy factory class of Mapper class. Every time getMapper is called, an instance will be newInstance. The code is as follows:

  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
   
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

It can be found that a proxy class is created through the proxy class Proxy.newProxyInstance(...) provided by the jdk. MapperProxy is set as the InvocationHandler. When instantiating MapperProxy, a methodCache object is passed in. This object is a Map, which stores the methods in each Mapper, which is defined as MapperMethod here. So far, we have understood the general process of getMapper , continue to see the execution method below;

2. Implementation method

From the above analysis, we can see that through getMapper, a dynamic proxy class of Mapper is returned, and MapperProxy is specified as the InvocationHandler, so we actually call the invoke method in MapperProxy every time we call a method:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

The above two judgments are: whether it is the method in the Object class and whether it is the default method. These two situations are also the reasons I show in the above example. It is not necessary to map the statement in xxMapper.xml, and it can directly execute the return result. The next step is to deal with the two situations. The cache used here is optimized, that is to say, if I continuously call Using the same Mapper and the following same method multiple times will not create multiple mappermethods. The main reason why cache is needed is that there are many things to initialize each instantiation of MapperMethod, as shown below:

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

SqlCommand and MethodSignature are the two main instances. These two classes roughly mean that SqlCommand saves the operation types of the method, including add, delete, modify, query, unknown, refresh and the ID of the statement in the corresponding xxMapper.xml; MethodSignature saves the signature of the method, including the return type, etc.; here we have a general understanding of the line, and the following article will continue Detailed introduction: with the above initialized parameters, you can execute the execute method that calls MapperMethod:

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

Execute different sql according to the command type of SqlCommand; process the parameters when executing, process the result set after executing, and cache the result set of course; let's have a general understanding here, and each point will be introduced separately; after executing, you can return the result;

summary

This article has roughly learned that a dynamic proxy class of the xxMapper interface is obtained through getMapper, and a new Object is obtained for each get operation. Instead of caching this class, Mybatis caches every method in the xxMapper interface. The caching method here is shared by every dynamic proxy class Object, and the proxy class is not cached. The main reason is that each class can have its own sqlSession. The other point is that Mybatis deals with two special methods: whether it is a method in the Object class and whether it is a default method. Finally, execute the relevant sql according to the statement mapped by the method name.

Sample code address

Github

Topics: Programming Mybatis xml SQL Session