Preparation interview diary (4.3) - (framework. Mybatis)

Posted by subodh_dg on Sat, 12 Feb 2022 06:47:59 +0100

I am a graduate of 21 years with one year's work experience. My resume and professional skills are as follows. Now I review and prepare for the interview according to my resume and knowledge.

Record date: January 15, 2022

Most knowledge points are only briefly introduced, and the specific contents are reviewed in detail according to the recommended blog links.

Framework principle - Mybatis

It is recommended to read the interview article [recommended]: Summary of MyBatis interview questions

Recommended reading function articles: MyBatis details

It is recommended to read the source code analysis article: Interpretation of mybatis source code of mybatis series

Recommended articles by Ji Hui: Interview question: how does the DAO interface in mybatis establish a relationship with SQL in XML files?

Other recommendations: Collation of Mybatis knowledge points

concept

Mybatis official website: Mybatis – MyBatis 3 | getting started

brief introduction

Mybatis is a semi ORM framework, which encapsulates JDBC internally. During development, you only need to pay attention to the SQL statement itself, and you don't need to spend energy dealing with the complicated processes such as loading drivers, creating connections, creating statements and so on.

MyBatis can use XML or annotations to configure and map native information, map POJO s to records in the database, and avoid almost all JDBC code, manually setting parameters and obtaining result sets.

Various statements to be executed are configured through xml files or annotations, and the final executed sql statements are generated through the mapping between java objects and the dynamic parameters of sql in the statement. Finally, the MySQL framework executes sql and maps the results into java objects and returns them.

effect

Help programmers store data into the database.

Traditional JDBC code is too complex. Simplification, framework, automation.

advantage

  1. Programming based on SQL statement will not have any impact on the existing design of application program or database, and the coupling between SQL and program code will be removed for unified management; Provide XML tags, support the writing of dynamic SQL statements, and have high reusability.
  2. Compared with JDBC, it reduces the amount of code by more than 50%, eliminates a large number of redundant codes in JDBC, and does not need manual switch connection;
  3. It is well compatible with various databases (because MyBatis uses JDBC to connect to the database, so as long as the database supported by JDBC is supported by MyBatis).
  4. It can integrate well with Spring.
  5. Provide a mapping label to support the mapping between the object and the ORM field of the database; Provide object relationship mapping labels to support the maintenance of object relationship components.

shortcoming

  1. The workload of writing SQL statements is large, especially when there are many fields and associated tables, which has certain requirements for developers to write SQL statements.
  2. SQL statements depend on the database, resulting in poor portability of the database, and the database cannot be replaced at will.

Mybatis configuration file [readable]

Those who are not familiar with the configuration file can read it.

Reference blog link: [MyBatis] Chapter 2: detailed explanation of MyBatis xml configuration file

When using the mybatis framework, first import its corresponding jar package and configure it accordingly, so you have to understand each parameter of the configuration file. The structure of a complete mybatis configuration file is as follows:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- Root element of configuration file -->
<configuration>
    <!-- Attributes: defining configuration externalities -->
    <properties></properties>
    <!-- Settings: defining mybatis Some global settings for -->
    <settings>
       <!-- Specific parameter name and parameter value -->
       <setting name="" value=""/> 
    </settings>
    <!-- Type name: defines aliases for some classes -->
    <typeAliases></typeAliases>
    <!-- Processor types: Definitions Java Conversion relationship between type and data type in database -->
    <typeHandlers></typeHandlers>
    <!-- Object factory -->
    <objectFactory type=""></objectFactory>
    <!-- plug-in unit: mybatis Plug in for,The plug-in can be modified mybatis Internal operation rules of -->
    <plugins>
       <plugin interceptor=""></plugin>
    </plugins>
    <!-- Environments: configuring mybatis Environment -->
    <environments default="">
       <!-- Environment variables: multiple environment variables can be configured. For example, when using multiple data sources, multiple environment variables need to be configured -->
       <environment id="">
          <!-- Transaction manager -->
          <transactionManager type=""></transactionManager>
          <!-- data source -->
          <dataSource type=""></dataSource>
       </environment> 
    </environments>
    <!-- Database vendor identification -->
    <databaseIdProvider type=""></databaseIdProvider>
    <!-- Mapper: Specifies the mapping file or mapping class -->
    <mappers></mappers>
</configuration>

properties

The properties element is mainly used to define configuration externalities, such as database connection properties. These properties are externally configurable and dynamically replaceable. They can be configured in a typical Java property file or passed through the child elements of the properties element. For example:

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="root"/>
  <property name="password" value="123456"/>
</properties>

The attributes can be used in the whole configuration file to replace the attribute values that need to be dynamically configured. For example, an example used in a data source:

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

The username and password in this example will be replaced by the corresponding values set in the properties element. The driver and url properties will be defined by config Replace with the corresponding value in the properties file. This provides many flexible options for configuration. Properties can also be passed to sqlsessionbuilder In the build () method. For example:

SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, props);
 // or
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment, props);

However, this also involves the issue of priority. If the attributes are configured in more than one place, mybatis will load them in the following order:

  1. The properties specified in the properties element body are read first.
  2. Then read the property file under the classpath according to the resource attribute in the properties element or read the property file according to the path specified by the url attribute, and overwrite the read property with the same name.
  3. Finally, read the attribute passed as a method parameter and overwrite the read attribute with the same name.

Therefore, the attributes passed through the method parameters have the highest priority, followed by the configuration file specified in the resource/url attribute, and the attribute specified in the properties attribute has the lowest priority.

settings

Setting is to specify some global configuration properties of MyBatis, which is a very important adjustment setting in MyBatis. They will change the runtime behavior of MyBatis, so we need to clearly know the role and default values of these properties.

Set parametersdescribeEffective valueDefault value
cacheEnabledThis configuration affects the global switch of the cache configured in all mapperstrue ,falsetrue
lazyLoadingEnabledGlobal switch for delayed loading. When turned on, all associated objects are loaded late. In a specific association relationship, the switch state of the item can be overridden by setting the fetchType propertytrue,falsefalse
aggressiveLazyLoadingWhen enabled, calling any delay attribute will make the object with delay load attribute load completely; Conversely, each attribute will be loaded on demand.true,falsetrue
multipleResultSetsEnabledWhether to allow a single statement to return multiple result sets (compatible driver is required).true,flasetrue
useColumnLabelUse column labels instead of column names. Different drivers will have different performances in this aspect. For details, please refer to the relevant driver documents or observe the results of the drivers used by testing these two different modes.true,falsetrue
useGeneratedKeysJDBC is allowed to support automatic generation of primary keys, which requires driver compatibility. If it is set to true, this setting forces the automatic generation of primary keys. Although some drivers are incompatible, they can still work normally (such as Derby).true,falsefalse
autoMappingBehaviorSpecify how MyBatis should automatically map columns to fields or attributes. NONE means to cancel automatic mapping; PARTIAL automatically maps only result sets that do not have nested result set mappings defined. FULL automatically maps any complex result set (whether nested or not).NONE, PARTIAL, FULLPARTIAL
defaultExecutorTypeConfigure the default actuator. SIMPLE is an ordinary actuator; REUSE executor will REUSE prepared statements; The BATCH executor reuses the statements and performs BATCH updates.SIMPLE REUSE BATCHSIMPLE
defaultStatementTimeoutSet the timeout, which determines the number of seconds the driver waits for a response from the database.Any positive integerNot Set (null)
defaultFetchSizeSets the driver a hint as to control fetching size for return results. This parameter value can be override by a query setting.Any positive integerNot Set (null)
safeRowBoundsEnabledAllows you to use RowBounds in nested statements.true,falsefalse
mapUnderscoreToCamelCaseWhether to enable automatic hump naming rule (camel case) mapping, that is, from the classic database column name a_ A similar mapping from column to the classic Java property name aColumn.true,falsefalse
localCacheScopeMyBatis uses the Local Cache mechanism to prevent circular references and accelerate repeated nested queries. The default value is SESSION, in which case all queries executed in a SESSION are cached. If the value is set to state, the local SESSION is only used for STATEMENT execution, and different calls to the same SqlSession will not share data.SESSION,STATEMENTSESSION
jdbcTypeForNullSpecify a JDBC type for a NULL value when no specific JDBC type is provided for the parameter. Some drivers need to specify the JDBC type of the column. In most cases, they can directly use the general type, such as NULL, VARCHAR or OTHER.JdbcType enumeration. Most common are: NULL, VARCHAR and OTHEROTHER
lazyLoadTriggerMethodsSpecifies which object's method triggers a deferred load.A method name list separated by commasequals,clone,hashCode,toString
defaultScriptingLanguageSpecifies the default language for dynamic SQL generation.A type alias or fully qualified class name.org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver
callSettersOnNullsSpecifies whether to call the setter (put in case of map object) method of the mapping object when the value in the result set is null Keyset () is useful when initializing with a dependency or null value. Note that basic types (int, boolean, etc.) cannot be set to null.true,falsefalse
logPrefixSpecifies the prefix that MyBatis adds to the log name.Any StringNot set
logImplSpecify the specific implementation of the log used by MyBatis. If it is not specified, it will be found automatically.SLF4J,LOG4J,LOG4J2,JDK_LOGGING,COMMONS_LOGGING,STDOUT_LOGGING,NO_LOGGINGNot set
proxyFactorySpecifies the proxy tool used by Mybatis to create objects with deferred loading capability.CGLIB,JAVASSISTJAVASSIST (MyBatis 3.3 or above)

An example of a complete settings element is as follows:

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
  <setting name="useColumnLabel" value="true"/>
  <setting name="useGeneratedKeys" value="false"/>
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <setting name="mapUnderscoreToCamelCase" value="false"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

typeAliases

A type alias is a short name for a Java type. It is only related to xml configuration. Its significance is only to reduce the redundancy of class fully qualified names, such as:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
  <typeAlias alias="Post" type="domain.blog.Post"/>
  <typeAlias alias="Section" type="domain.blog.Section"/>
  <typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

When configured in this way, blog can be used in any domain blog. Blog place.

You can also specify a package name. MyBatis will search for the required JavaBean s under the package name, such as:

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

Each one is in the package domain JavaBean s in blogs, without annotations, will use the initial lowercase unrestricted class name of the Bean as its alias. For example, domain blog. The alias of author is author; If there is an annotation, the alias is the annotation value. Take the following example:

@Alias("author")
public class Author {
	...
}

Corresponding type aliases have been built in for many common Java types. They are case insensitive. It should be noted that there are special treatments caused by duplicate basic type names.

aliasType of mapping
_bytebyte
_longlong
_shortshort
_intint
_integerint
_doubledouble
_floatfloat
__booleanboolean
stringString
byte ByteString
longLong
shortShort
intInteger
integerInteger
doubleDouble
floatFloat
booleanBoolean
dateDate
decimalBigDecimal
bigdecimalBigDecimal
objectObject
mapMap
hashmapHashMap
listList
arraylistArrayList
collectionCollection
iteratorIterator

typeHandlers

Whether MyBatis sets a parameter in the PreparedStatement or takes a value from the result set, it will use the type processor to convert the obtained value into Java type in an appropriate way. The following table describes some of the default processor types.

Type processorJava typeJDBC type
BooleanTypeHandlerjava.lang.Boolean, booleanDatabase compatible BOOLEAN
ByteTypeHandlerjava.lang.Byte, byteDatabase compatible NUMERIC or BYTE
ShortTypeHandlerjava.lang.Short, shortDatabase compatible NUMERIC or SHORT INTEGER
IntegerTypeHandlerjava.lang.Integer, intDatabase compatible NUMERIC or INTEGER
LongTypeHandlerjava.lang.Long, longDatabase compatible NUMERIC or LONG INTEGER
FloatTypeHandlerjava.lang.Float, floatDatabase compatible NUMERIC or FLOAT
DoubleTypeHandlerjava.lang.Double, doubleDatabase compatible NUMERIC or DOUBLE
BigDecimalTypeHandlerjava.math.BigDecimalDatabase compatible NUMERIC or DECIMAL
StringTypeHandlerjava.lang.StringCHAR, VARCHAR
ClobTypeHandlerjava.lang.StringCLOB, LONGVARCHAR
NStringTypeHandlerjava.lang.StringNVARCHAR, NCHAR
NClobTypeHandlerjava.lang.StringNCLOB
ByteArrayTypeHandlerbyte[]Database compatible byte stream type
BlobTypeHandlerbyte[]BLOB, LONGVARBINARY
DateTypeHandlerjava.util.DateTIMESTAMP
DateOnlyTypeHandlerjava.util.DateDATE
TimeOnlyTypeHandlerjava.util.DateTIME
SqlTimestampTypeHandlerjava.sql.TimestampTIMESTAMP
SqlDateTypeHandlerjava.sql.DateDATE
SqlTimeTypeHandlerjava.sql.TimeTIME
ObjectTypeHandlerAnyOTHER or unspecified type
EnumTypeHandlerEnumeration TypeVARCHAR - any compatible string type that stores the name of the enumeration (not the index)
EnumOrdinalTypeHandlerEnumeration TypeAny compatible NUMERIC or DOUBLE type that stores the index (not the name) of the enumeration.

You can override the type processor or create your own type processor to handle unsupported or nonstandard types. The specific approach is to realize org. Org apache. ibatis. type. Typehandler interface, or inherit a convenient class org apache. ibatis. type. Basetypehandler, which can then be optionally mapped to a JDBC type. For example:

// ExampleTypeHandler.java
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {
 
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, parameter);
    }
 
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException
    {
        return rs.getString(columnName);
    }
 
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException
    {
        return rs.getString(columnIndex);
    }
 
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return cs.getString(columnIndex);
    }
    
}

In addition, you need to add:

<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

Using this type processor will override the existing type processors that handle Java's String type properties, VARCHAR parameters and results. Note that MyBatis does not snoop on the database meta information to determine which type to use, so it must indicate the VARCHAR type field in the parameter and result mapping to bind it to the correct type processor. This is because MyBatis does not know the data type until the statement is executed.

Through the generic type of type processor, MyBatis can know the Java type of this type processor, but this behavior can specify the associated JDBC type in two ways:

  • Add a javaType attribute on the typeHandler element (for example, javaType = "String");
  • Add an @ MappedTypes annotation on the TypeHandler class to specify the list of Java types associated with it. If it is also specified in the javaType attribute, the annotation method will be ignored.

Finally, you can also let MyBatis find the type processor:

<!-- mybatis-config.xml -->
<typeHandlers>
  <package name="org.mybatis.example"/>
</typeHandlers>

Note that when using the auto discovery function, the JDBC type can only be specified by annotation.

You can create a generic type processor that can handle more than one class. To achieve this goal, you need to add a constructor that receives this class as a parameter, so that MyBatis will pass in a specific class when constructing a type processor.

//GenericTypeHandler.java
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {
    private Class<E> type;
    public GenericTypeHandler(Class<E> type) {
        if (type == null) 
            throw new IllegalArgumentException("Type argument cannot be null");
        this.type = type;
    }
  ...
}

EnumTypeHandler and EnumOrdinalTypeHandler are both generic type handlers, which will be discussed in detail in the next section.

Handle enumeration types

If you want to map Enum type Enum, you need to choose one of EnumTypeHandler or EnumOrdinalTypeHandler to use

For example, we want to use the rounding mode when storing approximations. By default, MyBatis will use EnumTypeHandler to convert Enum value to corresponding name.

Note that EnumTypeHandler is special in a sense. Other processors only target a specific class. Unlike it, it will handle any class that inherits Enum.

However, we may not want to store names. Instead, our DBA will insist on using integer value code. It's just as easy; Add EnumOrdinalTypeHandler to typeHandlers in the configuration file, so that each RoundingMode will be mapped into the corresponding integer through their ordinal value.

<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>
</typeHandlers>

But how can you map the same Enum to both string and integer?

The auto mapper will automatically select EnumOrdinalTypeHandler for processing, so if we want to use ordinary EnumTypeHandler, we have to set the type processor to be used for those SQL statements.

<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
 
<mapper namespace="org.apache.ibatis.submitted.rounding.Mapper">
	<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap">
		<id column="id" property="id"/>
		<result column="name" property="name"/>
		<result column="funkyNumber" property="funkyNumber"/>
		<result column="roundingMode" property="roundingMode"/>
	</resultMap>
 
 
	<select id="getUser" resultMap="usermap">
		select * from users
	</select>
	<insert id="insert">
        insert into users (id, name, funkyNumber, roundingMode)
        values
	    (#{id},#{name},#{funkyNumber},#{roundingMode})
	</insert>
		
	<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2">
		<id column="id" property="id"/>
		<result column="name" property="name"/>
		<result column="funkyNumber" property="funkyNumber"/>
		<result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
	</resultMap>
	<select id="getUser2" resultMap="usermap2">
		select * from users2
	</select>
	<insert id="insert2">
        insert into users2 (id, name, funkyNumber, roundingMode) values(
	    	#{id}, #{name}, #{funkyNumber}, #{roundingMode, typeHandler=
	    org.apache.ibatis.type.EnumTypeHandler})
	</insert>
 
 
</mapper>

Note that the select statement here forces the use of resultMap instead of resultType.

objectFactory

Every time MyBatis creates a new instance of the result object, it uses an object factory instance to complete it. The default object factory only needs to instantiate the target class, either through the default constructor or through the parameter constructor when the parameter mapping exists. If you want to override the behavior of the object factory, you can create your own object factory, for example:

//ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {
    public Object create(Class type) {
        return super.create(type);
    }
    public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
        return super.create(type, constructorArgTypes,constructorArgs);
    }
    
    public void setProperties(Properties properties) {
        super.setProperties(properties);
    }
    public <T> boolean isCollection(Class<T> type) {
        return Collection.class.isAssignableFrom(type);
    }
}
<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>

The ObjectFactory interface is very simple. It contains two creation methods. One is to deal with the default construction method, and the other is to deal with the construction method with parameters. Finally, the setProperties method can be used to configure ObjectFactory. After initializing your ObjectFactory instance, the properties defined in the ObjectFactory element will be passed to the setProperties method.

plugins

Mybatis allows you to intercept calls at some point during the execution of mapped statements. By default, mybatis allows the use of plug-ins to intercept method calls, including:

  • Executor(update,query,flushStatements,commit,rollback,getTransaction,close,isClosed)
  • ParameterHandler(getParameterObejct,setParameters)
  • ResultSetHandler(handlerResultSets,handlerOutputParameters)
  • StatementHandler(prepare,parameterize,batch,update,query)

The details of the methods in these classes can be found by viewing the signature of each method, or directly viewing the source code in the distribution package of MyBatis. Assuming you want to do more than just method calls, you should have a good understanding of the behavior of the method being rewritten. Because if you modify or rewrite the behavior of existing methods in the view, you are likely to destroy the core module of MyBatis. These are lower level classes and methods, so you should be particularly worried when using plug-ins.

Through the powerful mechanism provided by MyBatis, it is very simple to use the plug-in. You only need to implement the Interceptor interface and specify the method signature you want to intercept.

// ExamplePlugin.java
@Intercepts(
    {
        @Signature(
            type= Executor.class,
            method = "update",
  			args = {
      			MappedStatement.class,Object.class
  			}
         )
    }
)
public class ExamplePlugin implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }
    public void setProperties(Properties properties) {

    }
}
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

The above plug-in will intercept all "update" method calls in the Executor instance, where the Executor is the internal object responsible for executing the underlying mapping statement.
Override configuration class
In addition to using plug-ins to modify the core behavior of mybatis, you can also achieve this by completely overriding the configuration class. Just inherit and overwrite each method, and then pass it to sqlsessionfactorybuilder Build (myconfig) method. Again, this may seriously affect mybatis's behavior. Please be careful!

environments

MyBatis can be configured to adapt to a variety of environments. This mechanism helps to apply sql mapping to a variety of databases. In reality, there are many reasons to do so. For example, development, testing and production environments need different configurations; Or multiple production databases sharing the same Schema want to use the same sql mapping. Many similar use cases.
Although multiple environments can be configured, only one can be selected for each SqlSessionFactory instance.

Therefore, if you want to connect two databases, you need to create two SqlSessionFactory instances, one for each database. If there are three databases, three instances are required, and so on.

Each database corresponds to an instance of SqlSessionFactory.

To specify which environment to create, simply pass it as an optional parameter to SqlSessionFactoryBuilder. Two method signatures that can accept environment configuration are:

SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment);
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment,properties);

If the environment parameter is ignored, the default environment will be loaded as follows:

SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader);
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader,properties);

The environment element defines how to configure the environment:

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

Note the key points here:

  • ID of the default environment (for example: default = "development")
  • The environment ID defined by each environment element (for example: id = "development")
  • Configuration of transaction manager (for example: type = "JDBC")
  • Configuration of data source (for example: type = "POOLED")

The default environment and environment ID are clear at a glance. Whatever you name it, just make sure that the default environment matches one of the environment ID transaction managers.

There are two types of transaction managers in MyBatis (that is, type="[JDBC|MANAGED]")

  1. JDBC: this configuration directly uses the commit and rollback settings of JDBC. It relies on the connection from the data source to manage the transaction scope.

  2. MANAGED: this configuration does little. It never commits or rolls back a connection, but lets the container manage the entire life cycle of the transaction (such as the JEE application server context). By default, it closes the connection, but some containers don't want this, so you need to set the closeConnection property to false to prevent its default behavior. For example:

    <transactionManager type="MANAGED">
      <property name="closeConnection" value="false"/>
    </transactionManager>
    

If you are using Spring+MyBatis, there is no need to configure the transaction manager, because the Spring module will use its own manager to override the previous configuration.
Neither transaction manager type requires any attributes. They are just type aliases. In other words, you can replace them with the fully qualified name or type alias of the implementation class of the TransactionFactory interface.

public interface TransactionFactory {
    
  void setProperties(Properties props);   
    
  Transaction newTransaction(Connection conn);
    
  Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);   

}

Any property configured in xml will be passed to setProperties method after instantiation. You also need to create an implementation class of the Transaction interface, which is also very simple.

 public interface Transaction{
     
  Connection getConnection() throws SQLException;
     
  void commit() throws SQLException;
     
  void rollback() throws SQLException;
     
  void close() throws SQLException;
     
}

Using these two interfaces, you can completely customize the transaction processing of MyBatis.

Data source

The dataSource element uses the standard JDBC data source interface to configure the resources of the JDBC connection object.

Many MyBatis applications will configure the data source as shown in the example. However, it is not necessary. You should know that in order to facilitate the use of deferred loading, the data source is necessary.

There are three built-in data source types (that is, type="[UNPOOLED|POOLED|JNDI]):

UNPOOLED

The implementation of this data source only opens and closes the connection when requested. Although a little slow, it is a good choice for simple applications with no performance requirements in terms of timely available connections. Different databases perform differently in this aspect, so the use of connection pool is not important for some databases, and this configuration is also ideal. UNPOOLED data sources only need to be configured with the following five attributes:

  • Driver – this is the fully qualified name of the JDBC driven Java class (not the data source class that may be included in the jdbc driver)
  • url – this is the JDBC URL address of the database.
  • username – the user name to log in to the database.
  • Password – the password to log in to the database.
  • defaultTransactionIsolationLevel – the default connection transaction isolation level.

Optionally, you can pass attributes to the database driver. To do this, the attribute is prefixed with "driver.", For example:

  • driver.encoding=UTF-8

This will pass the encoding property with the value of UTF-8 to the database driver through the DriverManager,getConnection(url,driverProperties) method.

POOLED

The implementation of this data source uses the concept of "pool" to organize JDBC connection objects, avoiding the necessary initialization and authentication time when creating new connection instances. This is a popular way to make concurrent web applications respond to requests quickly.

In addition to the above mentioned attributes under UNPOOLED, there will be more attributes to configure the data source of POOLED:

  • poolMaximumActiveConnections – the number of active (that is, in use) connections that can exist at any time. The default value is 10.
  • poolMaximumIdleConnections – the number of idle connections that may exist at any time.
  • poolMaximumCheckoutTime – the time that connections in the pool are checked out before being forcibly returned. The default value is 20000 milliseconds (i.e. 20 seconds).
  • poolTimeToWait – this is an underlying setting. If it takes a long time to obtain a connection, it will print a status log to the connection pool and try to obtain a connection again (to avoid a quiet failure in case of misconfiguration). The default value is 20000 milliseconds (i.e. 20 seconds).
  • poolPingQuery – a detection query sent to the database to check whether the connection is in normal working order and ready to accept the request. The default is "NOT PING QUERY SET", which will cause most database connections to fail with an appropriate error message.
  • poolPingEnabled – whether detection is enabled. If enabled, you must also use an executable SQL statement to set the poolPingQuery property (preferably a very fast SQL). The default value is false.
  • poolPingConnectionsNotUsedFor – configure how often poolPingQuery is used. This can be set to match the specific database connection timeout to avoid unnecessary detection. The default value is 0 (that is, all connections are detected at all times - of course, it only applies when poolPingEnabled is true).
JNDI

The implementation of this data source is to be used in containers such as EJB or application server. The container can configure the data source centrally or externally, and then place a reference to JNDI context. This data source configuration requires only two attributes:

  • initial_ Context – this attribute is used to find the context in InitialContext (i.e. initialContext.lookup(initial_context)). This is an optional attribute. If omitted, data_ The source attribute will be found directly from the InitialContext.
  • data_source – this is the path of the context that references the location of the data source instance. Initial is provided_ When the context is configured, it will be found in the returned context. If it is not provided, it will be found directly in InitialContext.

Similar to other data source configurations, you can add the prefix "env." Pass the attribute directly to the initial context. For example:

  • env.encoding=UTF-8

This will pass the encoding attribute with the value of UTF-8 to its constructor when the initial context is instantiated.

Through the need to implement the interface org apache. ibatis. datasource. Datasourcefactory, or any third-party data source:

 public interface DataSourceFactory {
     
  void setProperties(Properties props);
     
  DataSource getDataSource();
     
}

org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory can be used as a parent class to build a new data source adapter. For example, the following code is necessary to insert C3P0:

import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;

public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {
    public C3P0DataSourceFactory() {
        this.dataSource =  new ComboPooledDataSource();
    }
}

To make it work, add a property to each setter method that needs to be called by MyBatis. The following is an example of connecting to a PostgreSQL database:

<dataSource type="org.myproject.C3P0DataSourceFactory">
  <property name="driver" value="org.postgresql.Driver"/>
  <property name="url" value="jdbc:postgresql:mydb"/>
  <property name="username" value="postgres"/>
  <property name="password" value="root"/>
</dataSource>

databaseIdProvider

MyBatis can execute different statements according to different database vendors. This multi vendor support is based on the databaseId attribute in the mapping statement. MyBatis loads all statements without the databaseId attribute and with the databaseId attribute matching the current database. If the same statement with and without databaseId is found at the same time, the latter is discarded. To support the multi vendor feature, just click MyBatis config Add databaseIdProvider to the XML file:

<databaseIdProvider type="DB_VENDOR" />

DB here_ Vendor will be set through the string returned by DatabaseMetaData#getDatabaseProductName(). Since this string is usually very long and different versions of the same product will return different values, it is best to shorten it by setting the property alias, as follows:

<databaseIdProvider type="DB_VENDOR">
    <property name="SQL Server" value="sqlserver"/>
    <property name="DB2" value="db2"/>         
    <property name="Oracle" value="oracle" />
</databaseIdProvider>

When there are properties, DB_ The of vendor databaseidprovider will be set to the value corresponding to the first property key that can match the database product name. If there is no matching property, it will be set to "null". In this example, if getDatabaseProductName() returns "Oracle(DataDirect)", databaseId will be set to "oracle".
You can implement the interface org apache. ibatis. mapping. DatabaseIdProvider and in mybatis config Register in XML to build your own DatabaseIdProvider:

public interface DatabaseIdProvider {
    
    void setProperties(Properties p);
    
    String getDatabaseId(DataSource dataSource) throws SQLException;
    
}

mappers

Now that the behavior of MyBatis has been configured by the above elements, it is time to define the SQL mapping statement. But first you need to tell MyBatis where to find these statements. Java doesn't provide a good method for automatic search, so the best way is to tell MyBatis where to find the mapping file. You can use resource references relative to the classpath, or fully qualified resource locators (including the URL of file: / /), or class and package names, and so on. For example:

<!-- Using classpath relative resources -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- Using url fully qualified paths -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- Using mapper interface classes -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- Register all interfaces in a package as mappers -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

These configurations will tell MyBatis where to find the mapping file. The remaining details should be each SQL mapping file.

Analysis of Mybatis source code

This part only involves the source code of SqlSessionFactoryBuilder, SqlSession, MapperProxy, plug-in and so on, and only provides blog link reference for log, cache and so on.

This paragraph refers to the blog link: Mybatis operation principle and source code analysis

introduce

We can refer to the official documents for the construction of the project. This time is based on mybatis-3.5.3 The jar version is built and analyzed without the integration of Spring. Referring to the official documentation, we only need to create mybatis config XML and mapper XML file and corresponding mapper interface. For your convenience, the source code is as follows:

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mybatis/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

UserMapper.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="mapper.UserMapper">
 
    <resultMap id="User" type="pojo.User">
        <result column="user_id" property="userId" jdbcType="INTEGER" javaType="Integer"/>
        <result column="user_account" property="userAccount" jdbcType="VARCHAR" javaType="String"/>
        <result column="user_createtime" property="userCreatetime" jdbcType="TIMESTAMP"
                javaType="java.time.LocalDateTime"/>
    </resultMap>
    <sql id="userColumn">
        user_id,
        user_account,
        user_createtime
    </sql>
    <select id="getUserById" parameterType="java.lang.String" resultMap="User">
        SELECT
        <include refid="userColumn"/>
        FROM `user`
        where
        user_id = #{userId}
    </select>
</mapper>

UserMapper.java

public interface UserMapper {
    WxUser getUserById(String userId);
}

MybatisTest.java entry similarity class

public static void main(String[] args) {
    try {
        String resource = "mybatis-config.xml";
        // Get the configuration file through classLoader
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        // Encapsulate the Configuration file and mapper file into the Configuration entity class
        SqlSessionFactory sqlSessionFactory = builder.build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // Dynamic agent mode
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.getUserById("8");
        System.out.println("Return results through dynamic proxy" + user.getUserAccount());

        // Get the query directly from the statement without dynamic agent
        User u2 = sqlSession.selectOne("mapper.UserMapper.getUserById", "8");
        System.out.println("adopt statement Return results" + u2.getUserAccount());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

SqlSessionFactoryBuilder

Since there are many parameters required for SqlSessionFactory initialization, Mybatis uses the constructor mode to instantiate a SqlSessionFactory object through xml. You can view the official documents for specific properties and configurations. Analyze the source code by looking at the build() method of SqlSessionFactoryBuilder. The main logic is to look at the code comments (it's best to check in combination with the source code).

public class SqlSessionFactoryBuilder {
    
	public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        try {
            // It mainly constructs the configuration file into an XMLConfigBuilder object 
        	XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
        	return build(parser.parse());
        } catch (Exception e) {
        	throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
        	ErrorContext.instance().reset();
          	try {
            	reader.close();
          	} catch (IOException e) {
            	// Intentionally ignore. Prefer previous error.
          	}
        }
  	}
    
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    	try {
            // It mainly constructs the configuration file into an XMLConfigBuilder object 
      		// Generally speaking, get config xml inputStream, then parse xml and encapsulate the configuration information into the parser object
      		XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      		return build(parser.parse());
    	} catch (Exception e) {
      		throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    	} finally {
      		ErrorContext.instance().reset();
        	try {
        		inputStream.close();
      		} catch (IOException e) {
        		// Intentionally ignore. Prefer previous error.
      		}
        }
    }
    
    public SqlSessionFactory build(Configuration config) {
    	return new DefaultSqlSessionFactory(config);
  	}
}

XMLConfigBuilder#parse()

// Parse config XML and all mappers XML encapsulated into Configuration object and returned
public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}
XMLConfigBuilder#parseConfiguration()
private void parseConfiguration(XNode root) {
    try {
        //issue #117 read properties first
        /**Resolve various properties in the configuration file*/
        propertiesElement(root.evalNode("properties"));
        /**Parsing global setting information of mybatis*/
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        loadCustomLogImpl(settings);
        /**Resolve alias configuration*/
        typeAliasesElement(root.evalNode("typeAliases"));
        /**Resolve plug-in configuration*/
        pluginElement(root.evalNode("plugins"));
        /**Resolve object factory elements*/
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        /**Parsing the environment configuration of mybatis*/
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        /**Resolve type processor configuration information*/
        typeHandlerElement(root.evalNode("typeHandlers"));
        /**Parsing mapper configuration information*/
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

In fact, it is to parse each node in the main Configuration file, save it in Configuration, and then use Configuration to create a DefaultSqlsessionFactory object.

Here, we can focus on the following two places to see what we are doing:

pluginElement(root.evalNode("plugins"));
mapperElement(root.evalNode("mappers"));
Plug in registration
pluginElement(root.evalNode("plugins"));

Click to view the detailed implementation:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            // Get content, such as com github. pagehelper. PageHelper
            String interceptor = child.getStringAttribute("interceptor");
            // Get the property information of the configuration
            Properties properties = child.getChildrenAsProperties();
            // Interceptor instance created
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            // Bind properties to interceptors
            interceptorInstance.setProperties(properties);
            /** Put the instantiated interceptor class into interceptorChain in configuration */
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

In fact, the interceptor class is parsed through the interceptor tag, and then instantiated and saved to the InterceptorChain in the Configuration class for future use.

public void addInterceptor(Interceptor interceptor) {
    // Interceptors are added to the interceptor chain, which is essentially an ordered set of lists
    this.interceptorChain.addInterceptor(interceptor);
}
mapper scanning and parsing
mapperElement(root.evalNode("mappers"));

Click to view the detailed implementation:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        // Traverse config XML all < mappers >
      	//                       <mapper resource=""/>
      	//                   </mappers>
        for (XNode child : parent.getChildren()) {
            /*If the child node is a configured < package >, perform automatic package scanning*/
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                /**If the child node is configured with resource, url and maperclass, we use resource in this article*/
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    // Get mapper XML file
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    // Encapsulate xml into objects
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // Parse encapsulated into Configuration object
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    /**Parse another xml file introduced by resource*/
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url == null && mapperClass != null) {
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

Let's take a look at how it parses another xml file:

// mapperParser.parse()
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        /*Parsing sql statements*/
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        /*
        	Bind mapper's namespace
        	That is, resolving the namespace is actually the interface class corresponding to the binding
        */
        bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
}

Let's take a look at what configurationElement(parser.evalNode("/mapper")) does:

private void configurationElement(XNode context) {
    try {
        // mapper maps the namespace field of the file
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
}

Let's take a look at what buildStatementFromContext(context.evalNodes("select|insert|update|delete")) does:

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

Let's take a look at the overloaded method of buildStatementFromContext():

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

Take a look at statementparser In the parsestatementnode() method:

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                                                   configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
        resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                        resultSetTypeEnum, flushCache, useCache, resultOrdered,
                                        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

Let's take a look at addMappedStatement(...) of MappedStatement returned by adding parameters method:

public MappedStatement addMappedStatement(
    String id,
    SqlSource sqlSource,
    StatementType statementType,
    SqlCommandType sqlCommandType,
    Integer fetchSize,
    Integer timeout,
    String parameterMap,
    Class<?> parameterType,
    String resultMap,
    Class<?> resultType,
    ResultSetType resultSetType,
    boolean flushCache,
    boolean useCache,
    boolean resultOrdered,
    KeyGenerator keyGenerator,
    String keyProperty,
    String keyColumn,
    String databaseId,
    LanguageDriver lang,
    String resultSets) {

    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

By parsing the tags one by one, all the information of the sql statement is finally encapsulated into MappedStatement objects, which are then stored in the configuration object.

So what does bindMapperForNamespace() do?

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            //ignore, bound type is not required
        }
        if (boundType != null) {
            if (!configuration.hasMapper(boundType)) {
                // Spring may not know the real resource name so we set a flag
                // to prevent loading again this resource from the mapper interface
                // look at MapperAnnotationBuilder#loadXmlResource
                configuration.addLoadedResource("namespace:" + namespace);
                configuration.addMapper(boundType);
            }
        }
    }
}

In fact, it is to parse the class corresponding to the sql and put the class into mapperRegistry in configuration. In fact, all the configuration information of mybatis and the configuration parameters of runtime are saved in the configuration object.

Therefore, the whole process can be represented by the following sequence diagram:

SqlSession

SqlSession is obtained mainly through the default implementation class of SqlSessionFactory. The openSessionFromDataSource of DefaultSqlSessionFactory encapsulates a DefaultSqlSession (implementing the SqlSession interface) and returns.

When openSession() is executed, the code actually executed is as follows:

@Override
public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // Obtain database link information and transaction object from configuration object
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // Create an Executor object to execute SQL script later
        final Executor executor = configuration.newExecutor(tx, execType); // [core code]
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

As can be seen from the code, the openSession() operation will create the Executor object, one of the four Mybatis objects. The creation process is as follows:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    /**If L2 caching is enabled, the executor will be wrapped once by cacheingexecution*/
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    /*
        Try to wrap the executor once with each interceptor in the interceptorChain (according to the configuration),
        Here is to support the powerful plug-in development function of Mybatis
    */
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

By default, a simpleexecution object is returned. Then simpleexecution is encapsulated into DefaultSqlSession.

Here, we need to note that after the executor is created, we will decide whether to use cacheingexecution to wrap the executor once according to whether the L2 cache is enabled. Finally, try to wrap the executor once with each interceptor in the interceptorChain (according to the configuration). Here is to support the powerful plug-in development function of Mybatis.

Mapper proxy

When we use the following code:

UserMapper mapper = session.getMapper(UserMapper.class);

To get UserMapper, you actually get the proxy object of UserMapper from MapperRegistry in configuration:

/**
  * You can see that we obtain the MapperRegistry object from the Configuration object through the class object as the key
  * MapperProxyFactory Then the proxy object is generated through the dynamic proxy of jdk
  (This explains why we need to create a Mapper interface instead of an entity class)  
  * The addMapper() method inside is not deja vu.
  */

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

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

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

// MapperRegistry 44
@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);
    }
}

Let's take a look at the implementation of MapperProxyFactory:

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }
	
    // This method is called in the top code.
    // The value in the knownMappers attribute is actually what we put in during mappers scanning and parsing.
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }

}
public class MapperProxy<T> implements InvocationHandler, Serializable {
    ...
}

MapperProxy implements the InvocationHandler interface. As can be seen from the above code, it actually uses the dynamic proxy of jdk to generate a proxy object for the UserMapper interface. In fact, it is an object of MapperProxy, as shown in the debugging information below:

Therefore, the whole generation process of proxy object can be represented by the following sequence diagram:

Execute query statement

We know that the UserMapper we obtained is actually the proxy object MapperProxy, so when we execute the query statement, we actually execute the invoke method of MapperProxy:

// MapperProxy 78
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        /**If an Object native method is called, it will be released directly*/
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else if (method.isDefault()) {
            if (privateLookupInMethod == null) {
                return invokeDefaultMethodJava8(proxy, method, args);
            } else {
                return invokeDefaultMethodJava9(proxy, method, args);
            }
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

Let's take another look at the cachedMapperMethod method:

private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method,
        k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }

As you can see, first get the method from the method cache according to the method signature. If it is empty, generate a MapperMethod, put it into the cache and return it.

Therefore, the final execution of the query is the execute method of MapperMethod:

// MapperMethod 57
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;
        }
         /**select Query statement*/
        case SELECT:
            /**When the return type is empty*/
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
            /**When many is returned*/
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
            /**When the return value type is Map*/
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
                
            } else if (method.returnsCursor()) {
                result = executeForCursor(sqlSession, args);
            } else {
                /**Remove the above and perform the steps here*/
                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());
    }
    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;
}

The branch statements executed in our example are:

 Object param = method.convertArgsToSqlCommandParam(args);
 result = sqlSession.selectOne(command.getName(), param);

Here is a query parameter parsing process:

// MapperMethod 308
public Object convertArgsToSqlCommandParam(Object[] args) {
    return paramNameResolver.getNamedParams(args);
}

// ParamNameResolver 110
public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
        return null;
        /**The parameter is not annotated with @ Param annotation, and the number of parameters is one*/
    } else if (!hasParamAnnotation && paramCount == 1) {
        return args[names.firstKey()];
        /**Otherwise, execute this branch*/
    } else {
        final Map<String, Object> param = new ParamMap<>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            param.put(entry.getValue(), args[entry.getKey()]);
            // add generic param names (param1, param2, ...)
            final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
            // ensure not to overwrite parameter named with @Param
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        return param;
    }
}

The meaning of the code here is: if the parameter analysis judges that there is only one parameter (a single parameter or a set parameter) and there is no @ Param annotation, the value of this parameter will be returned directly, otherwise it will be encapsulated into a Map and then returned.

The style of encapsulation is explained by several examples:

Example 1:

/**Interface is*/
User selectByNameSex(String name, int sex);
/**We call in the following format*/
userMapper.selectByNameSex("Zhang San",0);
/**Parameters are encapsulated in the following format:*/
0 ---> Zhang San
1 ---> 0
param1 ---> Zhang San
param2 ---> 0

Example 2:

/**Interface is*/
User selectByNameSex(@Param("name") String name, int sex);
/**We call in the following format*/
userMapper.selectByNameSex("Zhang San",0);
/**Parameters are encapsulated in the following format:*/
name ---> Zhang San
1 ---> 0
param1 ---> Zhang San
param2 ---> 0

After the parameters are processed, the execution process is called. Finally, the selectList method in DefaultSqlSession is called and executed:

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

Here, call the query method of simpleexecution to execute the query operation, and then call the doQuery method:

// SimpleExecutor 57
@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();
        /**The StatementHandler of the four Mybatis objects appears here*/
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}

// Configuration 591
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) 
        /**Create StatementHandler and apply to plug-in support*/   
        interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

While creating StatementHandler, the plug-in function is applied, and two other objects among the four objects of Mybatis are created at the same time:

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
   ......
   ......
   ......
 /**Mybatis ParameterHandler in the four objects*/     
  this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
  /**Mybatis ResultSetHandler in four objects*/ 
  this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    	......
        ......
        ......
        interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
                                            ResultHandler resultHandler, BoundSql boundSql) {
    	......
        ......
        ......
        interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

The next step is to execute the normal JDB C query function. The parameter setting is operated by ParameterHandler, and the result set processing is processed by ResultSetHandler. So far, the whole query process is over.

The sequence diagram of the whole query stage can be shown in the following figure:

The whole query process can be summarized as follows:

Plug in development

introduce

First of all, we know that the process of a query is:

  1. Initialize the Configuration object according to the Configuration file (global, sql mapping)

  2. Create a DefaultSqlSession object, which contains Configuration and Executor (the corresponding Executor is created according to the defaultExecutorType of the global Configuration file)

  3. DefaultSqlSession.getMapper() gets the MapperProxy corresponding to the Mapper interface

  4. There is DefaultSqlSession in MapperProxy

  5. Methods of adding, deleting, modifying and querying:

    • (proxy object) call the addition, deletion, modification and query (Executor) of DefaultSqlSession

    • Create a StatementHandler object, as well as ParameterHandler and ResultSetHandler

    • Call the precompiled parameter of StatementHandler (set the parameter value with ParameterHandler)

    • Call the addition, deletion, modification and query method of StatementHandler

    • ResultSetHandler encapsulates the result

Four objects

The four objects of Mybatis refer to: Executor, StatementHandler, parameterhandler and ResultSetHandler.

  • ParameterHandler: handles the parameter object of SQL.
  • ResultSetHandler: handles the returned result set of SQL.
  • StatementHandler: the processing object of the database, which is used to execute SQL statements.
  • Executor: the executor of MyBatis, which is used to perform addition, deletion, modification and query operations.

Mybatis allows us to intercept the specified methods during the execution of the four objects, which makes it convenient to enhance the function. This function is very similar to the aspect programming of Spring. As mentioned above, plug-ins are enhanced when the four objects are created. Let's explain its implementation principle.

Mybatis plug-in interface - Interceptor

First, we need to understand the Intercaptor interface.

  1. Intercept method, the core method of plug-in.
  2. plugin method to generate the proxy object of target.
  3. setProperties method to pass the parameters required by the plug-in.

See the method implementation and parameter comments of the following code to understand:

/** Sign the plug-in and tell mybatis which method the single money plug-in uses to intercept that object**/
@Intercepts({@Signature(type = ResultSetHandler.class,method ="handleResultSets",args = Statement.class)})
public class MyFirstInterceptor implements Interceptor {

     /** @Description Method of intercepting target object**/
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("Intercepted target object:"+invocation.getTarget());
        Object object = invocation.proceed();
        return object;
    }
     /**
      * @Description Wrapping a target object creates a proxy object for the target object
      * @Param target For the object to be intercepted
      * @Return Proxy object
      */
    @Override
    public Object plugin(Object target) {
        System.out.println("Target object to wrap:"+target);
        return Plugin.wrap(target,this);
    }
    /** Get the properties of the configuration file**/
    @Override
    public void setProperties(Properties properties) {
        System.out.println("Initialization parameters of plug-in configuration:"+properties);
    }
}

Then in mybatis Configure plug-ins in XML.

<!-- Custom plug-ins -->
<plugins>
    <plugin interceptor="mybatis.interceptor.MyFirstInterceptor">
        <!--configuration parameter-->
        <property name="name" value="Bob"/>
    </plugin>
</plugins>

Call the query method, and the query method will return ResultSet.

public class MyBatisTest {
    
    public static SqlSessionFactory sqlSessionFactory = null;

    public static SqlSessionFactory getSqlSessionFactory() {
        if (sqlSessionFactory == null) {
            String resource = "mybatis-config.xml";
            try {
                Reader reader = Resources.getResourceAsReader(resource);
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sqlSessionFactory;
    }

    public void testGetById() {
        SqlSession sqlSession = this.getSqlSessionFactory().openSession();
        PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
        Person person=personMapper.getById(2001);
        System.out.println(person.toString());
    }

public static void main(String[] args) {
        new MyBatisTest().testGetById();
    }
}

The final output result is:

Initialization parameters of plug-in configuration:{name=Bob}
Target object to wrap: org.apache.ibatis.executor.CachingExecutor@754ba872
 Target object to wrap: org.apache.ibatis.scripting.defaults.DefaultParameterHandler@192b07fd
 Target object to wrap: org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7e0b0338
 Target object to wrap: org.apache.ibatis.executor.statement.RoutingStatementHandler@1e127982
 Intercepted target object: org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7e0b0338
Person{id=2001, username='Tom', email='email@0', gender='F'}
Practical examples

For example, we want to print the time before and after the sql statement to calculate the execution time of sql. With this function, we can intercept StatementHandler. Here we need time for the Intercaptor interface provided by Mybatis.

/**The annotation signature tells the interceptor which method of which of the four objects is intercepted and the signature information of the method*/
@Intercepts(
    {
        @Signature(
            type = StatementHandler.class,
            method = "query",
            args = {Statement.class,ResultHandler.class}
        )
	}
)
public class SqlLogPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long begin = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long time = System.currentTimeMillis() - begin;
            System.out.println("sql Run:" + time + " ms");
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

Next, we need to open the mybatis config XML to configure the Interceptor:

<plugins>
    <plugin interceptor="com.test.mybatis.intercaptor.SqlLogPlugin">
        <property name="Parameter 1" value="root"/>
        <property name="Parameter 2" value="123456"/>
    </plugin>
</plugins>

At this time, the configuration of interceptor is completed, and the operation results are as follows:

DEBUG 11-24 17:51:34,877 ==>  Preparing: select * from user where id = ?   (BaseJdbcLogger.java:139) 
DEBUG 11-24 17:51:34,940 ==> Parameters: 1(Integer)  (BaseJdbcLogger.java:139) 
DEBUG 11-24 17:51:34,990 <==      Total: 1  (BaseJdbcLogger.java:139) 
sql Run: 51 ms
User{id=1, name='Zhang San', age=42, sex=0}

Plug in principle

Mybatis plug-in principle, that is, mybatis plug-in handles interception with the help of the mode of responsibility chain; Using dynamic agent to package the target object to achieve the purpose of interception; Acts on the scope object of mybatis.

So how does the plug-in intercept and add additional functions?

Let's start with ParameterHandler:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object object, BoundSql sql, InterceptorChain interceptorChain){
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

pluginAll is implemented as follows:

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

interceptorChain saves all interceptors, which were created when Mybatis was initialized. Call the interceptors in the interceptor chain to intercept or enhance the target in turn. interceptor. The target in plugin (target) can be understood as the four objects in Mybatis. The returned target is the object after being represented.

Let's see from the above practical application example:

public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

This code is the packaging of the target object. In actual operation, it is the class after the packaging used. In operation, it executes the intercept method. Now let's see how it is packaged:

public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
        // The JDK dynamic proxy class is returned. The proxy class enhances the target class
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap));
    }
    return target;
}

The Plugin class implements the InvocationHanlder interface:

public class Plugin implements InvocationHandler {
    // ...
}

Obviously, the dynamic proxy of JDK is used here, wrapping a layer on the target object and overriding the invoke() method.

// Plugin.invoke
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        Set<Method> methods = signatureMap.get(method.getDeclaringClass());
        // If it is a defined interception method, execute the intercept method
        if (methods != null && methods.contains(method)) {
            //This method enhances
            return interceptor.intercept(new Invocation(target, method, args));
        }
        // It is not a method that needs to be intercepted and executed directly
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

If an object is wrapped by multiple interceptors for many times, the post wrapped will be executed first in the outermost layer.

Mybatis interview questions

#What's the difference between {} and ${}?

  • ${} is a variable placeholder in the properties file. It can be used for tag attribute values and internal sql. It belongs to static text substitution.
  • #{} is the parameter placeholder of sql, and Mybatis will replace #{} in sql with? No. before sql execution, the parameter setting method of PreparedStatement will be used to give the sql in order? Number placeholder sets the parameter value.

For example:

# The parameters passed by ${param} will be regarded as part of the sql statement, for example:
order by ${param}
# The parsed sql is:
order by id
 
# #The incoming data of {param} is treated as a string, and a double quotation mark will be added to the automatically incoming data, for example:
select * from table where name = #{param}
# The parsed sql is:
select * from table where name =   "id"

Therefore, it is generally used #{} to replace variables.

Mybatis L1 cache and L2 cache?

  • Level 1 Cache: HashMap local Cache based on PerpetualCache. Its storage scope is Session. When Session flush or close, all caches in the Session will be cleared. Mybatis opens the level 1 Cache by default, and the level 1 Cache is stored in the localCache variable of BaseExecutor.
  • Second level cache: the mechanism is the same as that of first level cache. By default, it also adopts perpetual cache and HashMap storage. The difference is that its storage scope is Mapper(Namespace) level. Mybatis does not turn on the L2 cache by default. You can turn on the global L2 cache in the config file XML < Settings > < setting name = "cacheenabled" value = "true" / > < / Settings >, but it will not set the L2 cache for all mappers. Each mapper The tag is used in the XML file to open the secondary cache of the current mapper. The secondary cache is stored in the cache variable of MappedStatement class.

L1 cache

The code is as follows:

public abstract class BaseExecutor implements Executor {
    // ...
    protected PerpetualCache localCache;
	// ...
    
    // Construction method
    protected BaseExecutor(Configuration configuration, Transaction transaction) {
		// ...
        this.localCache = new PerpetualCache("LocalCache"); // Default initialization
        // ...
    }

    // Query query method
    @SuppressWarnings("unchecked")
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            queryStack++;
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; // Get cache
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            deferredLoads.clear();
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }
    
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER); // The execution placeholder is just a singleton enumeration class
        try {
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            localCache.removeObject(key);
        }
        localCache.putObject(key, list); // Storage cache
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }
}

L2 cache

The L2 cache is not enabled by default and needs to be manually enabled. When implementing L2 cache, MyBatis requires that the returned POJO s must be serializable.

The condition for enabling L2 cache is also relatively simple. You can set it directly in MyBatis configuration file:

<settings>
	<setting name = "cacheEnabled" value = "true" />
</settings>

It also needs to be in mapper Add the < cache > tag to the XML configuration file.

Cache invalidation

L1 cache

Mybatis L1 cache (session level cache: sqlSession) is enabled by default.

Mybatis L1 cache invalidation:

  1. Not the same sqlSession (two different sqlsessions)
  2. For the same sqlSession, the query conditions are different (the query conditions are different, and the corresponding data has not been put into the cache)
  3. For the same sqlSession, update operations (add, delete, modify) are performed between two queries
  4. For the same sqlSession, the cache is cleared between two queries: sqlSession clearCache()
L2 cache

When executing an sql query, first query from the secondary cache, if not, enter the primary cache query, and then query the database without executing the sql. Put the cache into the primary cache before the end of the session, and put it into the secondary cache after the end of the session (you need to open the secondary cache).

@Options(flushCache = FlushCachePolicy.TRUE, useCache = false)
/*
1. flushCache: Whether to clear the cache. The DEFAULT is DEFAULT. You can set flushcachepolicy directly TRUE
    1.1 FlushCachePolicy.DEFAULT: If it is a query (select) operation, it is false, and other update (insert, update, delete) operations are true
2. useCache: Whether to put query results into L2 cache
*/

How does Mybatis plug-in work?

It should have been mentioned above. Let's mention it a little more.

Write plug-ins

  1. Implement Interceptor interface method
  2. Identify intercepted signatures
  3. Configure the plug-in in the configuration file

Operation principle of plug-in

When creating three important handlers (statementhandler, ParameterHandler and ResultSetHandler), three handlers are wrapped through the plug-in array:

resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);

Obtain all interceptors (interceptors, i.e. the interfaces that the plug-in needs to implement), and call:

interceptor.plugin(target);

Return the object wrapped by target, and finally call back the intercept() method of the custom plug-in to execute the code logic in the plug-in.

List of interceptable interfaces and methods:

  • Executor(update,query , flushStatment , commit , rollback , getTransaction , close , isClose)
  • StatementHandler(prepare , paramterize , batch , update , query)
  • ParameterHandler( getParameterObject , setParameters )
  • ResultSetHandler( handleResultSets , handleCursorResultSets , handleOutputParameters )

Example: PageHelper

Configuration process

Relevant dependencies are as follows:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.2.8</version>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>1.2.15</version>
</dependency>

First, in myabtis Add a plug-in plugin to the global file of XML, as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <plugins>
        <!-- com.github.pagehelper by PageHelper Package name of class -->
        <plugin interceptor="com.github.pagehelper.PageHelper">
            <property name="dialect" value="mysql" />
            <!-- This parameter defaults to false -->
            <!-- Set to true When, will RowBounds First parameter offset regard as pageNum Page usage -->
            <!-- and startPage Medium pageNum Same effect -->
            <property name="offsetAsPageNum" value="true" />
            <!-- This parameter defaults to false -->
            <!-- Set to true When using RowBounds Paging will occur count query -->
            <property name="rowBoundsWithCount" value="true" />
            <!-- Set to true When, if pageSize=0 perhaps RowBounds.limit = 0 All the results will be found -->
            <!-- (It is equivalent to not executing paging query, but the returned result is still Page (type) -->
            <property name="pageSizeZero" value="true" />
            <!-- 3.3.0 Version available - Paging parameter rationalization, default false Disable -->
            <!-- When rationalization is enabled, if pageNum<1 The first page will be queried if pageNum>pages The last page will be queried -->
            <!-- When rationalization is disabled, if pageNum<1 or pageNum>pages Null data will be returned -->
            <property name="reasonable" value="false" />
            <!-- 3.5.0 Version available - To support startPage(Object params)method -->
            <!-- Added one`params`Parameter to configure parameter mapping from Map or ServletRequest Medium value -->
            <!-- Can configure pageNum,pageSize,count,pageSizeZero,reasonable,Default values for non configured mappings -->
            <!-- Do not copy the configuration without understanding the meaning -->
            <property name="params" value="pageNum=start;pageSize=limit;" />
            <!-- always Always return PageInfo type,check Check whether the return type is PageInfo,none return Page -->
            <property name="returnPageInfo" value="check" />
        </plugin>
    </plugins>
</configuration>

Use steps
Common use
// Get profile
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
// Obtain the SqlSessionFactory object by loading the configuration file
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// Get SqlSession object
SqlSession session = factory.openSession();
PageHelper.startPage(1, 5);
session.selectList("com.bobo.UserMapper.query");
Used in Service
@Service("orderService")
public class OrderServiceImpl implements OrderService {
    
	@Autowired
    private OrderDao orderDao;

    @Override
    public List<Order> findAll(int page, int size) throws Exception {
        // pageNum is the page number value, and pageSize is the number of items displayed on each page
        PageHelper.startPage(page, size);
        return orderDao.findAll();
    }
}
Principle description

According to the above, in the plug-in registration of SqlSessionFactoryBuilder using XMLConfigBuilder#parseConfiguration() to parse the configuration file, we know that PageHelper will be encapsulated as an Interceptor and registered into the Interceptor chain.

Then we just need to pay attention to its specific registered interception information.

Let's take a look at the header definition of the source code of PageHelper:

@SuppressWarnings("rawtypes")
// It is defined to intercept the data in the Executor object
// The interception method is: query (mappedstatement MS, object o, rowboundaries ob, resulthandler RH)
@Intercepts(
    @Signature(
        type = Executor.class, 
        method = "query", 
        args = {MappedStatement.class
            , Object.class
                , RowBounds.class
                    , ResultHandler.class
                    }))
public class PageHelper implements Interceptor {
    
    //sql tool class
    private SqlUtil sqlUtil;
    //Attribute parameter information
    private Properties properties;
    //Configure object mode
    private SqlUtilConfig sqlUtilConfig;
    //Automatically obtain the dialog. If there is no setProperties or setSqlUtilConfig, it can also be carried out normally
    private boolean autoDialect = true;
    //Get dialect automatically at runtime
    private boolean autoRuntimeDialect;
    //If there are multiple data sources, do you want to close the data source after obtaining the JDBC URL
    private boolean closeConn = true;
    
}

I won't analyze the details.. In fact, the implementation of PageHelper paging is to dynamically splice SQL statements into paging statements before we execute SQL statements, so as to realize the process of paging acquisition from the database.

Mapping between Xml Mapping file and internal data structure?

According to the picture of the first article I introduced at the beginning of the article, take a brief look.

configuration

resultMap

mappedStatment

What design patterns are used in Mybatis?

  • Log module: agent mode, adapter mode
  • Data source module: agent mode, factory mode
  • Cache module: decorator mode
  • Initialization phase: Builder mode
  • Agent phase: policy mode
  • Data reading and writing stage: template mode
  • Plug in development: responsibility chain model

This can be extended to introduce design patterns. In fact, those familiar with design patterns can be mapped to the implementation of many source codes through a design pattern.

Topics: Java Interview