General operations in traditional JDBC programming:
- 1. Register the database driver class and specify the URL address, database user name, password and other connection information of the database
- 2. Open database connection through DriverManager
- 3. Create a Statement object through a database connection.
- 4. Execute the SQL statement through the State object to get the ResultSet object.
- 5. Read the data through the ResultSet and convert the data into JavaBean objects
- 6. Close connections and free resources
ORM (Object Relational Mapping) object relational mapping. It is used to realize the conversion between data of different types of systems in object-oriented programming language.
1. Source code analysis
1. Read the information in the configuration file first
After many times of tracing, we found the core method of getResourceAsStream. The following is the source code of this method
The parameters of this method include not only the incoming file path, but also the classloader. The main function of class loading is to convert the name into a file name and read external resources.
As the entrance to debug
Trace to the core source code
Two manipulations are done here:
- Here, an object of XMLConfigBuilder is generated and its parse() method is called. Return the resolved configuration object information.
- The build() method of SqlSessionFactoryBuilder is called, and the parameters passed in are the Configuration object obtained in the previous step.
First trace the parse() method of the XMLConfigBuilder object
parsed here is guaranteed to be loaded only once. The / configuration here is the root node of the entire configuration file and the entry to parse the entire configuration file. Then call the parseConfiguration method to continue tracing.
Check the value passed in by root, which is actually each node under configuration
<configuration> <typeAliases> <package name="com.github.yeecode.mybatisdemo"/> </typeAliases> <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/mybatis?serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <mapper resource="com/github/yeecode/mybatisdemo/UserMapper.xml"/> </mappers> </configuration>
View the information under the environment and continue to trace to the environmentsElement method,
View the value of the passed in context. Here is the parsed information. That is, url, username, password and driver information. The information is different from the previous one
<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/mybatis?serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments>
By entering each sub method, you can see that all the Configuration information of the Configuration file is saved in the Configuration class.
The SqlSessionFactory object is returned by calling the build() method of SqlSessionFactoryBuilder itself.
Summary
In the initialization phase, MyBatis mainly completes the following tasks
- Get its input stream InputStream according to the location of the assignment file
- Starting from the root node of the Configuration file, the Configuration price is parsed layer by layer, including the corresponding mapping file. The parsing process keeps putting the parsing results into the Configuration object
- Take the configured Configuration object as a parameter to obtain a SqlSessionFactory object
2. Data read / write phase tracking
2.1 obtaining sqlsession
Trace openSession method
Enter the openSessionFromDataSource method. Here is the core source code for generating SqlSession
Enter the DefaultSqlSession class, which provides a large number of methods such as query, add, update, delete, commit and rollback
Execute new DefaultSqlSession(this.configuration, executor, autoCommit); After that, a SqlSession object is returned
2.2 binding of mapping interface file and mapping file
Operate the getMapper method of Configuration and finally enter the getMapper method of MapperRegistry class.
2.3 mapping interface proxy
session.getMapper(UserMapper.class) gets mapperproxyfactory The object returned by newinstance (sqlsession), which is obtained by tracing this method
The returned dynamic proxy object is based on reflection. Find the invoke method of MapperProxy class. The method of the proxy object will be intercepted by the invoke method of the proxy object and directly to the invoke method of MapperProxy class,
Set a breakpoint on invoke when usermapper.com is executed queryUserBySchoolName(userParam); It will automatically enter the breakpoint invoke
The execute method of the MapperMethod object is then triggered
Enter the method
public Object execute(SqlSession sqlSession, Object[] args) { Object result; Object param; switch(this.command.getType()) { case INSERT: param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.insert(this.command.getName(), param)); break; case UPDATE: param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.update(this.command.getName(), param)); break; case DELETE: param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.delete(this.command.getName(), param)); break; case SELECT: if (this.method.returnsVoid() && this.method.hasResultHandler()) { this.executeWithResultHandler(sqlSession, args); result = null; } else if (this.method.returnsMany()) { result = this.executeForMany(sqlSession, args); } else if (this.method.returnsMap()) { result = this.executeForMap(sqlSession, args); } else if (this.method.returnsCursor()) { result = this.executeForCursor(sqlSession, args); } else { param = this.method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(this.command.getName(), param); if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + this.command.getName()); } if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) { throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ")."); } else { return result; } }
Different processing methods will be called according to different operation types. Here, query operation is performed, so this method is called
Enter the executeForMany method. In this method, MyBatis has started to carry out subsequent query through the selectList method of SqlSession object.
2.4 searching SQL statements
Continue to trace to the selectList method in DefaultSqlSession. The source code is as follows
Each MappedStatement object corresponds to a database operation node, which mainly defines database operation statements, input / output parameters and other information. this. Configuration. The getmappedstatement (statement) statement finds the MappedStatement object to be executed from the mapping file information stored in the Configuration object.
2.5 query cache
The query method is an abstract method in the Executor. There are two implementations: one is to query the database and the other is to query the cache
Directly make a breakpoint on the abstract method to see which side to execute. Here is the method in cachingeexecutor. The source code is as follows
BoundSql is the sql statement with if, where, and other tags removed after layer by layer conversion, and CacheKey is built for the cache calculated by this query operation.
Continue to track the code flow here
Check whether the current query operation is cached in the name. If it is to obtain the data value from the cache, if not, call delegate and call the query method. Then put the results of this query into the cache.
2.6 database query
The abstract method in the executor interface is called again
Continue tracking code flow
Finally, it enters the query method in BaseExecutor. The source code is as follows
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 (this.closed) { throw new ExecutorException("Executor was closed."); } else { if (this.queryStack == 0 && ms.isFlushCacheRequired()) { this.clearLocalCache(); } List list; try { ++this.queryStack; list = resultHandler == null ? (List)this.localCache.getObject(key) : null; if (list != null) { this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { --this.queryStack; } if (this.queryStack == 0) { Iterator var8 = this.deferredLoads.iterator(); while(var8.hasNext()) { BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next(); deferredLoad.load(); } this.deferredLoads.clear(); if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { this.clearLocalCache(); } } return list; } }
The key part is to call the database to expand the query operation
The source code of queryFromDatabase is as follows
First, place a placeholder in the cache and then invoke the doQuery method to execute it. Finally, replace the placeholder in the cache with the real query result.
doQuery method is an abstract method in BaseExecutor, which is the final implementation code actually running
Generated a statement object, which is Java Class in SQL package. Statement class can execute static SQL statements and return results.
Obtain a StatementHandler object handler, and then hand over the query operation to StatementHandler. StatementHandler is a statement processor, which encapsulates many statement operation methods., Continue to track code flow.
handler.query(stmt,resultHandler) calls the abstract method in StatementHandler interface
Follow up the code tracking, entering PreparedStatementHandler, calling method, source code is as follows
Here, ps.execute() actually executes the SQL statement, and then gives the execution result to the ResulHandler object for processing.
Process combing
- 1. Check the cache first. If you must check the database, you need to put the results into the cache after querying the database
- 2. The SQL Statement is transformed for many times, passed through MappedStatement object, Statement object and PreparedStatement object, and finally executed
- Finally, the result of the query is handed over to the ResultHandler object for processing
2.7 processing result set
The queried result set is not returned directly, but is handled by the ResultHandler object. ResultHandler is the result processor. The method used to accept the query result is the abstract method handleResultSets in the interface
Continue code tracing. The final execution method is the handleResultSets method in the DefaultResultSetHandler class
public List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(this.mappedStatement.getId()); List<Object> multipleResults = new ArrayList(); int resultSetCount = 0; ResultSetWrapper rsw = this.getFirstResultSet(stmt); List<ResultMap> resultMaps = this.mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); this.validateResultMapsCount(rsw, resultMapCount); while(rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount); this.handleResultSet(rsw, resultMap, multipleResults, (ResultMapping)null); rsw = this.getNextResultSet(stmt); this.cleanUpAfterHandlingResultSet(); ++resultSetCount; } String[] resultSets = this.mappedStatement.getResultSets(); if (resultSets != null) { while(rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = (ResultMapping)this.nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = this.configuration.getResultMap(nestedResultMapId); this.handleResultSet(rsw, resultMap, (List)null, parentMapping); } rsw = this.getNextResultSet(stmt); this.cleanUpAfterHandlingResultSet(); ++resultSetCount; } } return this.collapseSingleResultList(multipleResults); }
The query results are traversed, put into the list multipleResults and returned.
Continue code tracking
- Createresultobject (resultsetwrapper RSW, resultmap, resultmap, list < class <? > > constructorargtypes, list < Object > constructorargs, string columnprefix) method: when the automatic attribute mapping function is enabled, this method assigns the value of the data record to the output result object.
Trace code into the applyAutomaticMappings method
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { List<DefaultResultSetHandler.UnMappedColumnAutoMapping> autoMapping = this.createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix); boolean foundValues = false; if (!autoMapping.isEmpty()) { Iterator var7 = autoMapping.iterator(); while(true) { DefaultResultSetHandler.UnMappedColumnAutoMapping mapping; Object value; do { if (!var7.hasNext()) { return foundValues; } mapping = (DefaultResultSetHandler.UnMappedColumnAutoMapping)var7.next(); value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column); if (value != null) { foundValues = true; } } while(value == null && (!this.configuration.isCallSettersOnNulls() || mapping.primitive)); metaObject.setValue(mapping.property, value); } } else { return foundValues; } }
- applyAutomaticMappings method: this method will assign values to the attributes of the output result object according to the user's mapping settings
Continue to trace the code and enter the applyPropertyMappings method
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix); boolean foundValues = false; List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings(); Iterator var9 = propertyMappings.iterator(); while(true) { while(true) { Object value; String property; do { ResultMapping propertyMapping; String column; do { if (!var9.hasNext()) { return foundValues; } propertyMapping = (ResultMapping)var9.next(); column = this.prependPrefix(propertyMapping.getColumn(), columnPrefix); if (propertyMapping.getNestedResultMapId() != null) { column = null; } } while(!propertyMapping.isCompositeResult() && (column == null || !mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) && propertyMapping.getResultSet() == null); value = this.getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix); property = propertyMapping.getProperty(); } while(property == null); if (value == DEFERRED) { foundValues = true; } else { if (value != null) { foundValues = true; } if (value != null || this.configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive()) { metaObject.setValue(property, value); } } } } }
- applyPropertyMappings method: this method assigns values to the properties of the output result object according to the user's mapping settings
After execution, the loaded multipleResults are returned to the list < user > userlist variable
Summary
- 1. Establish an sqlsession to connect to the database
- 2. Find the database operation node corresponding to the abstract method in the current mapping interface, and generate the implementation of the interface according to the node
- 3. The implementation of the interface intercepts calls to abstract methods in the mapping interface and converts them into data query operations
- 4. The database operation statements in the database operation node are processed many times, and finally the standard sql statements are obtained.
- 5. Try to query, find and return from the cache; Unable to find database. Continue to find database
- 6. Query results from database
- 7. Processing result set (establish output object and assign value to the attribute of output object according to the output result)
- Record query results in cache
- Return query results