Getting started with MyBatis: real-world additions, deletions, and changes

A concept

1.What is MyBatis?

MyBatis is an excellent persistence layer framework that supports custom SQL, stored procedures, and advanced mapping. MyBatis eliminates almost all JDBC code, setting parameters, and getting result sets. MyBatis can configure and map raw types, interfaces, and Java POJO s (Plain Old Java Objects, plain old-fashioned Java objects) through simple XML or annotations.Is a record in the database.

2. SqlSessionFactory

Each MyBatis-based application is centered on an instance of SqlSessionFactory. SqlSessionFactory is a compiled memory image of a single database mapping relationship. Instances of SqlSessionFactory can be obtained through SqlSessionFactoryBuilder. SqlSessionFactoryBuilder can be obtained from an XML configuration file or a pre-customized Configuration.The instance builds an instance of SqlSessionFactory, the factory where the SqlSessionFactory was created.

3. SqlSession

SqlSession is an object for persistence operations. It fully contains all the methods required to execute SQL commands against the database. Mapped SqlSession statements can be executed directly through the SqlSession instance. After using SqlSession, we should use the finality block to ensure that it is closed.

4. SqlSessionFactoryBuilder

This class can be instantiated, used, and discarded, and once the SqlSessionFactory is created, it is no longer needed. Therefore, the best scope for the SqlSessionFactoryBuilder instance is the method scope (that is, the local method variable)You can reuse the SqlSessionFactoryBuilder to create multiple instances of SqlSessionFactory, but it is best not to keep it all the time to ensure that all XML parsing resources can be freed up for the more important things.

Two-step operation

2.1 Start the MySQL database service (using containers)

docker run -d --restart always --name mysql -e MYSQL_ROOT_PASSWORD='drh123' -v ${PWD}/data:/var/lib/mysql -p 3306:3306 mysql:5.7.28

2.2 Create libraries, tables

CREATE DATABASE mybatis DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE USER 'mybatis'@'%' IDENTIFIED BY 'drh123';
GRANT ALL PRIVILEGES ON mybatis.* to mybatis@'%' IDENTIFIED BY 'drh123' WITH GRANT OPTION;
CREATE TABLE user(
id int primary key auto_increment,
username varchar(20),
telephone varchar(20)
);
INSERT INTO mybatis.user (id, username, telephone) VALUES (1, 'Maria', '13813813802');
INSERT INTO mybatis.user (id, username, telephone) VALUES (2, 'Ivan', '13813813801');
INSERT INTO mybatis.user (id, username, telephone) VALUES (6, 'John', '13813813803');
INSERT INTO mybatis.user (id, username, telephone) VALUES (7, 'John', '13813813803');
INSERT INTO mybatis.user (id, username, telephone) VALUES (8, 'John', '13813813803');
INSERT INTO mybatis.user (id, username, telephone) VALUES (9, 'John', '13813813803');

2.3 Create a Maven Project

mvn archetype:generate -DgroupId=com.ivandu.mybatis -DartifactId=mybatis -DarchetypeArtifactId=maven-archetype-quickstart

2.4 Configure Maven project pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.ivandu.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <packaging>jar</packaging>
    <version>1.0.0</version>
    <name>mybatis</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>

        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.32</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.32</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/log4j/log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.0</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

It is important to note that when the system prompts that config.xml cannot be found or the corresponding class file cannot be found, you can join in pom.xml:

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
    </build>

2.5 Configure resources/mybatis.cfg.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>

    <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>

    <typeAliases>
        <package name="com.ivandu.mybatis.model"/>
    </typeAliases>

    <!-- To configure mybatis Running Environment -->
    <environments default="development">
        <environment id="development">
            <!-- type="JDBC" Represents direct use JDBC Commit and rollback settings -->
            <transactionManager type="JDBC"/>
            <!-- POOLED Express support JDBC Data Source Connection Pool -->
            <!-- Database connection pool, by Mybatis Administration, database name is mybatis,MySQL User name mybatis,Password is:drh123 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://10.1.1.88:3306/mybatis?useSSL=false"/>
                <property name="username" value="mybatis"/>
                <property name="password" value="drh123"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="com/ivandu/mybatis/mapper/UserMapper.xml"/>
    </mappers>

</configuration>

Load mapper in mybatis.cfg.xml:

<mappers>
    <!-- adopt mapper Interface packages load mapping files for the entire package -->
    <package name="com.ivandu.mybatis.mapper" />
</mappers>

Alias Java Bean:

<!-- by JavaBean From Category Name -->
<typeAliases>
    <!-- Specify a package name as an alias and include Java Class name of class as class name -->
    <package name="com.ivandu.mybatis.model" />
</typeAliases>
  • The logImpl property configuration in < settings > specifies that the log is output using LOG4J.
  • A package alias is configured under the < typeAliases > element, and the fully qualified name of the class is usually used when determining a class, for example, com.ivandu.mybatis.model.User. The fully qualified name of the class is frequently used in MyBatis. For ease of use, we have configuredThe com.ivandu.mybatis.model package allows you to configure it so that you do not need to write the part of the package name when using the class, but just use Country.
  • Database connections are mainly configured in the <environments>environment configuration, and the url of the database is jdbc:mysql://10.1.1.88:3306/mybatis, followed by username and password, respectively, for the database.
  • A CountryMapper.xml containing the full class path is configured in <mappers>, which is the SQL statement and mapping configuration file for MyBatis.

Note the order: typeAliases -> environments -> mappers.

2.6 Create a model

package com.ivandu.mybatis.model;

public class User {

    private Integer id;
    private String username;
    private String telephone;

    public Integer getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getTelephone() {
        return telephone;
    }

    public void setTelephone(String telephone) {
        this.telephone = telephone;
    }
}

2.7 Create mapper

package com.ivandu.mybatis.mapper;

import com.ivandu.mybatis.model.User;

import java.util.List;

public interface UserMapper {
    void insertUser(User user) throws Exception;

    void updateUser(User user) throws Exception;

    void deleteUser(Integer id) throws Exception;

    User selectUserById(Integer id) throws Exception;

    List<User> selectAllUsers() throws Exception;
}

Define an interface within the mapper package that operates on the model, including all methods to add or delete the model. Also create the corresponding mapper xml configuration:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ivandu.mybatis.mapper.UserMapper">
    <!-- Custom Return Result Set -->
    <resultMap id="userMapper" type="User">
        <id property="id" column="id" javaType="Integer"/>
        <result property="username" column="username" javaType="String"/>
        <result property="telephone" column="telephone" javaType="String"/>
    </resultMap>
    <!-- Definition SQL Statement, where id Need to match method name in interface -->
    <!-- useGeneratedKeys: Implement automatic primary key generation -->
    <!-- keyProperty:  Uniquely label an attribute -->
    <!-- parameterType Indicates the type of parameter used in the query. resultType Indicates the type of result set returned by the query -->
    <insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
        insert into user (username, telephone)
        values (#{username}, #{telephone})
    </insert>

    <update id="updateUser" parameterType="User">
        update user
        set username=#{username}
        where id = #{id}
    </update>

    <delete id="deleteUser" parameterType="Integer">
        delete
        from user
        where id = #{id}
    </delete>

    <!-- If not Java Bean From the category name, resultType="com.ivandu.mybatis.model.User" -->
    <!-- Use resultType Make sure you have the same attribute name as the field name; if not, use resultMap -->

    <select id="selectUserById" parameterType="int" resultType="User">
        select *
        from user
        where id = #{id}
    </select>

    <select id="selectAllUsers" resultMap="userMapper">
        select *
        from user
    </select>

</mapper>

2.8 Configure resources/log4j.peoperties

# Global logging configuration
log4j.rootLogger=ERROR, stdout
# mybatis logging
log4j.logger.com.ivandu.mybatis.mapper=TRACE
# Console output
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

The lowest level of MyBatis logging is TRACE, where MyBatis outputs detailed information about the execution of SQL, a level appropriate for development purposes.

2.9 Test

package com.ivandu.mybatis;

import com.ivandu.mybatis.mapper.UserMapper;
import com.ivandu.mybatis.model.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.*;

import java.io.IOException;
import java.io.Reader;
import java.util.List;

public class UserMapperTest {
    private static SqlSessionFactory sqlSessionFactory;

    @BeforeClass
    public static void init() throws IOException {
        Reader reader = Resources.getResourceAsReader("mybatis.cfg.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        reader.close();
    }

// insert record
    @Test
    public void insertUser() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = new User();
        user.setId(3);
        user.setUsername("John");
        user.setTelephone("13813813803");
        try {
            userMapper.insertUser(user);
            sqlSession.commit();
        } catch (Exception e)
        {
            e.printStackTrace();
            sqlSession.rollback();
        } finally {
            sqlSession.close();
        }
    }

// Query using UserMapper interface
    @Test
    public void selectAllUsersMapper() {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            List<User> users = userMapper.selectAllUsers();
            for (User user : users) {
                System.out.printf("%d %s %s\n", user.getId(), user.getUsername(), user.getTelephone());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

// Delete Record
    @Test
    public void deleteUser(){
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        try {
            userMapper.deleteUser(4);
            sqlSession.commit();
        } catch (Exception e) {
            e.printStackTrace();
            sqlSession.rollback();
        } finally {
            sqlSession.close();
        }
    }

// Update Records
    @Test
    public void updateUser(){
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user;
        try {
           user = userMapper.selectUserById(1);
           user.setUsername("Lily");
           userMapper.updateUser(user);
           sqlSession.commit();
        } catch ( Exception e) {
            e.printStackTrace();
            sqlSession.rollback();
        } finally {
            sqlSession.close();
        }
    }

// Query all users
    @Test
    public void selectAllUsers() {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            List<User> userList = sqlSession.selectList("selectAllUsers");
            System.out.println("Find all user information and execute as follows:");
            for (User user: userList) {
                System.out.printf("%d %s %s\n",user.getId(),user.getUsername(),user.getTelephone());
            }
        }
    }

// Query based on user id
    @Test
    public void selectUserById() {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper userMapper =  sqlSession.getMapper(UserMapper.class);
            User user = userMapper.selectUserById(1);
            System.out.println("Press ID Search and the results are as follows:");
            System.out.println(user.getId() + " " + user.getUsername() + " " + user.getTelephone());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Reader reads the mybatis.cfg.xml configuration file through the Resources tool class. Reader is then used to create the SqlSessionFactory factory object using the SqlSessionFactoryBuilder construction class. During the creation of the SqlSessionFactory object, the mybatis.cfg.xml configuration file is parsed first, and the entire UserMapper.xml is read after reading the mappers configuration in the configuration filePerform method-specific parsing, after which the SqlSessionFactory contains all the information about attribute configuration and execution of SQL. Get a SqlSession using the SqlSessionFactory factory object. Find the method id="selectAll" in UserMapper.xml using the selectList method of SqlSession, and execute the SQL query using MyBatis at the bottom level.After JDBC executes SQL and gets the ResultSet of the query result set, it maps the result to a set of User types based on the configuration of the resultType and returns the query result.

In addition, this test uses the exception handling mechanism of ** try-with-resource**, which is briefly described here. Traditional manual release of external resources is typically placed on try{}catch(){}finally{}In the final code block of the mechanism, because statements in the final code block are guaranteed to be executed, that is, to ensure that external resources will eventually be released. Considering that there may be exceptions in the final code block, there is also a try{}catch(){} in the final code block, which is a classic, traditional method of releasing external resources and is obviously very cumbersome.

JDK1.7 was followed by a try-with-resource processing mechanism. Resources that are automatically shut down first need to implement Closeable or AutoCloseable interfaces, since only those interfaces are implemented can automatically call the close() method to automatically shut down resources. Written as try(){}catch(){}, external resources to be shut down are created in try(), catch()The try-with-resource mechanism is actually a syntax sugar and its underlying implementation is still try{}catch(){}finally{} but there is an addSuppressed() in the catch(){} code block.Method, the exception suppression method. If there are exceptions to both business processing and closing connections, the exception to business processing will suppress the exception to close the connection, throwing only the exception in processing, and the exception to close the connection can still be obtained through the getSuppressed() method.

Three Mybatis Configuration

3.1 Profile

The ** configuration ** tag of the MyBatis profile mainly includes:

  • configuration Configuration
  • properties attribute
  • Settings settings
  • TypeeAliases Type Naming
  • TypeeHandlers Type Processor
  • objectFactory object factory
  • plugins plugin
  • environments environment
    • Environment environment variable
    • transactionManager Transaction Manager
  • Database vendor identification for databaseIdProvider
  • mappers mapper

These attributes are externally configurable and dynamically replaceable, such as the Java property file config.properties, which is set up under the directory src/resources on the basis of the previous section, to configure information for some databases, as follows:

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://10.1.1.88:3306/mybatis
username=mybatis
password=drh123

Note: The order in which MyBatis loads properties is as follows (referenced from Chinese Document for MyBatis:properties):

  • The properties specified in the body of the properties element are read first.
  • Then read the attribute file under the class path based on the resource attribute in the properties element or the path specified by the url attribute, overwriting the read attribute with the same name.
  • Finally, the property passed as a method parameter is read, overwriting the property with the same name that has been read.

3.2 settings Settings

Settings are extremely important settings for MyBatis, which can change the runtime behavior of MyBatis, such as turning on secondary caches, turning on delayed loading, and so on.
Refer to settings settings for details Chinese Document for MyBatis: settings .
The purpose of typeHandlers is to convert between JDBC and Java types. The default type handler in MyBatis can basically meet everyday needs. Custom typeHandlers are not described here.

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 UMERIC or FLOAT
DoubleTypeHandlerjava.lang.Double, doubleDatabase Compatible NUMERIC or DOUBLE
BigDecimalTypeHandlerjava.math.BigDecimalDatabase Compatible NUMERIC or DECIMAL
StringTypeHandlerjava.lang.StringCHAR, VARCHAR
ClobReaderTypeHandlerjava.io.Reader-
ClobTypeHandlerjava.lang.StringCLOB, LONGVARCHAR
NStringTypeHandlerjava.lang.StringNVARCHAR, NCHAR
NClobTypeHandlerjava.lang.StringNCLOB
BlobInputStreamTypeHandlerjava.io.InputStream-
ByteArrayTypeHandlerbyte[]Database Compatible Byte Stream Types
BlobTypeHandlerbyte[]BLOB, LONGVARBINARY
DateTypeHandlerjava.util.DateTIMESTAMP
DateOnlyTypeHandlerjava.util.DateDATE
TimeOnlyTypeHandlerjava.util.DateTIME
SqlTimestampTypeHandlerjava.sql.TimestampTIMESTAMP
SqlDateTypeHandlerjava.sql.Date DATE
SqlTimeTypeHandlerjava.sql.Time TIME
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

The environment configuration for MyBatis is actually the configuration of the data source. MyBatis can configure multiple environments to help you map SQL to multiple databases.
Note: Although you can configure multiple environments, each SqlSessionFactory instance can only correspond to one database, and several databases require several SqlSessionFactory instances to be created.
Two ways to accept environment configuration:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader, environment); 
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader, environment,properties); 

If environment parameters are ignored, the default environment will be loaded:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); 
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader,properties); 

3.3 transactionManager Transaction Manager

Two transaction managers in MyBatis, type="[JDBC|MANAGED]":

  • JDBC: Direct use of JDBC commit and rollback settings;
  • MANAGED: Let the container manage the entire life cycle of the transaction.

dataSource data source:
The dataSource element uses the standard JDBC data source interface to configure resources for JDBC connection objects. MyBatis has three built-in data source types, namely type='[UNPOOLED|POOLED|JNDI]', which are described in more detail here:
(1)UNPOOLED
UNPOOLED does not support JDBC data source connection pooling, only by opening and closing connections each time they are requested. It contains properties:

  • driver: The fully qualified name of a JDBC-driven Java class, such as MySQL's com.mysql.cj.jdbc.Driver;
  • url: The JDBC URL address of the database;
  • username: The user name of the database;
  • Password: the password of the database;
  • defaultTransactionIsolationLevel: Default connection transaction isolation level.

(2)POOLED
POOLED supports JDBC data source connection pools and uses the concept of pools to organize JDBC connection objects, avoiding the initialization and authentication time necessary to create new connection instances. In addition to the properties of UNPOOLED, there are also properties such as poolMaximum ActiveConnections, poolMaximum IdleConnections, and so on.
(3)JNDI
JNDI supports external data source connection pooling, which is implemented to be used in containers such as EJB s or application servers that can centralize or externally configure data sources and place a reference to a JNDI context. It contains properties:

  • initial_context: Used to find context in InitialContext;
  • data_source: The path to the context that references the location of the data source instance.

3.4 Mappers

mappers are used to reference a defined mapping file and tell MyBatis where to look for statements that map SQL. Common methods are:
(1) Load a single mapping file through resource:

<mappers>
    <mapper resource="com/ivandu/mybatis/mapper/UserMapper.xml"/>
</mappers>

(2) Load a single mapping file by fully qualifying the resource locator (preceded by the absolute path with file://):

<mappers>
    <mapper url="file:///home/project/MyBatisTest/src/com/ivandu/mybatis/mapper/UserMapper.xml"/>
</mappers>

(3) Load a single mapping file through the mapper interface object:

<mappers>
    <mapper class="com.ivandu.mybatis.mapper.UserMapper" />
</mappers>

(4) Load a map file for the entire package through the mapper interface package:

<mappers>
    <package name="com.ivandu.mybatis.mapper" />
</mappers>

Note: (3) and (4) The mapper interface class name and the mapper.xml mapping file name need to be identical and in the same directory. MyBatis Chinese Document: XML Mapping Profile.

Fourth Alert Release and Troubleshooting

  1. Alert that the "XML tag is Empyt Body" solution is as follows:

    <id property="id" column="id" javaType="Integer"></id>Change to <id property="id" column="id" javaType="Integer"/>

  2. WARN: This connection is using TLSv1.1 which is now deprecated and will be removed in a future release of Connector/J. The solution is as follows:

    JDBC in JDBC:mysql://10.1.1.88:3306/mybatis?useSSL=falseUseSSL for changed from true to false.

Tags: Java MySQL Maven Mybatis SQL

Posted on Sat, 25 Sep 2021 12:44:12 -0400 by ewillms