Как MyBatis выполняет оператор SQL

Java
Как MyBatis выполняет оператор SQL

предисловие

Mybatis — широко используемый ORM-фреймворк в Java-разработке. В нашей повседневной работе мы все автоматически настраиваем и используем его непосредственно через Spring Boot, но мы не знаем, как Mybatis выполняет оператор SQL, и эта статья призвана раскрыть тайну Mybatis.

основные компоненты

Чтобы понять процесс выполнения Mybatis, мы должны сначала понять, какие важные классы есть в Mybatis и каковы обязанности этих классов?

SqlSession

Мы все знакомы с ним, он предоставляет методы, необходимые для взаимодействия между пользователем и базой данных, и скрывает основные детали. По умолчанию класс реализацииDefaultSqlSession

Executor

Это привод,SqlSessionЕму делегируются операции над базой данных. Он имеет несколько классов реализации, которые могут использовать разные функции.

image-20210509201403306.png

Configuration

Это очень важный класс конфигурации, он содержит всю полезную информацию Mybatis, включая конфигурацию xml, динамический оператор sql и т. д. Мы можем видеть этот класс повсюду.

MapperProxy

Это очень важный прокси-класс, который представляет собой интерфейс, отображающий SQL в Mybatis. То есть интерфейс Dao мы часто пишем.

процесс работы

первоначальное использование

Во-первых, нам нужно получитьSqlSessionFactoryобъект, целью которого является получениеSqlSessionобъект.

// 读取配置
InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 创建一个 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);

когда мы получимSqlSessionFactoryобъект, вы можете передать егоopenSessionспособ получитьSqlSessionобъект.

 SqlSession sqlSession = sqlSessionFactory.openSession(true);

Наконец, мы проходимSqlSessionОбъект получает Mapper, чтобы он мог получать данные из базы данных.

// 获取 Mapper 对象
HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);
// 执行方法,从数据库中获取数据
Hero hero = mapper.selectById(1);

Подробный процесс

Получить объект MapperProxy

Сейчас наше основное вниманиеgetMapperметод, который создает для нас прокси-объект, который обеспечивает нам важную поддержку для выполнения операторов SQL.

// SqlSession 对象
@Override
public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
}

getMapperделегировать в методеConfigurationobject для получения соответствующего прокси-объекта Mapper, как я уже говорилConfigurationОбъект содержит всю важную информацию в Mybatis, включая нужный нам прокси-объект Mapper, и эта информация дополняется при чтении конфигурационной информации, то есть выполненииsqlSessionFactoryBuilder.buildметод.

// Configuration 对象
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

Мы видим, что он делегирует операцию получения прокси-объекта Mapper наMapperRegistryПредмет (где матрешка?), этоMapperRegistryВ объекте хранится нужный нам прокси-объект Mapper. Если вы так думаете, вы ошибаетесь. На самом деле он хранит не нужный нам прокси-объект Mapper, а фабрику прокси-объекта Mapper. Здесь используется Mybatis. Factory pattern.

public class MapperRegistry {

  private final Configuration config;
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

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

  @SuppressWarnings("unchecked")
  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);
    }
  }

  public <T> void addMapper(Class<T> 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);
        }
      }
    }
  }
}

я только держуgetMapperМетоды иaddMapperметод.

существуетgetMapperметод, он получаетMapperProxyFactoryObject, по названию можно сделать вывод, что это фабрика прокси-объектов Mapper, но мы хотим получитьMapperProxyобъект, а не заводской объект, посмотримgetMapperметод, который проходитmapperProxyFactory.newInstanceдля создания прокси-объекта.

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<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}

создалMapperProxyобъект и черезProxy.newProxyInstanceметод (никто не знает, что это динамический прокси JDK), создаем прокси-объект для обработки, и этот прокси-объект и есть тот результат, который нам нужен. Какой предмет здесь не представлен? На самом деле mapperInterface — это переменная-член, которая ссылается на объект, который необходимо проксировать. И эта переменная-член фактически создаетсяMapperProxyFactoryОбъект назначен, поэтому каждый интерфейс, который нам нужно проксировать, сгенерирует для него интерфейс в Mybatis.MapperProxyFactoryОбъект, роль объекта заключается в создании необходимого прокси-объекта.

mybatis 获取代理对象时序图.jpg

Метод выполнения кэша

Когда мы получим средство сопоставления прокси-объектов, мы сможем выполнять в нем методы.

Используйте пример здесь:

// Myabtis 所需要的接口
public interface HeroMapper {
    Hero selectById(Integer id);
}
// HeroMapper 接口所对应的 xml 文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    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>

мы выступаемselectByIdметод получения информации о пользователе.

// 获取 Mapper 对象
HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);
// 执行方法,从数据库中获取数据
Hero hero = mapper.selectById(1);

Из приведенного выше анализа было известно, что преобразователь здесь является ссылкой на прокси-объект, и этот прокси-классMapperProxy, так что мы в основном должны пониматьMapperProxyЧто делает этот прокси-класс.

public class MapperProxy<T> implements InvocationHandler, Serializable {
    
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethodInvoker> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, 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 {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
      return methodCache.computeIfAbsent(method, m -> {
           return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
  }
    
  private static class PlainMethodInvoker implements MapperMethodInvoker {
      private final MapperMethod mapperMethod;

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

      @Override
      public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
          return mapperMethod.execute(sqlSession, args);
      }
  }
}

Когда прокси-объект выполняет метод, он выполняется напрямую.invoke()метод, в этом методе мы в основном смотрим на операторcachedInvoker(method).invoke(proxy, method, args, sqlSession);

давайте сначала посмотримcachedInvokerметод, параметры которогоMethodТип, поэтому этот метод представляет метод, который мы выполняемHeroMapper.selectById, он сначала получает из кеша, был ли создан исполнитель метода для метода доPlainMethodInvokerОбъект, по сути, это просто класс-обертка, необязательный, с инженерной точки зрения, с этим классом-оболочкой его будет легче поддерживать. И в этом исполнителе есть только один объект-член, и этот объект-членMapperMethod, и этоMapperMethodКонструктор должен пройтиHeroMapper,HeroMapper.selectById,Cofigurationэти три параметра.

После того, как вышеуказанные шаги выполнены, мы можем увидеть выполнениеPlainMethodInvokerизinvokeметод, который, в свою очередь, делегирует реальную операциюMapperMethod,воплощать в жизньMapperMethodвнизexecuteметод, которому посвящена данная статья.

Mybatis 代理对象执行过程.jpg

Строительные параметры

Как видно из вышеприведенного анализа, в итоге этот метод будет выполнен.

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;
  }

В этом методе мы можем увидеть несколько знакомых ключевых слов: select, update, delete, insert. Это для того, чтобы найти метод выполнения. Поскольку мы являемся оператором выбора, ветвь пойдет на выбор и в конечном итоге будет выполнена дляsqlSession.selectOneметода, так что в итоге я сэкономил много времени, и все же вернулся к тому, о чем мы упоминали в началеSqlSessionв объекте.

В этом методе сначала строятся параметры, что мы и видимconvertArgsToSqlCommandParamметод, его внутренняя реализация заключается в преобразовании параметров следующим образом:

Пользовательское именование с использованием @param

amethod(@Param int a, @Param int b)построит map -> [{"a", a_arg}, {"b", b_arg}, {"param1", a_arg}, {"param2", b_arg}], a и param1 - имена параметров a, a_arg — фактическое переданное значение.

Хотя есть только два параметра, в конце на карте будет четыре пары ключ-значение, потому что Mybatis, наконец, генерирует имя параметра с префиксом param, и имя именуется в соответствии с позицией параметра.

не используйте @параметр

amethod(int a, int b), он создаст карту -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}], потому что для параметров нет специального именования , поэтому Myabtis берет для параметра имя по умолчанию с префиксом arg и суффиксом position.

Когда есть только один параметр, и этот параметр задан, будет сохранено несколько пар ключ-значение:

amethod(Collection<Integer> a), в этом случае создается карта -> [{"arg0", a_arg}, {"коллекция", a_arg}]

amethod(List<Integer> a), в этом случае создаст карту -> [{"arg0", a_arg}, {"коллекция", a_arg}, {"список", a_arg}]

amethod(Integer[] a), в этом случае создается map -> [{"arg0", a_arg}, {"array", a_arg}]

Однако если параметров два, то сохраняется не так, а обычным образом:

amethod(List<Integer> a,List<Integer> b)создаст карту -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}]

amethod(List<Integer> a,int b)создаст карту -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}]

объект, который не будет использоваться в качестве параметра

В Mybatis есть два особых объекта:RowBounds,ResultHandler, эти два объекта не будут помещены на карту, если они используются в качестве параметров, но будут занимать позиции.

amethod(int a,RowBounds rb, int b), в этом случае создаст карту -> [{"arg0", a_arg}, {"arg2", b_arg}, {"param1", a_arg}, {"param2", b_arg}]

Обратите внимание, что параметры b называются arg2 и param2 соответственно, arg2, потому что его позиция находится на третьем месте параметра, и param2, потому что это второй допустимый параметр.

Получить объект SQL, который необходимо выполнить

После завершения построения параметра нам нужно найти оператор SQL, который необходимо выполнить.

@Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> 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;
    }
  }

Заявление здесьStringтип, но это не настоящий оператор SQL, это поиск соответствующегоMapperStatementИмя объекта, в нашем случае это былоtest.HeroMapper.selectById, Mybatis может найти объекты, содержащие операторы SQL, по этому имени.

Мы отслеживаем выполнение кода и получаем следующий метод, который представляет собой перегруженный метод с тремя параметрами.

@Override
  public <E> List<E> 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();
    }
  }

В четвертой строке кода видно, что он передает оператор изConfigurationполученный от объектаMapperStatementобъект,MapperStatementИнформация, содержащаяся в объекте, определяется<select>,<update>,<delete>,<insert>Информация, которую мы определяем в этих элементах, будет храниться в объекте, например: инструкция Sql, resultMap, fetchSize и так далее.

Выполнить оператор SQL

После получения объекта, содержащего информацию оператора SQL, он будет переданExecuteОбъект-исполнитель выполняет последующую обработку, т. е.executor.queryметод.

@Override
public <E> List<E> 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);
}

Получите оператор Sql, который необходимо выполнить, а затем создайте ключ, используемый кешем для кеша первого уровня.

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // ....
    // 跟缓存有关,如果缓存中存在数据,则直接从缓存中返回,否则从数据库中查询
	list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
	return list;
}

в конечном итоге будет выполнятьсяdoQueryметод

@Override
public <E> List<E> 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);
    }
}

Этот код создаетStatementобработчик объектаStatementHandler, основной задачей этого процессора является выполнение JDBCPrepareStatementНекоторые приготовления объектов, в том числе: созданиеPrepareStatementObject, установите оператор sql для выполнения и присвойте значения параметрам в операторе sql. После выполнения этих задач пришло время получить данные из базы данных.

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
}

Четвертая строка кода выполняет соответствующий запрос Sql, а затем обрабатывает результаты.

执行 sql 语句时序图.jpg

Суммировать

Mybatis отMapperProxyПроксируйте наш класс интерфейса Dao, чтобы помочь нам выполнять предопределенные операторы Sql, кэшировать соответствующие результаты выполнения через Cache и передаватьStatementHandlerСоздайтеPrepareStatementОбъект для выполнения операций SQL через jdbc.