preface
Mybaits ranks high in ORM frameworks because of its lightweight, semi-automatic loading, flexibility and extensibility. Deeply loved by the majority of companies, so our program development is inseparable from Mybatis. But have we looked into the Mabtis source code? Or want to watch but don’t know how to watch?After all, we still need to know why there is MyBatis and what problem myBatis solves? To see what myBatis solves, it’s important to know what pain points exist in traditional JDBC operations that led to myBatis. With these questions, let’s learn step by step.
Problems with raw JDBC
So let’s look at the raw JDBC operation first: we know the original database operation. Obtain a preparedStatement. 3. Substitute placeholders for parameters. 4.
Here is the original JDBC query code, what are the problems?
public static void main(String[] args) { String dirver="com.mysql.jdbc.Driver"; String url="jdbc:mysql://localhost:3306/mybatis? characterEncoding=utf-8"; String userName="root"; String password="123456"; Connection connection=null; List<User> userList=new ArrayList<>(); try { Class.forName(dirver); connection= DriverManager.getConnection(url,userName,password); String sql="select * from user where username=?" ; PreparedStatement preparedStatement=connection.prepareStatement(sql); PreparedStatement. SetString (1, "zhang"); System.out.println(sql); ResultSet resultSet=preparedStatement.executeQuery(); User user=null; while(resultSet.next()){ user=new User(); user.setId(resultSet.getInt("id")); user.setUsername(resultSet.getString("username")); user.setPassword(resultSet.getString("password")); userList.add(user); } } catch (Exception e) { e.printStackTrace(); }finally { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } if (! userList.isEmpty()) { for (User user : userList) { System.out.println(user.toString()); }}}Copy the code
What do you find unwelcoming about it? Here I summarize the following points: 1. Database connection information is hard coded, that is, written in code. 2, Each operation will establish and release the connection, unnecessary waste of operation resources. 3. SQL and parameters are hard coded. 4, to encapsulate the returned result set into entity class trouble, to create a different entity class, and through the set method injection.
There are above problems, so Mybatis to improve the above problems. For hard coding, it’s easy to think of configuration files. Mybatis solved this too. For resource waste, we thought of using connection pooling, which is what MyBatis solves. For the problem of encapsulating the result set, we think of using JDK reflection mechanism, good coincidence, Mybatis is also solved this way.
Design ideas
In this case, we will write a custom persistence layer framework, to solve the above problems, of course, is the reference to mybatis design ideas, so we after writing, and then look at myBatis source code suddenly realized, this place so configuration is because of this ah. We have two parts: the user side and the frame side.
Using the
When we use Mybatis, do we need to use sqlMapconfig. XML configuration file, which is used to store database connection information and mapper. XML pointing information? The mapper. XML configuration file is used to store SQL information. So we create two files sqlmapconfig. XML and mapper. XML on the user side.
Frame side
What does the frame side do? 1. Obtain the configuration file. Sqlmapconfig. XML and mapper. XML files. Parse the retrieved files to get connection information, SQL, parameters, return types, and so on. This information is stored in the Configuration object. Create SqlSessionFactory to create an instance of SqlSession. 4. Create an SqlSession to perform the original JDBC operations above.
So what happens in SqlSession? SQL > create SQL statement (); execute SQL (); encapsulate result set as object ()
Use-side implementation
Well, the above said, about the design ideas, mainly modeled on the main mybatis class to achieve, to ensure that the class name is consistent, convenient for us to read the source code. Let’s start by configuring the user side and creating a Maven project. In the project, we create a User entity class
public class User { private Integer id; private String username; private String password; private String birthday; // Getter () and setter() methods}Copy the code
Create sqlmapconfig. XML and mapper. XML sqlmapconfig.xml
<? The XML version = "1.0" encoding = "utf-8"? > <configuration> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis? serverTimezone=UTC& characterEncoding=utf8& useUnicode=true& useSSL=false"></property> <property name="userName" value="root"></property> <property name="password" value="123456"></property> <mapper resource="UserMapper.xml"> </mapper> </configuration>Copy the code
You can see that our XML is configured with database connection information and an index of mapper. Sqlmapconfig. XML in Mybatis also contains other tags, just rich functionality, so we only use the most important.
Mapper. XML is a mapper. XML generated for each class of SQL. Let’s use the User class here, so we’ll create a usermapper.xml
<? The XML version = "1.0" encoding = "utf-8"? > <mapper namespace="cn.quellanan.dao.UserDao"> <select id="selectAll" resultType="cn.quellanan.pojo.User"> select * from user </select> <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User"> select * from user where username=#{username} </select> </mapper>Copy the code
ResultType Specifies the type of the result set to be returned. ParamType specifies the type of the parameter. We used the end first to create here, basically two configuration files, and then we’ll see how the framework side is implemented.
Come on.
Frame-side implementation
Frame end, we follow the above design ideas step by step.
Access to the configuration
How do I get the configuration file? We can use the JDK’s built-in Class Resources loader to get files. Let’s create a custom Resource class to encapsulate it:
import java.io.InputStream; Public class Resources {public static InputStream getResources(String path){// Use the system's own Resources loader to obtain the file. return Resources.class.getClassLoader().getResourceAsStream(path); }}Copy the code
In this way, you can get the corresponding file stream by passing in the path.
Parsing configuration files
The SQLMapconfig.xml configuration file was obtained above, and we will now parse it. But before we do that, we need to do a little prep work: where do we put the parsing memory? So let’s create two entity classes, Mapper and Configuration.
The Mapper entity class is used to store the contents of a written mapper. XML file, which contains. Id, SQL, resultType, and paramType. So we create the following Mapper entities:
public class Mapper { private String id; private Class<? > resultType; private Class<? > parmType; private String sql; // Getter () and setter() methods}Copy the code
Why don’t we add the namespace value here? Smart you must have found, because mapper inside these attributes indicate that each SQL corresponds to a Mapper, and namespace is a namespace, which is the upper layer of SQL, so in mapper temporarily used, it did not add.
The Configuration Configuration entity is used to store information in SqlMapConfig. To save the database connection, we use the DataSource provided by the JDK directly. And then there’s mapper information. Each mapper has its own identity, so hashMap is used here. As follows:
public class Configuration { private DataSource dataSource; HashMap <String,Mapper> mapperMap=new HashMap<>(); // Getter () and setter methods}Copy the code
XmlMapperBuilder
With that done, let’s parse mapper. We create an XmlMapperBuilder class to parse. XML files are parsed through dom4j’s utility classes. The dom4j dependency I use here is:
Dom4j </groupId> <artifactId>dom4j</artifactId> <version>2.1.3</version> </dependency>Copy the code
1, get the file stream, turn it into document. 2. Get the root node, known as mapper. Obtain the namespace attribute value of the root node. 3. Obtain the ID, SQL,resultType, and paramType of the SELECT node. 6. Generate the key value through namespace.id and save the Mapper object to the HashMap in the Configuration entity. 7, Return the following entity code:
public class XmlMapperBuilder { private Configuration configuration; public XmlMapperBuilder(Configuration configuration){ this.configuration=configuration; } public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException { Document document=new SAXReader().read(in); Element rootElement=document.getRootElement(); String namespace=rootElement.attributeValue("namespace"); List<Node> list=rootElement.selectNodes("//select"); for (int i = 0; i < list.size(); i++) { Mapper mapper=new Mapper(); Element element= (Element) list.get(i); String id=element.attributeValue("id"); mapper.setId(id); String paramType = element.attributeValue("paramType"); if(paramType! =null && ! paramType.isEmpty()){ mapper.setParmType(Class.forName(paramType)); } String resultType = element.attributeValue("resultType"); if (resultType ! = null && ! resultType.isEmpty()) { mapper.setResultType(Class.forName(resultType)); } mapper.setSql(element.getTextTrim()); String key=namespace+"."+id; configuration.getMapperMap().put(key,mapper); } return configuration; }}Copy the code
I only parsed the SELECT tag above. You can parse the insert/delete/uupdate tags and do the same.
XmlConfigBuilder
Sqlmapconfig. XML configuration information is the same as sqlmapConfig. XML configuration information. 2. Obtain the root node (Configuration). Select * from root; select * from root; Create a dataSource connection pool. 5. Save the connection pool information to the Configuration entity. 6 8. Complete the code as follows:
public class XmlConfigBuilder { private Configuration configuration; public XmlConfigBuilder(Configuration configuration){ this.configuration=configuration; } public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { Document document=new SAXReader().read(in); Element rootElement=document.getRootElement(); List<Node> propertyList=rootElement. SelectNodes ("//property"); Properties properties=new Properties(); for (int i = 0; i < propertyList.size(); i++) { Element element = (Element) propertyList.get(i); properties.setProperty(element.attributeValue("name"),element.attributeValue("value")); } ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setDriverClass(properties.getProperty("driverClass")); dataSource.setJdbcUrl(properties.getProperty("jdbcUrl")); dataSource.setUser(properties.getProperty("userName")); dataSource.setPassword(properties.getProperty("password")); configuration.setDataSource(dataSource); List<Node> mapperList=rootElement. SelectNodes ("//mapper"); for (int i = 0; i < mapperList.size(); i++) { Element element= (Element) mapperList.get(i); String mapperPath=element.attributeValue("resource"); XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration); configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath)); } return configuration; }}Copy the code
Create a SqlSessionFactory
After parsing, we create SqlSessionFactory to create Sqlseesion entity. Here, in order to restore mybatis design ideas, we also use factory design mode. SqlSessionFactory is an interface that contains a method to create SqlSessionf. As follows:
public interface SqlSessionFactory {
public SqlSession openSqlSession();
}
Copy the code
This interface alone is not enough, we also need to write an implementation class for the interface, so we create a DefaultSqlSessionFactory. As follows:
public class DefaultSqlSessionFactory implements SqlSessionFactory { private Configuration configuration; public DefaultSqlSessionFactory(Configuration configuration) { this.configuration = configuration; } public SqlSession openSqlSession() { return new DefaultSqlSeeion(configuration); }}Copy the code
As you can see, create a DefaultSqlSeeion and pass in the Configuration containing the configuration information. DefaultSqlSeeion is an implementation class of SqlSession.
Create a SqlSession
In SqlSession we will handle various operations such as selectList, selectOne, insert. Update,delete, etc. As follows:
Public interface SqlSession {/** * conditional query * @param statementid unique identifier, namespace.selectid * @param parm parameter. Public <E> List<E> selectList(String statementid,Object... parm) throws Exception; public <T> T selectOne(String statementid, Object... parm) throws Exception; public int insert(String statementid, Object... parm) throws Exception; public int update(String statementid, Object... parm) throws Exception; public int delete(String statementid, Object... parm) throws Exception; public void commit() throws Exception; @param mapperClass * @param <T> * @return */ public <T> T getMapper(Class<T> mapperClass);Copy the code
Then we create DefaultSqlSeeion to implement SqlSeesion.
public class DefaultSqlSeeion implements SqlSession { private Configuration configuration; private Executer executer=new SimpleExecuter(); public DefaultSqlSeeion(Configuration configuration) { this.configuration = configuration; } @Override public <E> List<E> selectList(String statementid, Object... parm) throws Exception { Mapper mapper=configuration.getMapperMap().get(statementid); List<E> query = executer.query(configuration, mapper, parm); return query; } @Override public <T> T selectOne(String statementid, Object... parm) throws Exception { List<Object> list =selectList(statementid, parm); if(list.size()==1){ return (T) list.get(0); }else{throw new RuntimeException(" return too many results "); } } @Override public int insert(String statementid, Object... parm) throws Exception { return update(statementid,parm); } @Override public int update(String statementid, Object... parm) throws Exception { Mapper mapper=configuration.getMapperMap().get(statementid); int update = executer.update(configuration, mapper, parm); return update; } @Override public int delete(String statementid, Object... parm) throws Exception { return update(statementid,parm); } @Override public void commit() throws Exception { executer.commit(); } @Override public <T> T getMapper(Class<T> mapperClass) { Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// Obtain the method name. String name = method.getName(); // Type String className = method.getIngClass ().getName(); String statementid=className+"."+name; Mapper mapper = configuration.getMapperMap().get(statementid); SqlCommandType sqlType = mapper.getSqlCommandType(); Type genericReturnType = method.getGenericReturnType(); switch (sqlType){ case SELECT: If (genericReturnType instanceof ParameterizedType){return selectList(statementid,args); }else { return selectOne(statementid,args); } case INSERT:return insert(statementid,args); case DELETE:return delete(statementid,args); case UPDATE:return update(statementid,args); default:break; } return null; }}); return (T) proxyInstance; }}Copy the code
We can see that DefaultSqlSeeion gets the Configuration and uses Statementid to get the Mapper from the configuration. The implementation is then handed over to the Executer class. Regardless of how Executer is implemented, let’s pretend it’s already implemented. Then the whole frame end is complete. Get the result by calling the sqlsession.selectList () method.Feels like we’ve framed it before we’ve even processed it? It’s true that earlier we took the file and parsed the file and created the factory. It’s all about preparation. Let’s start with our JDBC implementation.
Implementation of SqlSession
SqlSeesion is implemented in the following 5 steps: 1, obtain a database connection 2, obtain SQL, and parse SQL 3, by introspection, inject parameters into preparedStatement 4, execute SQL 5, encapsulate the result set as an object through reflection
But in DefaultSqlSeeion we handed the implementation over to the Executer to execute. So we’re going to implement these operations in executers.
Let’s start by creating an Executer interface and writing a Query method called in DefaultSqlSeeion.
public interface Executer { <E> List<E> query(Configuration configuration,Mapper mapper,Object... parm) throws Exception; }Copy the code
Next we write a SimpleExecuter class to implement Executer. Then in the simpleExecuter.query () method, we implement it step by step.
Getting a database connection
Because the database connection information is stored in configuration, you can get it directly.
/ / to get connected connection = configuration. GetDataSource () getConnection ();Copy the code
Get the SQL and parse the SQL
Let’s think about what the SQL that we wrote in usermapper.xml looks like.
select * from user where username=#{username}
Copy the code
{username}} {username}
#{***} #{***} No.
Parses #{***} to obtain the value of paramType.
The implementation uses the following classes. GenericTokenParser class, you can see that it has three arguments, the start tag, which is our “#{“, the end tag, which is”} “, and the tag handler processes the contents of the tag, which is username.
public class GenericTokenParser { private final String openToken; Private final String closeToken; Private final TokenHandler Handler; Public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } /** * parse ${} and #{} * @param text * @return * This method mainly implements the configuration file, script, etc. placeholder parsing, processing work, and return the final required data. Public String parse(String text) {// Parse (String text) {// Parse (String text)}}Copy the code
The main one is the parse() method, which is used to get the SQL for operation 1. Get results for example:
select * from user where username=?
Copy the code
So that’s using TokenHandler to handle the parameters. ParameterMappingTokenHandler TokenHandler class
public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); #{id} #{username} @override public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?" ; } private ParameterMapping buildParameterMapping(String content) { ParameterMapping parameterMapping = new ParameterMapping(content); return parameterMapping; } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } public void setParameterMappings(List<ParameterMapping> parameterMappings) { this.parameterMappings = parameterMappings; }}Copy the code
You can see that parameter names are stored in the ParameterMapping collection. The ParameterMapping class is an entity that holds parameter names.
public class ParameterMapping { private String content; public ParameterMapping(String content) { this.content = content; } // Getter () and setter() methods. }Copy the code
So what we’re going to do is we’re going to use GenericTokenParser to get the parsed SQL and the parameter names. We wrap this information into the BoundSql entity class.
public class BoundSql { private String sqlText; private List<ParameterMapping> parameterMappingList=new ArrayList<>(); public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) { this.sqlText = sqlText; this.parameterMappingList = parameterMappingList; } ////getter() and setter() methods. }Copy the code
Okay, so there’s two steps, get it, and then parse it and get the raw SQL is very simple, the SQL information is stored in a Mapper object, and you just get it.
String sql=mapper.getSql()
Copy the code
Resolution 1, 2, create a ParameterMappingTokenHandler processor to create a GenericTokenParser classes, and initialize the start tag and the end tag, Generictokenparser.parse (SQL); Get the parsed SQL ‘, and stored in the parameterMappingTokenHandler set of parameter names. 4. Wrap the parsed SQL and parameters into the BoundSql entity class.
/ analytical define placeholder * * * * @ param SQL * @ return * / private BoundSql getBoundSql (String SQL) {ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{","}",parameterMappingTokenHandler); String parse = genericTokenParser.parse(sql); return new BoundSql(parse,parameterMappingTokenHandler.getParameterMappings()); }Copy the code
Inject the parameters into the preparedStatement
This completes the SQL parsing, but we know that the SQL generated above still contains JDBC placeholders, so we need to inject the parameters into the preparedStatement. ParameterMappingList = mapper.getParmType = mapper.getParmType = mapper.getParmType = mapper.getParmType(); GetDeclaredField (content) getDeclaredField(content) Get () from the parameter class. 6. Inject it into a preparedStatement
BoundSql boundSql=getBoundSql(mapper.getSql()); String sql=boundSql.getSqlText(); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList(); / / get a preparedStatement, and pass the parameter value preparedStatement preparedStatement = connection. The prepareStatement (SQL); Class<? > parmType = mapper.getParmType(); for (int i = 0; i < parameterMappingList.size(); i++) { ParameterMapping parameterMapping = parameterMappingList.get(i); String content = parameterMapping.getContent(); Field declaredField = parmType.getDeclaredField(content); declaredField.setAccessible(true); Object o = declaredField.get(parm[0]); preparedStatement.setObject(i+1,o); } System.out.println(sql); return preparedStatement;Copy the code
Execute SQL
The executeQuery() or execute() methods of JDBC are actually called
/ / execute SQL ResultSet ResultSet = preparedStatement. ExecuteQuery ();Copy the code
Encapsulate the result set into objects by reflection
After we get the resultSet, we encapsulate it, similar to the parameter processing. Get the property name and property value 5. Create an property generator 6. Generate write methods for the property and write the property value to the property 7
/** * encapsulate resultSet * @param mapper * @param resultSet * @param <E> * @return * @throws Exception */ private <E> List<E> resultHandle(Mapper mapper,ResultSet resultSet) throws Exception{ ArrayList<E> list=new ArrayList<>(); // Encapsulate result set Class<? > resultType = mapper.getResultType(); while (resultSet.next()) { ResultSetMetaData metaData = resultSet.getMetaData(); Object o = resultType.newInstance(); int columnCount = metaData.getColumnCount(); for (int i = 1; i <= columnCount; I ++) {// Attribute name String columnName = metadata.getColumnName (I); // Attribute Value Object Value = resultSet.getobject (columnName); PropertyDescriptor PropertyDescriptor = new PropertyDescriptor(columnName,resultType); // create a PropertyDescriptor to generate read/write methods for properties. Method writeMethod = propertyDescriptor.getWriteMethod(); writeMethod.invoke(o,value); } list.add((E) o); } return list; }Copy the code
Create the SqlSessionFactoryBuilder is
Let’s now create an SqlSessionFactoryBuilder class to provide a population for the user side.
public class SqlSessionFactoryBuilder { private Configuration configuration; public SqlSessionFactoryBuilder(){ configuration=new Configuration(); } public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException { XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder(configuration); configuration=xmlConfigBuilder.loadXmlConfig(in); SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration); return sqlSessionFactory; }}Copy the code
As you can see, the build method parses the information to configuration via the SqlMapConfig file stream, creating and returning an sqlSessionFactory.
By now, the whole framework end has been built, but we can see that only the select operation, update, inster, delete operation we will have implementation in the source code I provide behind, here is just the overall design ideas and processes.
test
Finally, it’s time for the test. We have written a custom persistence layer, so let’s test it to see if it works. It’s time for a miracle
Let’s start by introducing our custom framework dependencies. As well as database and unit tests
< the dependency > < groupId > mysql < / groupId > < artifactId > mysql connector - Java < / artifactId > < version > 8.0.11 < / version > </dependency> <dependency> <groupId>cn.quellanan</groupId> <artifactId>myself-mybatis</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId> The < version > 4.10 < / version > < / dependency >Copy the code
Then we write a test class 1, get sqlMapperConfig. XML file stream 2, get Sqlsession 3, perform lookup
@org.junit.Test public void test() throws Exception{ InputStream inputStream= Resources.getResources("SqlMapperConfig.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession(); List<User> list = sqlSession.selectList("cn.quellanan.dao.UserDao.selectAll"); for (User parm : list) { System.out.println(parm.toString()); } System.out.println(); User user=new User(); User. SetUsername (" zhang "); List<User> list1 = sqlSession.selectList("cn.quellanan.dao.UserDao.selectByName", user); for (User user1 : list1) { System.out.println(user1); }}Copy the code
You can see that it is done. It looks like our custom persistence layer framework is working.
To optimize the
But do not be happy too early haha, we look at the above test method, is not the same feeling and use of statementId, every time write dead, so not too friendly, so we next point SAO operation, universal mapper configuration. We add a getMapper method to the SqlSession that takes a class as an argument. So we know the statementId from this class.
@param mapperClass * @param <T> * @return */ public <T> T getMapper(Class<T> mapperClass);Copy the code
Specific implementation is the use of JDK dynamic proxy mechanism. Get a Proxy object from proxy.newproxyInstance (). When the proxy object is created, an InvocationHandler interface is implemented and the invoke() method is overridden so that all methods that invoke the proxy will execute the invoke() method. So what does this method do? This method is to get the class name and method name of the object from the class object passed in. Used to generate statementid. Therefore, the namespace in the mapper. XML configuration file needs to be specified as the classpath and id as the method name. Implementation method:
@Override public <T> T getMapper(Class<T> mapperClass) { Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSeeion.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// Obtain the method name. String name = method.getName(); // Type String className = method.getIngClass ().getName(); String statementid=className+"."+name; return selectList(statementid,args); }}); return (T) proxyInstance; }Copy the code
Let’s write a UserDao
public interface UserDao {
List<User> selectAll();
List<User> selectByName(User user);
}
Copy the code
This is the interface of the Mapper layer. We then specify the namespace and ID in mapper.xmlNext we are writing a test method
@org.junit.Test public void test2() throws Exception{ InputStream inputStream= Resources.getResources("SqlMapperConfig.xml"); SqlSession sqlSession = new SqlSessionFactoryBuilder().build(inputStream).openSqlSession(); UserDao mapper = sqlSession.getMapper(UserDao.class); List<User> users = mapper.selectAll(); for (User user1 : users) { System.out.println(user1); } User user=new User(); User. SetUsername (" zhang "); List<User> users1 = mapper.selectByName(user); for (User user1 : users1) { System.out.println(user1); }}Copy the code
One day
Custom persistence layer framework, we’re done. This is actually the prototype of Mybatis, we manually write a persistent layer framework, and then look at the source of Mybatis, will be a lot clearer. The following class names are available in Mybatis.Here, I wish you a happy reading source code. If you think it’s useful, keep it.
Brazen beg wave praise!!
Brazen beg wave praise!!
Brazen beg wave praise!!