предисловие
Mybatis — широко используемый ORM-фреймворк в Java-разработке. В нашей повседневной работе мы все автоматически настраиваем и используем его непосредственно через Spring Boot, но мы не знаем, как Mybatis выполняет оператор SQL, и эта статья призвана раскрыть тайну Mybatis.
основные компоненты
Чтобы понять процесс выполнения Mybatis, мы должны сначала понять, какие важные классы есть в Mybatis и каковы обязанности этих классов?
SqlSession
Мы все знакомы с ним, он предоставляет методы, необходимые для взаимодействия между пользователем и базой данных, и скрывает основные детали. По умолчанию класс реализацииDefaultSqlSession
Executor
Это привод,SqlSession
Ему делегируются операции над базой данных. Он имеет несколько классов реализации, которые могут использовать разные функции.
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
делегировать в методеConfiguration
object для получения соответствующего прокси-объекта 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
метод, он получаетMapperProxyFactory
Object, по названию можно сделать вывод, что это фабрика прокси-объектов 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
Объект, роль объекта заключается в создании необходимого прокси-объекта.
Метод выполнения кэша
Когда мы получим средство сопоставления прокси-объектов, мы сможем выполнять в нем методы.
Используйте пример здесь:
// 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
метод, которому посвящена данная статья.
Строительные параметры
Как видно из вышеприведенного анализа, в итоге этот метод будет выполнен.
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
Некоторые приготовления объектов, в том числе: созданиеPrepareStatement
Object, установите оператор 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, а затем обрабатывает результаты.
Суммировать
Mybatis отMapperProxy
Проксируйте наш класс интерфейса Dao, чтобы помочь нам выполнять предопределенные операторы Sql, кэшировать соответствующие результаты выполнения через Cache и передаватьStatementHandler
СоздайтеPrepareStatement
Объект для выполнения операций SQL через jdbc.