`

最全Mybatis使用笔记

阅读更多
说明:本篇文章中有部分内容参考自下面相关链接上的内容,小伙伴可自行参考.本文在其基础上进行整理并扩充部分内容.

我们从最开始的 jdbc 开始说起.

public class JdbcUtil {

    // 单机 MySQL 支持的最大连接数是 16384
    private static final String DRIVER_CLASS = "com.mysql.jdbc.Driver";
    private static final String URL = "jdbc:mysql://localhost:3306/guns";
    private static final String USER_NAME = "root";
    private static final String PASSWORD = "123456";

    //最大空闲链接
    private static final int MAX_IDLE = 10000;
    //最的等待时间
    private static final long MAX_WAIT = 30000;
    //最大活动链接
    private static final int MAX_ACTIVE = 1000;
    //初始化时链接池的数量
    private static final int INITIAL_SIZE = 1000;

    private static final BasicDataSource dataSource = new BasicDataSource();

    static {
        dataSource.setDriverClassName(DRIVER_CLASS);
        dataSource.setUrl(URL);
        dataSource.setUsername(USER_NAME);
        dataSource.setPassword(PASSWORD);
        dataSource.setMaxIdle(MAX_IDLE);
        dataSource.setMaxWaitMillis(MAX_WAIT);
        dataSource.setMaxTotal(MAX_ACTIVE);
        dataSource.setInitialSize(INITIAL_SIZE);
    }

    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
}

经过测试:使用 dbcp 创建的线程数最大不超过 1000,而不使用线程池最大可以达到 2111 个. 后续再来研究这是为什么.

使用原生的 JDBC 的话,会有很多问题,比如说 sql 和 java 代码耦合严重,需要根据下标设置参数,最后还要自己遍历结果集获得结果. 所以一般会进行封装,例如对结果集遍历的封装,直接返回对象.



下面我们进入正题:MyBatis 的学习.

下面看下 MyBatis 的架构图


稍后来解释上面的东西,我们还是从一个例子出发进行讲解.

项目结构图:


1.新建 mybatis-config.xml 文件.
<!-- 根标签 -->
<configuration>
    // 配置的连接基本参数.
    <properties>
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/guns?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </properties>

    <!-- 环境,可以配置多个,default:指定采用哪个环境 -->
    <environments default="test">
        <!-- id:唯一标识 -->
        <environment id="test">
            <!-- 事务管理器,JDBC类型的事务管理器 —>
            <transactionManager type="JDBC" />
            <!-- 数据源,池类型的数据源 —>
            <!— 还有 UNPOOLED 和 jndi -—>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis-110" />
                <property name="username" value="root" />
                <property name="password" value="123456" />
            </dataSource>
        </environment>
        <environment id="development">
            <!-- 事务管理器,JDBC类型的事务管理器 -->
            <transactionManager type="JDBC" />
            <!-- 数据源,池类型的数据源 -->
            <dataSource type="POOLED">
                <property name="driver" value="${driver}" /> <!-- 配置了properties,所以可以直接引用 -->
                <property name="url" value="${url}" />
                <property name="username" value="${username}" />
                <property name="password" value="${password}" />
            </dataSource>
        </environment>
    </environments>
</configuration>

2.新建 User 类.
public class User {
    private int id;
    private String account;
    private String password;

    public User(String account, String password) {
        this.account = account;
        this.password = password;
    }

    public int getId() {
        return id;
    }

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

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", account='" + account + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

3.新建 UserMapper.xml 文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- mapper:根标签,namespace:命名空间,随便写,一般保证命名空间唯一 -->
<mapper namespace="userMapper">
    <!-- statement,内容:sql语句。id:唯一标识,随便写,在同一个命名空间下保持唯一
       resultType:sql语句查询结果集的封装类型,tb_user即为数据库中的表
     -->
    <select id="selectUser" resultType="com.hanlin.fadp.dao.user.User">
        select * from tb_user where id = #{id}
    </select>
</mapper>

4.在mybatis-config.xml 文件中配置 mapper.
<mappers>
        <mapper resource="mappers/userMapper.xml"></mapper>
    </mappers>

5.测试
public class MybatisTest {

    private static final String SOURCE_PATH = "mybatis-config.xml";
    @Test
    public void test() throws IOException {
        // 1.构建 SqlSessionFactory
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(SOURCE_PATH));
        // 2.获取 SqlSession
        SqlSession sqlSession = sessionFactory.openSession();
        // 3.操作CRUD
        // 第一个参数:指定statement,规则:命名空间+“.”+statementId
        // 第二个参数:指定传入sql的参数:这里是用户id
        User user = sqlSession.selectOne("userMapper.selectUser", 1);
        System.out.println(user);
    }
}

为了监控,引入日志包.
<dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>

在 resource 下新建 log4j.properties 配置文件.
log4j.rootLogger=DEBUG,A1
log4j.logger.org.apache=DEBUG
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n

这样,在运行的时候就会打印出日志.

下面我们规范下,建 dao 层.
1.新建 UserDao.
public interface UserDao {

    public void insertUser(User user);

    public User queryUserById(int id);
}

2.新建 UserDaoImpl
public class UserDaoImpl implements UserDao{
    private SqlSession sqlSession;

    public UserDaoImpl(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    public void insertUser(User user) {
        this.sqlSession.insert("userMapper.insertUser", user);
    }

    public User queryUserById(int id) {
        return this.sqlSession.selectOne("userMapper.queryUserById", id);
    }
}

3.更新 userMapper.xml 文件为:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- mapper:根标签,namespace:命名空间,随便写,一般保证命名空间唯一 -->
<mapper namespace="userMapper">
    <!-- statement,内容:sql语句。id:唯一标识,随便写,在同一个命名空间下保持唯一
       resultType:sql语句查询结果集的封装类型,tb_user即为数据库中的表
     -->
    <select id="selectUser" resultType="com.hanlin.fadp.dao.user.User">
        select * from tb_user where id = #{id}
    </select>

    <select id="selectUserById" resultType="com.hanlin.fadp.dao.user.User">
        select * from tb_user where id = #{id}
    </select>

    <insert id="insertUser" parameterType="com.hanlin.fadp.dao.user.User">
        insert into tb_user(id, account, password) values(#{id}, #{account},#{password})
    </insert>

</mapper>

4.新建测试用例:
public class MybatisTest {

    private static final String SOURCE_PATH = "mybatis-config.xml";
    SqlSession sqlSession;

    {
        // 1.构建 SqlSessionFactory
        try {
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(SOURCE_PATH));
            // 2.获取 SqlSession
            sqlSession = sqlSessionFactory.openSession();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test() throws IOException {
        // 1.构建 SqlSessionFactory
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(SOURCE_PATH));
        // 2.获取 SqlSession
        SqlSession sqlSession = sessionFactory.openSession();
        // 3.操作CRUD
        // 第一个参数:指定statement,规则:命名空间+“.”+statementId
        // 第二个参数:指定传入sql的参数:这里是用户id
        User user = sqlSession.selectOne("userMapper.selectUser", 1);
        System.out.println(user);
    }

    @Test
    public void testInsertUser(){
        UserDaoImpl userDao = new UserDaoImpl(sqlSession);
        userDao.insertUser(new User("xiaoheng", "123456"));
        this.sqlSession.commit();
    }

}

测试结果:
Sat Mar 07 20:31:22 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
2020-03-07 20:31:23,182 [main] [org.apache.ibatis.datasource.pooled.PooledDataSource]-[DEBUG] Created connection 1884122755.
2020-03-07 20:31:23,182 [main] [org.apache.ibatis.transaction.jdbc.JdbcTransaction]-[DEBUG] Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@704d6e83]
2020-03-07 20:31:23,189 [main] [userMapper.insertUser]-[DEBUG] ==>  Preparing: insert into tb_user(id, account, password) values(?, ?,?)
2020-03-07 20:31:23,250 [main] [userMapper.insertUser]-[DEBUG] ==> Parameters: 0(Integer), xiaoheng(String), 123456(String)
2020-03-07 20:31:23,255 [main] [userMapper.insertUser]-[DEBUG] <==    Updates: 1
2020-03-07 20:31:23,256 [main] [org.apache.ibatis.transaction.jdbc.JdbcTransaction]-[DEBUG] Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@704d6e83]

现在的包结构:


注意:如果出现数据库字段名和类定义中的字段名不一致的情况,有如下几种办法可以解决.
1.在 sql 中使用别名.
2.在 resultMap 中进行映射.
<select id="selectUser" resultMap="UserMap">
  select * from user
</select>
<!--设置结果的映射类型-->
<resultMap id="UserMap" type="User">
    <!--
    一般通过id标签来映射主键
    column = 数据库的列名
    property = 结果集对应的数据库列名的映射名
    -->
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="pwd" property="password"/>
</resultMap>
3.参考驼峰匹配 — mybatis-config.xml 的时候
<settings>
      <setting name="mapUnderscoreToCamelCase" value="true" />
  </settings>

我们现在思考一个问题:
UserDaoImpl 这个类是否是必须的? 答案是不一定.

补充一点知识:
不知道有的小伙伴是否有这么一个疑问,为啥有的接口的参数中使用了 @Param 注解,有的没有,why?
1.多参数时使用 @Param 注解.
@Mapper
public interface UserMapper {
    Integer insert(@Param("username") String username, @Param("address") String address);
}

<insert id="insert" parameterType="org.javaboy.helloboot.bean.User">
    insert into user (username,address) values (#{username},#{address});
</insert>


2.方法参数要取别名,需要 @Param 注解
@Mapper
public interface UserMapper {
    User getUserByUsername(@Param("name") String username);
}

<select id="getUserByUsername" parameterType="org.javaboy.helloboot.bean.User">
    select * from user where username=#{name};
</select>

3.XML 中的 SQL 使用了 $ ,那么参数中也需要 @Param 注解
注意:$ 会有注入漏洞的问题,但是有的时候你不得不使用 $ 符号,例如要传入列名或者表名的时候,这个时候必须要添加 @Param 注解.
换句话说,包含 $ 的 sql 语句不会预先编译.

@Mapper
public interface UserMapper {
    List<User> getAllUsers(@Param("order_by")String order_by);
}

<select id="getAllUsers" resultType="org.javaboy.helloboot.bean.User">
    select * from user
    <if test="order_by!=null and order_by!=''">
        order by ${order_by} desc
    </if>
</select>

4.那就是动态 SQL ,如果在动态 SQL 中使用了参数作为变量,那么也需要 @Param 注解,即使你只有一个参数
@Mapper
public interface UserMapper {
    List<User> getUserById(@Param("id")Integer id);
}

<select id="getUserById" resultType="org.javaboy.helloboot.bean.User">
    select * from user
    <if test="id!=null">
        where id=#{id}
    </if>
</select>

参考:https://www.cnblogs.com/lenve/p/11229590.html

我们现在来看一个例子.
1.新建一个 mapper 类.
public interface UserMapper {

    public User queryUserMapperById(int id);

    public void insertUserMapper(User user);
}

2.新建 userMapper2.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- mapper:根标签,namespace:命名空间,随便写,一般保证命名空间唯一 -->
<mapper namespace="com.hanlin.fadp.dao.mapper.UserMapper">
    <!-- statement,内容:sql语句。id:唯一标识,随便写,在同一个命名空间下保持唯一
       resultType:sql语句查询结果集的封装类型,tb_user即为数据库中的表
     -->
    <select id="queryUserMapperById" resultType="com.hanlin.fadp.dao.user.User">
        select * from tb_user where id = #{id}
    </select>

    <insert id="insertUserMapper" parameterType="com.hanlin.fadp.dao.user.User">
        insert into tb_user(id, account, password) values(#{id}, #{account},#{password})
    </insert>

</mapper>

3.测试
public class MybatisTest2 {

    private static final String SOURCE_PATH = "mybatis-config.xml";
    SqlSession sqlSession;

    {
        // 1.构建 SqlSessionFactory
        try {
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(SOURCE_PATH));
            // 2.获取 SqlSession
            sqlSession = sqlSessionFactory.openSession();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Test
    public void testInsertUserMapper(){
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        mapper.insertUserMapper(new User("abc", "123456"));
        sqlSession.commit();
    }

}

根据这两种方式,会发现少写了一个类. UserDaoImpl 这个类. 我们大胆的猜测下,这块是通过动态代理实现的.
现在的目录结构:


动态代理总结:
1.Mapper 的 namespace 属性必须和 mapper 接口的全路径一致.
2.Mapper 接口的方法名必须和 sql 中的 id 一致.
3.Mapper 接口中方法的输入参数必须和 sql 中的 parameterType 一致.
4.Mapper 接口中方法的输出参数必须和 sql 中的 resultType 一致.

mybatis-config.xml 文件讲究严格的定义顺序,文档顶层结构如下:


properties 属性读取外部资源.

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

然后其中的属性可以这么用:
<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

这个例子中的 username 和 password 将会由 properties 元素中设置的相应值来替换。 driver 和 url 属性将会由 config.properties 文件中对应的值来替换。这样就为配置提供了诸多灵活选择.

也可以这么传递:
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, props);
// ... or ...
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, props);

优先级:properties 中定义的 property < resource < 方法传递.

settings 设置


typeAliases
类型别名是为 Java 类型命名的一个短的名字. 它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余.

<typeAliases>
    <typeAlias type=“com.hanlin.fado.model.User" alias="User"/>
</typeAliases>

缺点:每个pojo类都要去配置。
解决方案:使用扫描包,扫描指定包下的所有类,扫描之后的别名就是类名(不区分大小写),建议使用的时候和类名一致。
<typeAliases>
    <!--type:实体类的全路径。alias:别名,通常首字母大写-->
    <!--<typeAlias type=“com.hanlin.fadp.model.User" alias="User"/>-->
    <package name=“com.hanlin.fadp.model"/>
</typeAliases>

Mybatis已经为普通的 Java 类型内建了许多相应的类型别名。它们都是大小写不敏感的


typeHandlers(类型处理器)

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。可以重写类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型.

plugins(插件)拦截器

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)



现在一些MyBatis 插件比如PageHelper都是基于这个原理,有时为了监控sql执行效率,也可以使用插件机制
原理:

自定义拦截器步骤:

1.在 mybatis-config.xml 文件中配置如下内容:
<plugins>
        <plugin interceptor="com.hanlin.fadp.dao.interceptor.ExampleInterceptor"></plugin>
    </plugins>

2.写一个类实现 Interceptor 接口.
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class ExampleInterceptor implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        System.out.println(target);
        return invocation.proceed();
    }

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

    public void setProperties(Properties properties) {

    }
}

environments(环境)

MyBatis 可以配置成适应多种环境,例如,开发、测试和生产环境需要有不同的配置;
尽管可以配置多个环境,每个 SqlSessionFactory 实例只能选择其一。
虽然,这种方式也可以做到很方便的分离多个环境,但是实际使用场景下,我们更多的是选择使用spring来管理数据源,来做到环境的分离

mappers

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

<mappers>
    <mapper resource="mappers/MyMapper.xml"/>
    <mapper resource="mappers/UserDaoMapper.xml"/>
    <!--注解方式可以使用如下配置方式-->
    <mapper class="com.zpc.mybatis.dao.UserMapper"/>
</mappers>

Mapper XML 文件.

select
select – 书写查询sql语句
select中的几个属性说明:
id属性:当前名称空间下的statement的唯一标识。必须。要求id和mapper接口中的方法的名字一致。
resultType:将结果集映射为java的对象类型。必须(和 resultMap 二选一)
parameterType:传入参数类型。可以省略

insert
insert 的几个属性说明:
id:唯一标识,随便写,在同一个命名空间下保持唯一,使用动态代理之后要求和方法名保持一致
parameterType:参数的类型,使用动态代理之后和方法的参数类型一致
useGeneratedKeys:开启主键回写
keyColumn:指定数据库的主键
keyProperty:主键对应的pojo属性名
标签内部:具体的sql语句。

update
id属性:当前名称空间下的statement的唯一标识(必须属性);
parameterType:传入的参数类型,可以省略。
标签内部:具体的sql语句。

delete
delete 的几个属性说明:
id属性:当前名称空间下的statement的唯一标识(必须属性);
parameterType:传入的参数类型,可以省略。
标签内部:具体的sql语句。

#{} 和 ${}
比如现在有这么一个需求,由于历史原因,现在有两张表,一张是新表,一张是旧表.
<select id="queryUserByTableName" resultType="com.zpc.mybatis.pojo.User">
    select * from #{tableName}
</select>

/**
* 根据表名查询用户信息(直接使用注解指定传入参数名称)
*
* @param tableName
* @return
*/
public List<User> queryUserByTableName(String tableName);



改正:
<select id="queryUserByTableName" resultType="com.zpc.mybatis.pojo.User">
    select * from ${tableName}
</select>

其实这个问题和我上面说的 @Param 的用法有关联. #{} 的 sql 会预编译,而 ${} 是拼接.

当使用 #{} 多个参数的时候:
/**
* 登录(直接使用注解指定传入参数名称)
*
* @param userName
* @param password
* @return
*/
public User login( String userName, String password);

<select id="login" resultType="com.zpc.mybatis.pojo.User">
    select * from tb_user where user_name = #{userName} and password = #{password}
</select>

会报如下错:
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database.  Cause: org.apache.ibatis.binding.BindingException: Parameter 'userName' not found. Available parameters are [0, 1, param1, param2]
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'userName' not found. Available parameters are [0, 1, param1, param2]

解决办法:
<select id="login" resultType="com.zpc.mybatis.pojo.User">
    select * from tb_user where user_name = #{0} and password = #{1}
</select>

<select id="login" resultType="com.zpc.mybatis.pojo.User">
    select * from tb_user where user_name = #{param1} and password = #{param2}
</select>

但是第一种方案不知道传的变量是啥,第二种需要构造一个 mapper,太麻烦. 最终方案如下:
/**
* 登录(直接使用注解指定传入参数名称)
*
* @param userName
* @param password
* @return
*/
public User login(@Param("userName") String userName, @Param("password") String password);

<select id="login" resultType="com.zpc.mybatis.pojo.User">
    select * from tb_user where user_name = #{userName} and password = #{password}
</select>

注意:
通常在方法的参数列表上加上一个注释@Param(“xxxx”) 显式指定参数的名字,然后通过${“xxxx”}或#{“xxxx”}
sql语句动态生成的时候,使用${};
sql语句中某个参数进行占位的时候#{}

resultMap 是 Mybatis 功能最强大的东西.




sql 片段.
是将公共 sql 部分抽取出来,定义成通用的,减少书写.
<sql id=””></sql>
<include refId=”” />

<sql id="commonSql">
        id,
            user_name,
            password,
            name,
            age,
            sex,
            birthday,
            created,
            updated   
</sql>

<select id="queryUserById" resultMap="userResultMap">
    select <include refid="commonSql"></include> from tb_user where id = #{id}
</select>

<select id="queryUsersLikeUserName" resultType="User">
    select <include refid="commonSql"></include> from tb_user where user_name like "%"#{userName}"%"
</select>

<mappers>
        <mapper resource="CommonSQL.xml"/>
        <!-- 开启mapper接口的包扫描,基于class的配置方式 -->
        <package name="com.zpc.mybatis.mapper"/>
</mappers>

动态 sql.

if 的使用.
/**
* 查询男性用户,如果输入了姓名,则按姓名查询
* @param name
* @return
*/
List<User> queryUserList(@Param("name") String name);

<select id="queryUserList" resultType="com.zpc.mybatis.pojo.User">
    select * from tb_user WHERE sex=1
    <if test="name!=null and name.trim()!=''">
      and name like '%${name}%'
    </if>
</select>

choose when otherwise 的使用.
/**
* 查询男性用户,如果输入了姓名则按照姓名模糊查找,否则如果输入了年龄则按照年龄查找,否则查找姓名为“鹏程”的用户。
* @param name
* @param age
* @return
*/
List<User> queryUserListByNameOrAge(@Param("name") String name,@Param("age") Integer age);

<select id="queryUserListByNameOrAge" resultType="com.zpc.mybatis.pojo.User">
    select * from tb_user WHERE sex=1
    <!--
    1.一旦有条件成立的when,后续的when则不会执行
    2.当所有的when都不执行时,才会执行otherwise
    -->
    <choose>
        <when test="name!=null and name.trim()!=''">
            and name like '%${name}%'
        </when>
        <when test="age!=null">
            and age = #{age}
        </when>
        <otherwise>
            and name='鹏程'
        </otherwise>
    </choose>
</select>

where and set 的使用.
/**
* 查询所有用户,如果输入了姓名按照姓名进行模糊查询,如果输入年龄,按照年龄进行查询,如果两者都输入,两个条件都要成立
* @param name
* @param age
* @return
*/
List<User> queryUserListByNameAndAge(@Param("name") String name,@Param("age") Integer age);

<select id="queryUserListByNameAndAge" resultType="com.zpc.mybatis.pojo.User">
    select * from tb_user
    <!--如果多出一个and,会自动去除,如果缺少and或者多出多个and则会报错-->
    <where>
        <if test="name!=null and name.trim()!=''">
            and name like '%${name}%'
        </if>
        <if test="age!=null">
            and age = #{age}
        </if>
    </where>
</select>

/**
* 根据id更新用户信息
*
* @param user
*/
public void updateUser(User user);

<update id="updateUser" parameterType="com.zpc.mybatis.pojo.User">
    UPDATE tb_user
    <trim prefix="set" suffixOverrides=",">
        <if test="userName!=null">user_name = #{userName},</if>
        <if test="password!=null">password = #{password},</if>
        <if test="name!=null">name = #{name},</if>
        <if test="age!=null">age = #{age},</if>
        <if test="sex!=null">sex = #{sex},</if>
        <if test="birthday!=null">birthday = #{birthday},</if>
        updated = now(),
    </trim>
    WHERE
    (id = #{id});
</update>

foreach 的使用
/**
* 按多个Id查询
* @param ids
* @return
*/
List<User> queryUserListByIds(@Param("ids") String[] ids);

<select id="queryUserListByIds" resultType="com.zpc.mybatis.pojo.User">
    select * from tb_user where id in
    <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>

缓存

1级缓存:


sqlSession.clearCache();可以强制清除缓存

@Test
public void testQueryUserById() {
    System.out.println(this.userMapper.queryUserById("1"));
    sqlSession.clearCache();
    System.out.println(this.userMapper.queryUserById("1"));
}

注意:执行update、insert、delete的时候,会清空缓存.

mybatis 的二级缓存的作用域是一个mapper的namespace ,同一个namespace中查询sql可以从缓存中命中.

<mapper namespace="com.zpc.mybatis.dao.UserMapper">
    <cache/>
</mapper>

注意:开启二级缓存必须序列化.

public class User implements Serializable{
    private static final long serialVersionUID = -3330851033429007657L;

关闭二级缓存.
<settings>
    <!--开启驼峰匹配-->
    <setting name="mapUnderscoreToCamelCase" value="true"/>
    <!--开启二级缓存,全局总开关,这里关闭,mapper中开启了也没用-->
    <setting name="cacheEnabled" value="false"/>
</settings>


高级查询.


创建order表:
CREATE TABLE tb_order (
id int(11) NOT NULL AUTO_INCREMENT,
user_id int(11) DEFAULT NULL,
order_number varchar(255) DEFAULT NULL,
create datetime DEFAULT NULL,
updated datetime DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

public class Order {
    private Integer id;
    private Long userId;
    private String orderNumber;
    private Date created;
    private Date updated;
}

1对1查询.
public class OrderUser extends Order {
    private String userName;
    private String password;
    private String name;
    private Integer age;
    private Integer sex;
    private Date birthday;
    private Date created;
    private Date updated;
}

public interface OrderMapper {
     OrderUser queryOrderUserByOrderNumber(@Param("number") String number);
}

<mapper namespace="com.zpc.mybatis.dao.OrderMapper">
    <select id="queryOrderUserByOrderNumber" resultType="com.zpc.mybatis.pojo.OrderUser">
      select * from tb_order o left join tb_user u on o.user_id=u.id where o.order_number = #{number}
   </select>
</mapper>

public class Order {
    private Integer id;
    private Long userId;
    private String orderNumber;
    private Date created;
    private Date updated;
    private User user;
}

/**
* 根据订单号查询订单用户的信息
* @param number
* @return
*/
Order queryOrderWithUserByOrderNumber(@Param("number") String number);

<resultMap id="OrderUserResultMap" type="com.zpc.mybatis.pojo.Order" autoMapping="true">
     <id column="id" property="id"/>
     <!--association:完成子对象的映射-->
     <!--property:子对象在父对象中的属性名-->
     <!--javaType:子对象的java类型-->
     <!--autoMapping:完成子对象的自动映射,若开启驼峰,则按驼峰匹配-->
     <association property="user" javaType="com.zpc.mybatis.pojo.User" autoMapping="true">
         <id column="user_id" property="id"/>
     </association>
</resultMap>

<select id="queryOrderWithUserByOrderNumber" resultMap="OrderUserResultMap">
   select * from tb_order o left join tb_user u on o.user_id=u.id where o.order_number = #{number}
</select>

1对多查询.
public class Order {
    private Integer id;
    private Long userId;
    private String orderNumber;
    private Date created;
    private Date updated;
    private User user;
    private List<OrderDetail> detailList;
}

public class OrderDetail {
    private Integer id;
    private Integer orderId;
    private Double totalPrice;
    private Integer status;
}

/**
* 根据订单号查询订单用户的信息及订单详情
* @param number
* @return
*/
Order queryOrderWithUserAndDetailByOrderNumber(@Param("number") String number);

<resultMap id="OrderUserDetailResultMap" type="com.zpc.mybatis.pojo.Order" autoMapping="true">
    <id column="id" property="id"/>
    <!--collection:定义子对象集合映射-->
    <!--association:完成子对象的映射-->
    <!--property:子对象在父对象中的属性名-->
    <!--javaType:子对象的java类型-->
    <!--autoMapping:完成子对象的自动映射,若开启驼峰,则按驼峰匹配-->
    <association property="user" javaType="com.zpc.mybatis.pojo.User" autoMapping="true">
        <id column="user_id" property="id"/>
    </association>
    <collection property="detailList" javaType="List" ofType="com.zpc.mybatis.pojo.OrderDetail" autoMapping="true">
        <id column="id" property="id"/>
    </collection>
</resultMap>

<select id="queryOrderWithUserAndDetailByOrderNumber" resultMap="OrderUserDetailResultMap">
   select * from tb_order o
   left join tb_user u on o.user_id=u.id
   left join tb_orderdetail od on o.id=od.order_id
   where o.order_number = #{number}
</select>

多对对查询.
public class OrderDetail {
    private Integer id;
    private Integer orderId;
    private Double totalPrice;
    private Integer status;
    private Item item;
}

public class Item {
    private Integer id;
    private String itemName;
    private Float itemPrice;
    private String itemDetail;
}

/**
* 根据订单号查询订单用户的信息及订单详情及订单详情对应的商品信息
* @param number
* @return
*/
Order queryOrderWithUserAndDetailItemByOrderNumber(@Param("number") String number);

<resultMap id="OrderUserDetailItemResultMap" type="com.zpc.mybatis.pojo.Order" autoMapping="true">
    <id column="id" property="id"/>
    <association property="user" javaType="com.zpc.mybatis.pojo.User" autoMapping="true">
        <id column="user_id" property="id"/>
    </association>
    <collection property="detailList" javaType="List" ofType="com.zpc.mybatis.pojo.OrderDetail" autoMapping="true">
        <id column="detail_id" property="id"/>
        <association property="item" javaType="com.zpc.mybatis.pojo.Item" autoMapping="true">
            <id column="item_id" property="id"/>
        </association>
    </collection>
</resultMap>

<select id="queryOrderWithUserAndDetailItemByOrderNumber" resultMap="OrderUserDetailItemResultMap">
   select * ,od.id as detail_id from tb_order o
   left join tb_user u on o.user_id=u.id
   left join tb_orderdetail od on o.id=od.order_id
   left join tb_item i on od.item_id=i.id
   where o.order_number = #{number}
</select>

数据库脚本.
CREATE TABLE tb_order (
id int(11) NOT NULL AUTO_INCREMENT,
user_id int(11) DEFAULT NULL,
order_number varchar(255) DEFAULT NULL,
create datetime DEFAULT NULL,
updated datetime DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO tb_order VALUES (‘1’, ‘2’, ‘201807010001’, ‘2018-07-01 19:38:35’, ‘2018-07-01 19:38:40’);

CREATE TABLE tb_item (
id int(11) NOT NULL,
itemName varchar(255) DEFAULT NULL,
itemPrice decimal(10,2) DEFAULT NULL,
itemDetail varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO tb_item VALUES (‘1’, ‘袜子’, ‘29.90’, ‘香香的袜子’);
INSERT INTO tb_item VALUES (‘2’, ‘套子’, ‘99.99’, ‘冈本001’);

CREATE TABLE tb_orderdetail (
id int(11) NOT NULL AUTO_INCREMENT,
order_id int(11) DEFAULT NULL,
total_price decimal(10,0) DEFAULT NULL,
item_id int(11) DEFAULT NULL,
status int(10) unsigned zerofill DEFAULT NULL COMMENT ‘0成功非0失败’,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO tb_orderdetail VALUES (‘1’, ‘1’, ‘10000’, ‘1’, ‘0000000001’);
INSERT INTO tb_orderdetail VALUES (‘2’, ‘1’, ‘2000’, ‘2’, ‘0000000000’);

resultMap 的继承.


延迟加载:







sql 中出现 < 的解决办法.

第一种:
SELECT * FROM test WHERE 1 = 1 AND start_date <= CURRENT_DATE AND end_date >= CURRENT_DATE

第二种:
<![CDATA[
     and mm.ttime > to_date(#{startDateTime},'yyyy-mm-dd hh24:mi:ss')
     and mm.ttime <= to_date(#{endDateTime},'yyyy-mm-dd hh24:mi:ss')
]]> 


重头戏:Spring 集成 Mybatis.

pom.xml 文件引入:
<!--数据库连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.8</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.2.2</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>4.1.3.RELEASE</version>
</dependency>
<!--spring集成Junit测试-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>4.1.3.RELEASE</version>
    <scope>test</scope>
</dependency>
<!--spring容器-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.1.3.RELEASE</version>
</dependency>

2.spring 配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-4.0.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/utilhttp://www.springframework.org/schema/util/spring-util-4.0.xsd">

    <!-- 加载配置文件 -->
    <context:property-placeholder location="classpath:properties/*.properties"/>
    <!-- 数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
          destroy-method="close">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url"
                  value="jdbc:mysql://${jdbc.host}:3306/${jdbc.database}?useUnicode=true&amp;characterEncoding=utf-8&amp;zeroDateTimeBehavior=convertToNull"/>
        <property name="username" value="${jdbc.userName}"/>
        <property name="password" value="${jdbc.passWord}"/>
        <!-- 初始化连接大小 -->
        <property name="initialSize" value="${jdbc.initialSize}"></property>
        <!-- 连接池最大数据库连接数  0 为没有限制 -->
        <property name="maxActive" value="${jdbc.maxActive}"></property>
        <!-- 连接池最大的空闲连接数,这里取值为20,表示即使没有数据库连接时依然可以保持20空闲的连接,而不被清除,随时处于待命状态 0 为没有限制 -->
        <property name="maxIdle" value="${jdbc.maxIdle}"></property>
        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="${jdbc.minIdle}"></property>
        <!--最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制-->
        <property name="maxWait" value="${jdbc.maxWait}"></property>
    </bean>

    <!-- spring和MyBatis完美整合 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 自动扫描mapping.xml文件 -->
        <property name="mapperLocations" value="classpath:mappers/*.xml"></property>
        <!--如果mybatis-config.xml没有特殊配置也可以不需要下面的配置-->
        <property name="configLocation" value="classpath:mybatis-config.xml" />
    </bean>

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.zpc.mybatis.dao"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
    </bean>

    <!-- (事务管理)transaction manager -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

3.db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.host=localhost
jdbc.database=ssmdemo
jdbc.userName=root
jdbc.passWord=123456
jdbc.initialSize=0
jdbc.maxActive=20
jdbc.maxIdle=20
jdbc.minIdle=1
jdbc.maxWait=1000

4.测试
import com.zpc.mybatis.dao.UserMapper;
import com.zpc.mybatis.pojo.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.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

//目标:测试一下spring的bean的某些功能
@RunWith(SpringJUnit4ClassRunner.class)//junit整合spring的测试//立马开启了spring的注解
@ContextConfiguration(locations="classpath:spring/applicationContext-*.xml")//加载核心配置文件,自动构建spring容器
public class UserMapperSpringTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testQueryUserByTableName() {
        List<User> userList = this.userMapper.queryUserByTableName("tb_user");
        for (User user : userList) {
            System.out.println(user);
        }
    }

    @Test
    public void testLogin() {
        System.out.println(this.userMapper.login("hj", "123456"));
    }

    @Test
    public void testQueryUserById() {
        System.out.println(this.userMapper.queryUserById("1"));
        User user = new User();
        user.setName("美女");
        user.setId("1");
        userMapper.updateUser(user);

        System.out.println(this.userMapper.queryUserById("1"));
    }

    @Test
    public void testQueryUserAll() {
        List<User> userList = this.userMapper.queryUserAll();
        for (User user : userList) {
            System.out.println(user);
        }
    }

    @Test
    public void testInsertUser() {
        User user = new User();
        user.setAge(20);
        user.setBirthday(new Date());
        user.setName("大神");
        user.setPassword("123456");
        user.setSex(2);
        user.setUserName("bigGod222");
        this.userMapper.insertUser(user);
        System.out.println(user.getId());
    }

    @Test
    public void testUpdateUser() {
        User user = new User();
        user.setBirthday(new Date());
        user.setName("静静");
        user.setPassword("123456");
        user.setSex(0);
        user.setUserName("Jinjin");
        user.setId("1");
        this.userMapper.updateUser(user);
    }

    @Test
    public void testDeleteUserById() {
        this.userMapper.deleteUserById("1");
    }

    @Test
    public void testqueryUserList() {
        List<User> users = this.userMapper.queryUserList(null);
        for (User user : users) {
            System.out.println(user);
        }
    }

    @Test
    public void queryUserListByNameAndAge() throws Exception {
        List<User> users = this.userMapper.queryUserListByNameAndAge("鹏程", 20);
        for (User user : users) {
            System.out.println(user);
        }
    }

    @Test
    public void queryUserListByNameOrAge() throws Exception {
        List<User> users = this.userMapper.queryUserListByNameOrAge(null, 16);
        for (User user : users) {
            System.out.println(user);
        }
    }

    @Test
    public void queryUserListByIds() throws Exception {
        List<User> users = this.userMapper.queryUserListByIds(new String[]{"5", "2"});
        for (User user : users) {
            System.out.println(user);
        }
    }

Spring Boot 集成 Mybatis

Mybatis Generator 的使用:

1.引入相关 jar.
<!-- mybatis-generator自动生成代码插件 -->
<plugin>
   <groupId>org.mybatis.generator</groupId>
   <artifactId>mybatis-generator-maven-plugin</artifactId>
   <version>1.3.5</version>
</plugin>

2.配置 generator.properties 文件到 resource 下.
#generatorConfig Info
generator.location=D:\\software\\maven\\apache-maven-3.3.9\\repository\\mysql\\mysql-connector-java\\5.1.32\\mysql-connector-java-5.1.32.jar
generator.targetPackage=com.zpc.videoshow.generated
#gererator.schema=oracle-schema
gererator.tableName=video_info
gererator.domainObjectName=VideoInfo

jdbc.driver=com.mysql.jdbc.Driver
jdbc.host=jdbc:mysql://localhost:3306/videoshow
jdbc.userName=root
jdbc.passWord=123456
jdbc.initialSize=0
jdbc.maxActive=20
jdbc.maxIdle=20
jdbc.minIdle=1
jdbc.maxWait=1000

3.generatorConfig.xml 文件到 resource 下.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <!-- 引入配置文件 -->
    <properties resource="generator.properties"/>
    <!-- 数据库驱动包位置,路径请不要有中文-->
    <!-- <classPathEntry location="D:\software\lib\mysql-connector-java-5.1.21.jar" /> -->
    <classPathEntry location="${generator.location}"/>
    <!-- 一个数据库一个context-->
    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!-- 生成的pojo,将implements Serializable -->
        <plugin type="org.mybatis.generator.plugins.SerializablePlugin"></plugin>

        <!-- 注释 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true"/><!-- 是否取消注释 -->
            <!-- <property name="suppressDate" value="true" />  是否生成注释代时间戳 -->
        </commentGenerator>

        <!-- 数据库链接URL、用户名、密码 -->
        <!-- <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/sy" userId="sypro" password="sypro"> -->
        <jdbcConnection driverClass="${jdbc.driver}" connectionURL="${jdbc.host}" userId="${jdbc.userName}"
                        password="${jdbc.passWord}">
        </jdbcConnection>

        <!-- 类型转换 -->
        <javaTypeResolver>
            <!-- 默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer true,把JDBC DECIMAL
                和 NUMERIC 类型解析为java.math.BigDecimal -->
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>

        <!-- 生成model模型,设置对应的包名(targetPackage)和存放路径(targetProject)。targetProject可以指定具体的路径,如./src/main/java,也可以使用MAVEN来自动生成,这样生成的代码会在target/generatord-source目录下 -->
        <javaModelGenerator targetPackage="${generator.targetPackage}" targetProject="./src/main/java">
            <!-- 是否在当前路径下新加一层schema,eg:false路径com.oop.eksp.user.model 而true:com.oop.eksp.user.model.[schemaName] -->
            <property name="enableSubPackages" value="false"/>
            <!-- 从数据库返回的值被清理前后的空格 -->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!--对应的mapper.xml文件 -->
        <sqlMapGenerator targetPackage="${generator.targetPackage}" targetProject="./src/main/java">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!-- 对应的Mapper接口类文件 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="${generator.targetPackage}"
                             targetProject="./src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!-- 列出要生成代码的所有表,这里配置的是不生成Example文件 -->
        <!-- schema即为数据库名tableName为对应的数据库表 domainObjectName是要生成的实体类 enable*ByExample是否生成 example类   -->
        <table tableName="${gererator.tableName}" domainObjectName="${gererator.domainObjectName}"
               schema="${gererator.schema}"
               enableCountByExample="false" enableUpdateByExample="false"
               enableDeleteByExample="false" enableSelectByExample="false"
               selectByExampleQueryId="false">
            <!-- 忽略列,不生成bean 字段
            <ignoreColumn column="FRED" />-->
            <!-- 指定列的java数据类型
            <columnOverride column="LONG_VARCHAR_FIELD" jdbcType="VARCHAR" />  -->
            <!-- 用于指定生成实体类时是否使用实际的列名作为实体类的属性名。false是 Camel Case风格-->
            <property name="useActualColumnNames" value="false"/>
        </table>
    </context>
</generatorConfiguration>

4.运行 generator 插件.


参考:
    ① https://blog.csdn.net/hellozpc/article/details/80878563
    ② https://blog.csdn.net/jiaqingShareing/article/details/81840657
0
1
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics