Create a mybatis custom persistence framework [implement simple single table crud]

Custom persistence framework mybatis

preface

JDBC operation database and Mybatis operation database. Why use the Mybatis framework instead of the native JDBC operation database?
With these questions, let's take a look at the native JDBC data operation steps!

JDBC database operation steps

  • 1 load drive
  • 2 create connection
  • 3 compiling sql statements
  • 4 setting parameters
  • 5 execute sql statement
  • 6 get result return set
  • 7 close the connection

Several pain points in JDBC operation database:

  • 1. The first step is to load the driver. We can solve this problem through reflection and replace different database drivers;
  • 2 to create a connection, every time you operate the database, you have to go to the ready-made connection database. If you operate the database very frequently, this overhead consumes resources. We can use the idea of thread pool to solve it!
  • 3, 3 and 4 steps can be seen together. When compiling sql statements, some parameters are usually set. If there are many parameter objects, they are often changed greatly. In the process of hard coding, a little careless operation may lead to code errors, high change cost and great coupling!
  • 4. There is nothing to say about executing sql;
  • 5. Get the result return set. In terms of query result return, the result set accepted by each query is different. Here, the object is different, and other codes are repeated;
  • 6 close the connection

In the above analysis processes, almost every operation will face the problems of database connection, closing, obtaining the result set (the return type does not pass, the object type is different), high repeated code, serious coupling of parameter settings, frequent changes and high error rate;

The code with high repeatability can be solved by encapsulation and utilization;
For drivers, you can change different database drivers through configuration files;
Frequent connections can be solved through connection pool;
For setting parameters and obtaining result sets, you can encapsulate different types of returned result sets through configuration files, generics and reflection;

Project structure

###Dependencies needed

      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.17</version>
      </dependency>
      <dependency>
          <groupId>c3p0</groupId>
          <artifactId>c3p0</artifactId>
          <version>0.9.1.2</version>
      </dependency>
      <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>1.2.12</version>
      </dependency>
      <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.10</version>
      </dependency>
      <dependency>
          <groupId>dom4j</groupId>
          <artifactId>dom4j</artifactId>
          <version>1.6.1</version>
      </dependency>
      <dependency>
          <groupId>jaxen</groupId>
          <artifactId>jaxen</artifactId>
          <version>1.1.6</version>
      </dependency>

Custom framework

client

#####Steps

  • First, define the database configuration file and configure the imported sqlMapper file

    Use byte input stream to load it into memory, use dom4j parsing to package it into Configuration object, and reuse it;

  • Define the sqlMapper file, which is used to write sql statements, input parameters and output parameter types

    Load and parse the encapsulated object MappedStatement, which is used to save the input parameters, output parameter types and sql operation types of each sql mapper and each sql statement;

Define database profile

First, we define the database configuration file sqlmapconfiguration xml

<configuration>

    <!--Database connection information-->
    <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    <property name="jdbcUrl"  value="jdbc:mysql:///stu_test"></property>
    <property name="username" value="root"></property>
    <property name="password" value="root"></property>


    <!--    to configure mapper sql There will be multiple information files-->
    <mapper resource="mapper.xml"></mapper>

</configuration>

Define the configuration class

It is used to save the database configuration information and the unique sql type namespace in each mapper id of sql

public class Configration {

    /**
     * Data source object
     */
    private DataSource dataSource;

    /**
     * key The rule is namesac ++ ID (the ID of each sql statement) is used when setting parameters and return types
     */
    private Map<String,MappedStatement> mapperStamentMap = new HashMap<>();


    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Map<String, MappedStatement> getMapperStamentMap() {
        return mapperStamentMap;
    }

    public void setMapperStamentMap(Map<String, MappedStatement> mapperStamentMap) {
        this.mapperStamentMap = mapperStamentMap;
    }
}

Define xmlConfigerBuilder class resolution

Define query statement sql configuration file

mapper.xml

Define sql and sql input parameter object types, and sql query return types

<mapper namespace="User"> 

<select id="selectOne" paramterType="com.udeam.com.udeam.pojo.User" resultType="com.udeam.com.udeam.pojo.User"> 
select * from user where id = #{id} and name =#{name} 
</select>

<select id="selectList" resultType="com.udeam.com.udeam.pojo.User"> 
select * from user 
</select> 

</mapper>
Define MappedStatement entity class

Save the sql statement type, sql input parameter, return type and sql id of sql in each mapper

public class MappedStatement {

    /**
     * sql xml Statement id indicates the uniqueness of each sql
     */
    private String id;

    /**
     * sql Input parameter type
     */
    private Class<?> paramType;

    /**
     * sql Return type
     */
    private Class<?> resultType;

    /**
     * sql sentence
     */
    private String sql;


    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Class<?> getParamType() {
        return paramType;
    }

    public void setParamType(Class<?> paramType) {
        this.paramType = paramType;
    }

    public Class<?> getResultType() {
        return resultType;
    }

    public void setResultType(Class<?> resultType) {
        this.resultType = resultType;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }
}

Define SqlSessionFactoryBuilder class build() method to parse sqlmapconfiguration xml

public class SqlSessionFactoryBuilder {

    private Configration configration;

    public SqlSessionFactoryBuilder() {
        this.configration = new Configration();
    }

    public SqlSessionFactory build(InputStream inputStream) throws Exception {
        //1 parse the Configuration file and encapsulate the Configuration
        xmlConfigerBuilder xmlConfigerBuilder = new xmlConfigerBuilder(configration);
        //2 Analysis
        configration = xmlConfigerBuilder.parseConfigration(inputStream);

        // 3 create SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configration);
        return sqlSessionFactory;
    }

}

Define the xmlConfigerBuilder class for

public class xmlConfigerBuilder {

    private Configration configration;


    public xmlConfigerBuilder(Configration  configration) {
        this.configration = configration;
    }


    /**
     * Parsing and encapsulating xml
     * @return
     */
    public Configration parseConfigration(InputStream inputStream) throws Exception {
        Document read = new SAXReader().read(inputStream);

        //Get tag
        Element rootElement = read.getRootElement();
        //1 set datasource property
        List<Element> elementList = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        for (int i = 0; i < elementList.size(); i++) {
            //Set attribute value
            String name = elementList.get(i).attributeValue("name");
            String value = elementList.get(i).attributeValue("value");
            properties.setProperty(name,value);
        }
        // Set the connection pool attribute. Here, use c3p0 connection pool
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));
        comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        //Set datasource
        configration.setDataSource(comboPooledDataSource);

        //2. Encapsulate and resolve mapper attribute
        // Read mapper, set mapper return type and encapsulate MappedStatement objects such as sql
        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configration);
        List<Element> list = rootElement.selectNodes("//mapper");
        for (Element element : list) {
            String resource = element.attributeValue("resource");
            InputStream inputStream1 = Resource.inputStream(resource);
            //Read every mapper xml
            xmlMapperBuilder.parse(inputStream1);
        }
        return configration;
    }
}

Define the XMLMapperBuilder class, parse the information in Mapper class, parse the sql statement, return type and input parameter type of each sql in Mapper encapsulation configuration class

Parsing mapper encapsulated sql statement attributes to MappedStatement

public class XMLMapperBuilder {

    private Configration configration;

    public XMLMapperBuilder(Configration configration) {
        this.configration = configration;
    }

    public void parse(InputStream inputStream1) throws DocumentException, ClassNotFoundException {

        Document read = new SAXReader().read(inputStream1);
        Element rootElement = read.getRootElement();

        //Get namespace
        String namespace = rootElement.attributeValue("namespace");
        //Read each query tag
        List<Element> list = rootElement.selectNodes("//select");
        for (Element element : list) {
            MappedStatement mappedStatement = new MappedStatement();
            //Get sql
            String sql = element.getTextTrim();
            mappedStatement.setSql(sql);
            //Set id
            String id = element.attributeValue("id");
            mappedStatement.setId(id);

            //Set input parameter type
            String paramterType = element.attributeValue("paramterType");
            mappedStatement.setParamType(getClassType(paramterType));

            //Set return type
            String resultType = element.attributeValue("resultType");
            mappedStatement.setResultType(getClassType(resultType));

            //Set mapperstationmap
            configration.getMapperStamentMap().put(namespace + "." + id, mappedStatement);
        }
    }


    public Class<?> getClassType(String type) throws ClassNotFoundException {
        if(type==null){
            return null;
        }

        Class<?> clasz = Class.forName(type);
        return clasz;

    }
}

Define Resource class to read xml

public class Resource {

    /**
     * Load profile tool class
     * @param name
     * @return
     * @throws FileNotFoundException
     */
    public static InputStream inputStream(String name) throws Exception {
        //Loading configuration files using class loader
        InputStream inputStream = Resource.class.getClassLoader().getResourceAsStream(name);
        return inputStream;
    }
}

query

Define query interface SqlSession
public interface SqlSession {

    //Query multiple
    public <E>List<E> selectList(String statementId,Object...params) throws IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException;

    //Query single
    public <T> T  selectOne(String statementId,Object...params) throws IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException;


    public void close() throws Exception;

}

sql statement executor Excutor interface

public interface Excutor {

    /**
     * Query interface
     * @param configration Database configuration class
     * @param mappedStatement mapper Information object
     * @param params parameter
     * @param <E>
     * @return
     */
    <E> List<E> query(Configration configration, MappedStatement mappedStatement,Object[] params) throws SQLException, IllegalAccessException, InstantiationException, NoSuchFieldException, IntrospectionException, InvocationTargetException;

    void close() throws Exception;
}

sql statement executor class implementation
Specific sql executors (there are three in mybatis). The default is SimpleExcutor

Here, you can obtain sql and sql input parameter return type from MappedStatement through the passed in configuration file and specific key(namespace.sql id)
Then set the parameters through the reflection area to obtain the result return set;

Among them, BoundSql class is used to process sql in xml and convert it into? Placeholder, parse out #{} the value inside for storage, and then perform subsequent assignment operations!

public class SimpleExcutor implements Excutor {

    private Connection connection;

    @Override
    public <E> List<E> query(Configration configration, MappedStatement mappedStatement, Object[] params) throws SQLException, IllegalAccessException, InstantiationException, NoSuchFieldException, IntrospectionException, InvocationTargetException {

        //1 get connection
        connection = configration.getDataSource().getConnection();

        //2 get sql select * from user where id = #{id} and name = #{name}
        String sql = mappedStatement.getSql();
        //Process the sql. / / convert the sql statement: select * from user where id =? and name = ? , In the process of conversion, the values in #{} should be parsed and stored
        BoundSql boundSql = getBoundSql(sql);

        //Final sql
        String finalSql = boundSql.getSqlText();

        // 3 precompiled object
        PreparedStatement preparedStatement = connection.prepareStatement(finalSql);


        //Gets the type of parameter passed in
        Class<?> paramType = mappedStatement.getParamType();


        // 4 get incoming parameters
        List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();

        //Set parameters
        for (int i = 0; i < parameterMappingList.size(); i++) {
            String content = parameterMappingList.get(i).getContent();
            //Reflection settings
            Field declaredField = paramType.getDeclaredField(content);
            //Mandatory access
            declaredField.setAccessible(true);
            Object o = declaredField.get(params[0]);
            //The placeholder set value column starts with 1
            preparedStatement.setObject(i + 1, o);
            System.out.println(" The current property is " + content + " Value is : " + o);
        }

        // 5. Execute sql
        ResultSet resultSet = preparedStatement.executeQuery();
        //Parameter type returned
        Class<?> resultType = mappedStatement.getResultType();

        ArrayList<E> objects = new ArrayList<>();
        while (resultSet.next()) {
            //create object
            Object o = (E) resultType.newInstance();

            //Gets the metadata of the column value returned by the database
            ResultSetMetaData metaData = resultSet.getMetaData();
            //Returns the total number of columns
            int columnCount = metaData.getColumnCount();
            for (int i = 1; i <= columnCount; i++) {
                //Get column value
                String columnName = metaData.getColumnName(i);
                //Get value
                Object value = resultSet.getObject(columnName);
                //Using introspection techniques can also use reflection techniques
                //Create an attribute descriptor and generate read-write methods for attributes
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultType);
                //Get write method
                Method writeMethod = propertyDescriptor.getWriteMethod();
                // Write value to class
                writeMethod.invoke(o, value);
            }

            objects.add((E) o);
        }


        return objects;
    }


    @Override
    public void close() throws Exception {
        connection.close();
    }



    /**
     * Complete the analysis of #{}: 1 Will #{} use? Replace, 2 Parse out #{} the value and store it
     * @param sql
     * @return
     */
    private BoundSql getBoundSql(String sql) {
        //Tag processing class: configure the tag parser to complete the parsing and processing of placeholders
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //Parsed sql
        String parseSql = genericTokenParser.parse(sql);
        //#Parameter name resolved in {}
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();

        BoundSql boundSql = new BoundSql(parseSql,parameterMappings);
        return boundSql;

    }
}

The specific query interface is implemented, such as DefaultSqlSession
Default sqlsession implementation class (defaultsqlsession in mybatis)

public class DefaultSqlSession implements SqlSession {


    private Configration configration;

    //sql executor
    private Excutor simpleExcutor = new SimpleExcutor();

    public DefaultSqlSession(Configration configration) {

        this.configration = configration;
    }

    @Override
    public <E> List<E> selectList(String statementId, Object... params) throws IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException {

        //Get MappedStatement object according to statementId
        MappedStatement mappedStatement = configration.getMapperStamentMap().get(statementId);

        //sql executor
        List<Object> query = simpleExcutor.query(configration, mappedStatement, params);
        return (List<E>) query;
    }

    @Override
    public <T> T selectOne(String statementId, Object... params) throws IllegalAccessException, IntrospectionException, InstantiationException, SQLException, InvocationTargetException, NoSuchFieldException {
        List<Object> objects = selectList(statementId, params);
        if (objects==null || objects.size() == 0){
            return null;
        }
        if (objects.size()>1){
            throw new RuntimeException("Multiple values exist!");
        }

        return (T) objects.get(0);
    }

    @Override
    public void close() throws Exception {
        simpleExcutor.close();
    }

}

The SqlSessionFactory factory is defined to produce different sqlsessions to execute sql
Get SqlSession samples and object interfaces

public interface SqlSessionFactory {

    public SqlSession openSqlSession();
}

The specific factory implementation class produces sqlsession objects and performs the operations of adding, deleting, modifying and querying

public class DefaultSqlSessionFactory  implements SqlSessionFactory {


    private Configration configration;

    public DefaultSqlSessionFactory(Configration configration) {
        this.configration = configration;
    }

    @Override
    public SqlSession openSqlSession() {
        return new DefaultSqlSession(configration);
    }

Parsed #{id} into station character? Tool classes, and introspective object creation tool classes
GenericTokenParser

public class GenericTokenParser {

  private final String openToken; //Start tag
  private final String closeToken; //End tag
  private final TokenHandler handler; //Tag processor

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  /**
   * Resolve ${} and #{}
   * @param text
   * @return
   * This method mainly realizes the parsing and processing of placeholders in configuration files, scripts and other fragments, and returns the final required data.
   * Among them, the parsing work is completed by this method, and the processing work is realized by the handleToken() method of the processor handler
   */
  public String parse(String text) {
    // If the parameter is null, an empty string will be returned.
    if (text == null || text.isEmpty()) {
      return "";
    }

    // Next, continue to verify whether the start tag is included. If it is not included, it is not a placeholder by default. Just return it as it is. Otherwise, continue to execute.
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }

   // Convert text into a character array src, define the default offset offset=0, and store the variable builder that finally needs to return the string,
    // The variable name expression corresponding to the placeholder in the text variable. Judge whether start is greater than - 1 (i.e. whether opentoken exists in text). If so, execute the following code
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
     // Judge that if there is an escape character before starting marking, it will not be processed as openToken, otherwise continue processing
      if (start > 0 && src[start - 1] == '\\') {
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        //Reset the expression variable to avoid null pointer or old data interference.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {////When there is an end tag
          if (end > offset && src[end - 1] == '\\') {//If there is an escape character before the end tag
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {//There is no escape character, that is, it needs to be processed as a parameter
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //First, the parameter is processed according to the key (i.e. expression) of the parameter, and the parameter is returned? As placeholder
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

ParameterMapping

public class ParameterMapping {

    private String content;

    public ParameterMapping(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

ParameterMappingTokenHandler

public class ParameterMappingTokenHandler implements TokenHandler {
    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();

    // context is the parameter name #{id} #{username}

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

}

TokenHandler

public interface TokenHandler {
  String handleToken(String content);
}
General test
    String name = "sqlMapConfigration.xml";
    // 1 load xml configuration file
    InputStream inputStream = Resource.inputStream(name);
    // 2. Parsing configuration files
       //Initializing configuration initializing container maperstationmap container saves sql information in mapper
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    // 3 create session
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSqlSession();
    //4 query
    User user = new User();
    user.setId(2);
    user.setName("Xiao Wang");
    
    //Non agent mode
    User user1 = sqlSession.selectOne("User.selectOne", user);
    System.out.println(user1);
    List<User> usersList = sqlSession.selectList("User.selectList");
    System.out.println(usersList); 

This is using namespace ID is hard coded to query, but in the actual process, we directly call dao mapper through the service layer to query and execute;

Therefore, we need to define a mapper interface class like mybatis, and then use dynamic proxy to call and execute;

Define a mapper proxy interface in the SqlSession interface

    /**
     * Mapper Proxy interface
     * @param mapperClass
     * @param <T>
     * @return
     */
    public <T> T getMapper(Class<?> mapperClass);
UserMapper create
public interface UserMapper {

    /**
     * Query all
     * @return
     */
    List<User> selectList2();

    /**
     * Query single criteria
     * @param user
     * @return
     */
    User selectOne2(User user);
}

Newly created mapper2 xml

<!--mapper proxy pattern
 sentence id Must and mapper The method names in the query statements are consistent
 namespace Must be the permission naming of the class

 as a result of JDK The corresponding cannot be provided in the dynamic agent namespace And query statement configuration id
 Therefore, the method name and mapper Class key Get from mapper configuration file sql Input parameter of statement,Return type;
-->

<mapper namespace="com.udeam.test.mapper.UserMapper">

    <!--    Represents a single query-->
    <select id="selectOne2" paramterType="com.udeam.pojo.User" resultType="com.udeam.pojo.User">
        select * from user where id = #{id} and name = #{name}
    </select>


    <!--    Indicates multiple queries-->
    <select id="selectList2" resultType="com.udeam.pojo.User">
        select * from user
    </select>

</mapper>

Then add mapper in SqlMapXml

 <mapper resource="mapper2.xml"></mapper>

Implement the proxy method in a subclass

What needs to be noted here is

  • In the jdk agent below, we cannot get the namespace and sql id values in the xml file
  • In fact, when using mybatis, the method name of mapper agent will be consistent with that in mapper and XML, and namspacs will use the authority of the mapper to name it;

In the JDK dynamic agent, use the method name and full path to obtain the input parameter type and return type of sql in xml from the map object encapsulated by configuration;

    /**
     * Use JDK dynamic proxy to execute mapper
     */
    @Override
    public <T> T getMapper(Class<?> mapperClass) {

        Object o = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //Get the permission and name of class
                String className = method.getDeclaringClass().getName();
                //Get method name
                String name = method.getName();
                //Splice statementid to obtain the sql input parameter type and return type from the map
                String statementid = className + "." + name;
                //Determine whether generic type parameterization is implemented
                Type genericReturnType = method.getGenericReturnType();
                if (genericReturnType instanceof ParameterizedType) {
                    //Or to execute the query method
                    return selectList(statementid, args);
                }
                return selectOne(statementid, args);


            }
        });

       return (T) o;
    }
mapper proxy test
  // Proxy test
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        System.out.println(userMapper.selectList2());

The above only realizes the query operation of a single table and the incoming fixed parameters. The dynamic sql and deletion are realized in the source code at the bottom. You can download Kangkang. The basic implementation method of deletion and addition is the same;

Design patterns used

  • Factory mode
    When creating different sqlsessions, you can select different subclasses of new;
  • proxy pattern
    Proxy Mapper using JDK dynamic proxy
  • Builder model
    In the SqlSessionFactoryBuilder class, the build() method is built through the properties of the configuration object;

Code address

Click download

Tags: Java Mybatis Back-end

Posted by lady1dc on Fri, 06 May 2022 23:17:22 +0300