Moment For Technology

How does MyBatis execute a SQL statement

Posted on June 24, 2022, 2:29 a.m. by Robert Morris-Ali
Category: The back-end Tag: java

preface

Mybatis is a common ORM framework in Java development. In daily work, we are directly through Spring Boot automatic configuration, and directly use, but do not know how Mybatis is to execute a SQL statement, and this article is to uncover the mystery of Mybatis.

Based on the component

If we want to understand the execution process of Mybatis, we must first understand what are some important classes in Mybatis, what are the responsibilities of these classes?

SqlSession

We're all familiar with it. It hides the low-level details by providing the methods needed to interact with the database. Its default implementation class is DefaultSqlSession

Executor

This is the executor to which all database operations in SqlSession are delegated. It has multiple implementation classes that can use different functionality.

Configuration

It is a very important configuration class, it contains all the useful information of Mybatis, including XML configuration, dynamic SQL statements and so on, we can see this class everywhere.

MapperProxy

This is a very important proxy class, it is the proxy in Mybatis mapping SQL interface. This is the Dao interface we often write about.

The working process

Initial use

First, we need to get an SqlSessionFactory object, which is used to get the SqlSession object.

// Read the configuration
InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
Create an SqlSessionFactory object
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);
Copy the code

Once we have an SqlSessionFactory object, we can use its openSession method to get an SqlSession object.

 SqlSession sqlSession = sqlSessionFactory.openSession(true);
Copy the code

Finally, we get the Mapper through the SqlSession object so that we can get data from the database.

// Get the Mapper object
HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);
// Execute the method to get data from the database
Hero hero = mapper.selectById(1);
Copy the code

Detailed process

Obtain the MapperProxy object

Our main focus now is the getMapper method, which creates a proxy object for us that provides important support for executing SQL statements.

/ / SqlSession objects
@Override
public T T getMapper(ClassT type) {
    return configuration.getMapper(type, this);
}
Copy the code

The getMapper method delegates the Configuration object to obtain the corresponding Mapper proxy object. As mentioned before, the Configuration object contains all the important information in Mybatis, including the Mapper proxy object we need. And this information has to be done at the time of reading configuration information, namely implement the sqlSessionFactoryBuilder is. The build method.

/ / the Configuration object
public T T getMapper(ClassT type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}
Copy the code

We can see that it delegates the retrieval of the Mapper proxy object to the MapperRegistry object. In fact, it's not the Mapper proxy object we want, it's the factory of the Mapper proxy object, Mybatis uses factory mode here.

public class MapperRegistry {

  private final Configuration config;
  private finalMapClass? , MapperProxyFactory?  knownMappers =new HashMap();

  public MapperRegistry(Configuration config) {
    this.config = config;
  }

  @SuppressWarnings("unchecked")
  public T T getMapper(ClassT type, SqlSession sqlSession) {
    final MapperProxyFactoryT mapperProxyFactory = (MapperProxyFactoryT) 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); }}public T void addMapper(ClassT type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if(! loadCompleted) { knownMappers.remove(type); } } } } }Copy the code

I just kept the getMapper method and the addMapper method.

In the getMapper method, it gets the MapperProxyFactory object, and we know from the name that this is a MapperProxy object factory, but we want to get a MapperProxy object, not a factory object, Let's look at getMapper method, which through mapperProxyFactory. NewInstance to create a proxy object.

protected T newInstance(MapperProxyT mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
    final MapperProxyT mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
Copy the code

We create a MapperProxy object and use the proxy.newProxyInstance method to create a Proxy object, which is the desired result. Which object is being represented here? MapperInterface is a member variable that refers to the object to be proxied. Mybatis will generate a MapperProxyFactory object for each interface that needs to be proxied. The function of this object is to create the required proxy object.

Cache execution method

Once we get the proxy object Mapper, we can execute the methods in it.

Here's an example:

// The interface required by Myabtis
public interface HeroMapper {
    Hero selectById(Integer id);
}
Copy the code
// HeroMapper interface corresponding XML file
      
! DOCTYPEmapper
    PUBLIC "- / / mybatis.org//DTD Mapper / 3.0 / EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd"
mapper namespace="test.HeroMapper"
    select id="selectById" resultType="test.Hero"
        select * from hero where id = #{id}
    /select
/mapper
Copy the code

We execute the selectById method to get information about a user.

// Get the Mapper object
HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);
// Execute the method to get data from the database
Hero hero = mapper.selectById(1);
Copy the code

Mapper is a reference to a proxy object, and this proxy class is MapperProxy, so we mainly want to understand what the proxy class MapperProxy does.

public class MapperProxyT implements InvocationHandler.Serializable {
    
  private final SqlSession sqlSession;
  private final ClassT mapperInterface;
  private final MapMethod, MapperMethodInvoker methodCache;

  public MapperProxy(SqlSession sqlSession, ClassT mapperInterface, MapMethod, MapperMethodInvoker methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        returncachedInvoker(method).invoke(proxy, method, args, sqlSession); }}catch (Throwable t) {
      throwExceptionUtil.unwrapThrowable(t); }}private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
      return methodCache.computeIfAbsent(method, m - {
           return new PlainMethodInvoker(newMapperMethod(mapperInterface, method, sqlSession.getConfiguration())); }}private static class PlainMethodInvoker implements MapperMethodInvoker {
      private final MapperMethod mapperMethod;

      public PlainMethodInvoker(MapperMethod mapperMethod) {
          super(a);this.mapperMethod = mapperMethod;
      }

      @Override
      public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
          returnmapperMethod.execute(sqlSession, args); }}}Copy the code

Invoke (method). Invoke (proxy, method, args, sqlSession); invoke(proxy, method, args, sqlSession);

Let's start with the cachedInvoker Method, which takes a Method of type, so this Method represents the Method that we're executing heromapper.selectByid, It first gets a call from the cache to see if a method executor, PlainMethodInvoker, has previously been created for that method. This is actually just a wrapper class, which is optional and, in engineering terms, much easier to maintain. The executor has only one member object, MapperMethod, and the constructor of the MapperMethod passes HeroMapper, HeroMapper. SelectById, and Cofiguration.

With all the above steps done, we can then see that the Invoke method of PlainMethodInvoker executes, which delegates the real operation to MapperMethod, executes the Execute method under MapperMethod, This approach is the focus of this article.

Structural parameters

As you can see from the above parsing, this method is eventually executed.

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);
          if (method.returnsOptional()
               (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    return result;
  }
Copy the code

In this method, we can see some familiar keywords: Select, update, delete, insert, select, update, update, update, insert, etc. It's still back to the SqlSession object we mentioned in the beginning.

In this method, first structure parameters, namely we see convertArgsToSqlCommandParam method, its internal implementation methods to transformation parameters are as follows:

Use @param for custom naming

Amethod (@ Param int a, @ Param int b) will construct a map - [{" a ", a_arg}, {" b ", b_arg}, {" param1, "a_arg}, {" param2," b_arg}]. A and param1 are the names of parameter A, a_arg is the actual value passed.

Although there are only two parameters, there will end up being four key-value pairs in the Map because Mybatis will end up generating parameter names prefixed with param and named according to the position of the parameter.

Do not use the @ param

Amethod (int a, int b), will construct the map - [{" arg0 ", a_arg}, {" arg1, "b_arg}, {" param1," a_arg}, {" param2, "b_arg}]. Since there is no custom name for the parameter, Myabtis takes a default name for the parameter, prefixed by arg and suffixed by position.

If there is only one argument and the argument is a set, multiple key-value pairs will be stored:

Amethod (Collection a), in which case map - [{"arg0", a_arg}, {" Collection ", a_arg}] is constructed.

Amethod (List a), in which case map - [{"arg0", a_arg}, {"collection", a_arg}, {" List ", a_arg}] is constructed.

Amethod (Integer[] a), in which case map - [{"arg0", a_arg}, {"array", a_arg}] is constructed

However, if there are two parameters, it is not stored this way, but in the normal way:

Amethod (List Integer a, List Integer b) will construct a map - [{" arg0 ", a_arg}, {" arg1, "b_arg}, {" param1," a_arg}, {" param2, "b_arg}]

Amethod (List Integer a, int b) will construct a map - [{" arg0 ", a_arg}, {" arg1, "b_arg}, {" param1," a_arg}, {" param2, "b_arg}]

Objects that do not take arguments

There are two special objects in Mybatis: RowBounds and ResultHandler, which are not put into the map as arguments, but take up positions.

Amethod (int a, RowBounds rb, int b), in which case, Will construct a map - [{" arg0 ", a_arg}, {" arg2, "b_arg}, {" param1," a_arg}, {" param2, "b_arg}]

Note that the b arguments are named arg2 and param2, arg2 because it is in the third position of the argument and param2 because it is the second valid argument.

Gets the SQL object to execute

With the parameters constructed, we need to find the SQL statement to execute.

@Override
  public T T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    ListT list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size()  1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null; }}Copy the code

Although the statement here is of type String, but it is not really a SQL statement, it is a search for corresponding MapperStatement the name of the object, in our case, it is a test. HeroMapper. SelectById, Mybatis uses this name to find objects that contain SQL statements.

We trace the execution of the code and end up with the following method, which is an overloaded method with three arguments.

@Override
  public E ListE selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
    } finally{ ErrorContext.instance().reset(); }}Copy the code

In line 4, you can see that it gets a MapperStatement object from the Configuration object through statement, The MapperStatement object contains information provided by the SELECT , , DELETE , and INSERT elements. The information defined in these elements is stored in this object, for example: Sql statements, resultMap, fetchSize, and so on.

Execute SQL statement

Once the object containing the SQL statement information is retrieved, it is handed to the Execute executor object to perform the subsequent processing, known as the executor.query method.

@Override
public E ListE query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
Copy the code

Get the Sql statement that needs to be executed, and then create a cache key for level 1 caching.

@Override
public E ListE query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    / /...
    If there is data in the cache, it is returned directly from the cache, otherwise it is queried from the database
	list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
	return list;
}
Copy the code

Finally, a doQuery method is executed

@Override
public E ListE doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.query(stmt, resultHandler);
    } finally{ closeStatement(stmt); }}Copy the code

This code creates a StatementHandler handler for a Statement object. The handler is responsible for preparing a PrepareStatement object in JDBC, including: Create a PrepareStatement object, set the SQL statement to be executed, and assign values to the parameters in the SQL statement. Once this is done, it's time to get the data from the database.

@Override
public E ListE query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
}
Copy the code

The fourth line of code executes the corresponding Sql query, followed by processing the results.

conclusion

Mybatis proxies our Dao interface class through MapperProxy to help us execute predefined Sql statements and Cache corresponding execution results through Cache. Create a PrepareStatement object using StatementHandler and perform SQL operations using JDBC.

Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.