• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

MyBatis基础概念

武飞扬头像
技术烧烤屋
帮助1

什么是MyBatis?

MyBatis是一款优秀的支持自定义SQL查询、存储过程和高级映射的持久层框架,消除了几乎所有的JDBC代码和参数的手动设置以及结果集的检索。MyBatis可以使用XML或注解进行配置和映射,MyBatis通过将参数映射到配置的SQL形成最终执行的SQL语句,最后将执行SQL的结果映射成Java对象返回。

以下是MyBatis的一些重要特性和优势:

  1. 灵活的SQL映射:MyBatis允许开发者将SQL语句与Java方法进行映射,这样可以直接调用Java方法来执行数据库操作,避免了直接编写SQL语句的繁琐。
  2. 强大的动态SQL支持:MyBatis支持动态SQL,允许根据条件动态生成SQL语句,大大提高了SQL语句的可维护性和复用性。
  3. 支持存储过程和函数调用:MyBatis能够直接调用数据库的存储过程和函数,方便处理复杂的业务逻辑。
  4. 缓存机制:MyBatis支持多级缓存,可以提高查询性能,减少数据库访问次数。
  5. 映射注解支持:除了XML配置文件外,MyBatis还支持使用注解进行对象与数据库表之间的映射,简化了配置过程。
  6. 与Spring无缝整合:MyBatis可以很方便地与Spring框架整合,使得事务管理更加简单。
  7. 可扩展性:MyBatis提供了插件机制,可以通过自定义插件来扩展框架的功能。

MyBatis的核心组件有哪些?

  • SqlSessionFactoryBuilder(构造器):用于创建SqlSessionFactory的构建器。通过配置文件或代码,将数据源配置信息解析为SqlSessionFactory实例。
  • SqlSessionFactory(工厂接口):用于创建SqlSession的工厂。SqlSessionFactory是线程安全的,并且是一个重量级对象,通常在应用启动时创建一次,然后在整个应用生命周期内重复使用。
  • SqlSession(会话):MyBatis的主要接口之一,它是数据库会话的实例。SqlSession提供了在数据库中执行SQL语句的方法,以及获取映射器(Mapper)的方法。
  • Mapper接口:Mapper接口是用于执行数据库操作的Java接口。Mapper接口的方法与SQL映射文件中的SQL语句一个一个地对应。通过Mapper接口,可以将Java对象与数据库记录之间的映射配置在XML文件中,或使用注解进行配置。
  • SQL映射文件:SQL映射文件是用于配置SQL语句与Java方法之间映射关系的XML文件。SQL映射文件中可以包含动态SQL、参数映射、结果集映射等配置信息。
  • Executor:执行器,负责执行SQL语句,并将结果映射成Java对象。MyBatis提供了三种执行器类型:SimpleExecutor、ReuseExecutor、BatchExecutor。
  • StatementHandler:语句处理器,用于处理PreparedStatement对象,将SQL语句设置到PreparedStatement中并执行。
  • ParameterHandler:参数处理器,用于处理SQL语句中的参数,将Java对象转换为PreparedStatement需要的参数类型。
  • ResultSetHandler:结果集处理器,用于处理SQL执行后的结果集,将结果集映射为Java对象。
  • TypeHandler:类型处理器,用于处理Java对象与数据库数据类型之间的转换。MyBatis提供了一些默认的类型处理器,并支持自定义类型处理器。

1. #{} 和 ${}的区别是什么?

在MyBatis中,#{}和${}是两种不同的占位符语法,用于在SQL语句中插入动态参数。它们在使用和处理参数值上有一些不同之处。

  1. #{}(预编译占位符):
    • #{}用于在SQL语句中插入参数,并且会对参数进行预编译,防止SQL注入问题。
    • #{}中的参数会被转义并添加到SQL语句中,然后通过PreparedStatement进行参数绑定。
    • 使用#{}时,MyBatis会自动将参数转换为合适的数据类型,无需手动转换。
  2. ${}(拼接占位符):
    • ${}用于在SQL语句中插入参数,但不会对参数进行预编译处理。
    • ${}中的参数值会被直接拼接到SQL语句中,有潜在的SQL注入风险,因此要确保参数值的安全性。
    • 使用${}时,参数值会按照原始类型插入到SQL语句中,需要手动进行类型转换。

示例: 假设有一个Mapper接口方法如下:

@Select("SELECT * FROM users WHERE id = #{userId}")
User getUserById(@Param("userId") int userId);

在上述例子中,使用了#{}占位符来插入参数userId。MyBatis会自动将参数userId进行预编译,并绑定到SQL语句中,确保了SQL注入的安全性。

如果使用${}占位符,SQL语句将会是:

SELECT * FROM users WHERE id = 1;

在这种情况下,参数值1直接被拼接到SQL语句中,如果userId参数不受限制,有可能导致SQL注入问题。因此,一般情况下,推荐使用#{}占位符,以提高安全性和防止SQL注入。

2. MyBatis中9个动态标签

  • <if>
  • <choose><when><otherwise>
  • <trim>
  • <where>
  • <set>
  • <foreach>
  • <bind>

<if>

<if>:条件判断标签,用于在SQL语句中实现简单的条件判断。例如:

<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users
    <where>
        <if test="userId != null">
            AND id = #{userId}
        </if>
    </where>
</select>

<choose>、<when>、<otherwise>

<choose>标签类似于Java中的switch语句,可以包含多个<when>子标签和一个<otherwise>子标签。MyBatis会按顺序检查每个<when>子标签的test条件,如果有条件成立,就执行对应的SQL片段。如果所有<when>条件都不成立,就执行<otherwise>子标签中的SQL片段。

假设有一个Mapper接口,根据不同条件查询用户:

<select id="getUserByCondition" parameterType="User" resultType="User">
  SELECT * FROM users
  <where>
    <choose>
      <when test="username != null">
        AND username = #{username}
      </when>
      <when test="email != null">
        AND email = #{email}
      </when>
      <otherwise>
        AND age = #{age}
      </otherwise>
    </choose>
  </where>
</select>

在上述示例中,我们使用<choose>标签包裹了多个条件选择的逻辑。当传入的User对象中username不为null时,执行<when>标签中的SQL片段,当username为null时,检查email字段,如果email不为null,则执行对应的SQL片段。如果既没有满足username的条件,也没有满足email的条件,则执行<otherwise>标签中的SQL片段。

<trim>

在 MyBatis 中,<trim> 标签是用于动态地添加 SQL 片段的标签。它可以帮助我们处理 SQL 语句中的空白字符,避免不必要的逗号、AND、OR 等语法问题。<trim> 标签支持在 SQL 的开始和结尾处截取空白字符,并在指定位置添加指定的前缀和后缀,以构建更灵活和简洁的 SQL 语句。

<trim> 标签有三个主要属性:

  • prefix: 指定要添加在 SQL 片段前面的字符串。
  • suffix: 指定要添加在 SQL 片段后面的字符串。
  • prefixOverrides: 指定要截取的前缀字符串。

假设有一个 Mapper 接口方法,根据不同条件查询用户:

<select id="getUserByCondition" parameterType="User" resultType="User">
  SELECT * FROM users
  <where>
    <trim prefix="AND" prefixOverrides="AND">
      <if test="username != null">
        AND username = #{username}
      </if>
      <if test="email != null">
        AND email = #{email}
      </if>
    </trim>
  </where>
</select>

在上述示例中,我们使用了 <trim> 标签来处理 SQL 片段中的空白字符。prefix="AND" 表示在 SQL 片段前添加 "AND",prefixOverrides="AND" 表示在截取 SQL 片段前的 "AND"。这样,当传入的 User 对象中 username 不为 null 时,生成的 SQL 语句会是 "AND username = #{username}",如果 email 也不为 null,则生成的 SQL 语句会是 "AND username = #{username} AND email = #{email}"。

<where>

<where> 标签会自动处理 SQL 语句中的空白字符,并根据条件动态生成 WHERE 子句。如果 <where> 标签内部有条件满足,则会生成 WHERE 关键字和相应的条件表达式。如果 <where> 标签内部没有条件满足,则不会生成 WHERE 关键字,以避免生成无用的 WHERE 子句。

假设有一个 Mapper 接口方法,根据不同条件查询用户:

<select id="getUserByCondition" parameterType="User" resultType="User">
  SELECT * FROM users
  <where>
    <if test="username != null">
      AND username = #{username}
    </if>
    <if test="email != null">
      AND email = #{email}
    </if>
  </where>
</select>

<set>

<set> 标签会自动处理 SQL 语句中的空白字符,并根据条件动态生成 SET 子句。如果 <set> 标签内部有条件满足,则会生成 SET 关键字和相应的赋值语句。如果<set>标签内部没有条件满足,则不会生成 SET 关键字,以避免生成空的 SET 子句。

假设有一个 Mapper 接口方法,根据不同条件更新用户信息:

<update id="updateUser" parameterType="User">
  UPDATE users
  <set>
    <if test="username != null">
      username = #{username},
    </if>
    <if test="email != null">
      email = #{email},
    </if>
  </set>
  WHERE id = #{id}
</update>

在上述示例中,我们使用了 <set> 标签来动态生成 SET 子句。当传入的 User 对象中 username 不为 null 时,生成的 SQL 语句会是 "UPDATE users SET username = #{username},",如果email也不为 null,则生成的 SQL 语句会是 "UPDATE users SET username = #{username}, email = #{email},"。最后,无论哪种情况,SQL 语句都会以 "WHERE id = #{id}" 结尾。

<foreach>

<foreach> 标签用于循环遍历集合或数组,并在 SQL 语句中动态生成多个相同的 SQL 片段。这个标签通常用于构建 IN 条件,使得我们可以在 SQL 中使用一个集合或数组来匹配多个值。

以下是一个示例代码,演示了 标签的使用:

<select id="getUsersByIdList" resultType="User">
  SELECT * FROM users
  WHERE id IN
  <foreach collection="idList" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

在上述示例中,我们使用了 <foreach> 标签来动态生成 IN 条件。${idList} 是传入的一个 List 或数组,item="id" 表示将集合中的每个元素赋值给id变量,在 SQL 中通过 #{id} 使用。open="(" 表示循环开始时添加 "(",separator="," 表示每个元素之间用逗号分隔,close=")" 表示循环结束时添加 ")"。这样,当传入的 idList 是 [1, 2, 3] 时,生成的 SQL 语句会是 "SELECT * FROM users WHERE id IN (1, 2, 3)"。

<bind>

<bind> 标签用于将一个值或表达式绑定到一个变量上,使得我们可以在 SQL 语句中重复使用这个变量,避免多次重复书写。

以下是一个示例代码,演示了 标签的使用:

<select id="getUsersByAgeRange" resultType="User">
  <bind name="minAge" value="18" />
  <bind name="maxAge" value="30" />
  SELECT * FROM users
  WHERE age BETWEEN #{minAge} AND #{maxAge}
</select>

3. XML映射文件中,还有哪些标签?

  • <select>:定义查询语句
  • <insert>:定义插入语句
  • <update>:定义更新语句
  • <delete>:定义删除语句
  • resultMap:定义结果映射,将查询结果映射到Java对象
  • parameterType 属性来直接指定参数的类型。使用 parameterType 属性可以将数据库查询的参数直接映射到指定的 Java 对象类型中,而不需要再定义一个额外的 parameterMap
  • parameterMap:定义参数映射,不常用(自 MyBatis 3.5.0 起已废弃)
  • <sql>:定义可重用的SQL片段
  • <include>:引入已定义的SQL片段

4. MyBatis支持注解吗?优缺点分析

结论:支持

优点:

  1. 简洁直观:使用注解方式,将SQL语句直接定义在Mapper接口的方法上,使得代码结构更加清晰,易于阅读和理解。
  2. 减少配置:不再需要编写繁杂的XML配置文件,减少了配置的复杂性,提高了开发效率。
  3. 更好的编译时检查:注解方式可以在编译时检查SQL语句的正确性,避免了运行时可能出现的SQL语法错误。
  4. 与Java代码紧密结合:使用注解方式,SQL语句直接嵌入在Java代码中,与Java代码紧密结合,便于代码的维护和重构。

缺点:

  1. 可读性较差:在一些复杂的SQL语句中,注解方式可能会导致SQL语句的可读性较差,不如XML配置方式直观。
  2. 代码耦合度较高:使用注解方式,SQL语句直接嵌入在Java代码中,可能导致代码的耦合度较高,不利于解耦和模块化开发。
  3. 不适合复杂场景:对于一些复杂的SQL映射,使用注解方式可能会显得不够灵活和易于维护,此时XML配置方式更为合适。

5.MyBatis是如何进行分页的?

MyBatis可以通过在SQL语句中使用分页参数来实现结果的分页查询。常见的分页方式是使用LIMITOFFSET子句(在MySQL等数据库中)或ROWNUM(在Oracle等数据库中)来限制返回结果的数量和偏移量。MyBatis提供了两种主要的分页方式:基于RowBounds对象和基于插件的分页。

1. 基于RowBounds对象的分页:

RowBounds是MyBatis中的一个简单的分页辅助对象,通过将RowBounds对象作为参数传递给查询方法,可以实现结果的分页查询。RowBounds对象有两个参数:offsetlimit,用于指定结果集的偏移量和限制返回结果的数量。

示例代码:

int offset = 0; // 起始偏移量,即第几条记录开始查询
int limit = 10; // 每页记录数量,即每页返回多少条记录
RowBounds rowBounds = new RowBounds(offset, limit);

List<User> userList = sqlSession.selectList("getUserList", null, rowBounds);

在以上示例中,getUserList是Mapper接口方法名,null是传入查询参数的对象,rowBoundsRowBounds对象,它指定了偏移量为0(从第1条记录开始查询)和每页返回10条记录。

2. 基于插件的分页:

MyBatis还支持通过插件来实现分页功能。通过编写自定义的插件,在查询方法执行前拦截SQL语句,根据分页参数自动生成分页SQL,并将生成的SQL替换原始的SQL语句。这种方式可以更加灵活地实现分页,同时也避免了在每个查询方法中手动传入RowBounds对象。

示例代码(使用MyBatis-PageHelper插件):

首先,需要引入PageHelper插件的依赖,然后在MyBatis配置文件中添加PageHelper的插件配置:

<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <property name="dialect" value="mysql"/>
    </plugin>
</plugins>

然后,在查询方法执行前,PageHelper插件会自动拦截SQL语句,并根据分页参数自动生成分页SQL。

int pageNum = 1; // 当前页码
int pageSize = 10; // 每页记录数量

// 在查询方法执行前,设置分页参数
PageHelper.startPage(pageNum, pageSize);

// 执行查询方法,PageHelper会自动拦截SQL并生成分页SQL
List<User> userList = userMapper.getUserList();

在以上示例中,getUserList是Mapper接口方法名,PageHelper插件会自动拦截SQL,并根据传入的pageNumpageSize参数生成分页SQL,查询结果会自动分页返回。

6. MyBatis分页插件的原理

MyBatis分页插件的原理是通过拦截器(Interceptor) 来实现的。拦截器是MyBatis框架的一个扩展点,可以拦截SQL的执行过程,在SQL执行前后进行一些额外的处理。

MyBatis的分页插件一般会拦截查询方法的执行,在查询方法执行前解析传入的分页参数,然后根据分页参数生成对应的分页SQL,最后将生成的SQL替换原始的SQL语句。这样,就实现了查询结果的分页。

具体实现步骤如下:

  1. 定义分页参数对象:通常是一个包含页码(pageNum)和每页记录数量(pageSize)的Java对象。
  2. 在查询方法执行前,拦截器会从线程上下文中获取传入的分页参数对象。
  3. 根据传入的分页参数对象,计算出查询结果的偏移量(offset)和限制数量(limit)。
  4. 利用偏移量和限制数量,生成对应的分页SQL,通常是在原始SQL语句外层再包裹一层分页查询的语句,比如使用LIMITOFFSET子句(在MySQL等数据库中)或ROWNUM(在Oracle等数据库中)来限制返回结果的数量和偏移量。
  5. 将生成的分页SQL替换原始的SQL语句,然后继续执行原始的查询方法。
  6. 查询结果会自动进行分页处理,并将分页后的结果返回。

以下是一个简单的示例代码:

1.引入 MyBatis-PageHelper 依赖:

在 Maven 项目的 pom.xml 中添加依赖:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version> <!-- 版本号可能会有变化,请查看最新版本 -->
</dependency>

2.配置 MyBatis-PageHelper 插件:

在 MyBatis 的配置文件中,添加 PageHelper 插件的配置:

<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <property name="dialect" value="mysql"/> <!-- 数据库方言,可选配置 -->
    </plugin>
</plugins>

3.在查询方法中使用分页:

在 Mapper 接口中定义查询方法,然后在方法中使用 PageHelper.startPage() 来设置分页参数。

import com.github.pagehelper.PageHelper;

public interface UserMapper {
    List<User> getUserList();

    // 分页查询用户列表
    List<User> getUserListWithPaging(int pageNum, int pageSize);
}

在查询方法中使用 PageHelper.startPage() 来设置分页参数,并执行查询。

import org.springframework.stereotype.Repository;

@Repository
public class UserMapperImpl implements UserMapper {

    private final SqlSession sqlSession;

    @Autowired
    public UserMapperImpl(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    @Override
    public List<User> getUserList() {
        return sqlSession.selectList("getUserList");
    }

    @Override
    public List<User> getUserListWithPaging(int pageNum, int pageSize) {
        // 在查询方法执行前,设置分页参数
        PageHelper.startPage(pageNum, pageSize);

        // 执行查询方法,PageHelper会自动拦截SQL并生成分页SQL
        return sqlSession.selectList("getUserList");
    }
}

7. MyBatis中如何编写一个插件

Mybatis 只⽀持针对 ParameterHandlerResultSetHandlerStatementHandlerExecutor 这4 种接⼝的插件, Mybatis 使⽤JDK的动态代理, 为需要拦截的接口⽣成代理对象以实现接⼝⽅法拦截功能, 每当执⾏这 4 种接口对象的⽅法时,就会进⼊拦截⽅法,具体就是 InvocationHandler 的 invoke()⽅法, 拦截那些你指定需要拦截的⽅法。

编写插件: 实现 Mybatis 的 Interceptor 接⼝并复写 intercept()⽅法, 然后在给插件编写注解, 指定要拦截哪⼀个接⼝的哪些⽅法即可, 在配置⽂件中配置编写的插件。

下面是编写 MyBatis 插件的步骤:

  1. 定义插件类并实现 Interceptor 接口:
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;

@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class MyPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 在这里添加自定义的拦截逻辑
        // 可以在方法执行前后做一些额外的处理
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 将当前 Interceptor 实例应用到 MyBatis 的目标对象中
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可以读取在配置文件中配置的插件参数
    }
}
  1. 添加 @Intercepts 注解,并在 @Intercepts 注解中指定需要拦截的方法签名。在上面的例子中,我们指定了拦截 Executor 类中的 query 方法,并且传入了 MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class 作为方法的参数类型,这表示我们要拦截 Executor 类中的 query 方法,并且方法的参数类型为 MappedStatement, Object, RowBounds, ResultHandler
  2. 实现 intercept 方法,在这个方法中编写自定义的拦截逻辑。可以在方法执行前后做一些额外的处理。
  3. 实现 plugin 方法,在这个方法中将当前 Interceptor 实例应用到 MyBatis 的目标对象中。使用 Plugin.wrap(target, this) 来实现插件的包装。
  4. 如果需要在配置文件中配置插件的参数,可以实现 setProperties 方法,并读取配置文件中的参数。
  5. 最后,在 MyBatis 的配置文件中配置自定义插件:
    <plugins>
    <plugin interceptor="com.example.MyPlugin">
        <!-- 可以添加插件的配置参数 -->
        <!-- <property name="param1" value="value1"/> -->
    </plugin>
</plugins>

在上面的配置中,com.example.MyPlugin 是自定义插件的全限定类名,它会在 MyBatis 的执行过程中被应用到需要拦截的方法上。

8. MyBatis与Hibernate有哪些不同?

SQL 和 ORM 的争论,永远都不会终⽌

开发速度的对⽐:

  • Hibernate的真正掌握要⽐Mybatis难些。Mybatis框架相对简单很容易上⼿,但也相对简陋些。
  • ⽐起两者的开发速度,不仅仅要考虑到两者的特性及性能,更要根据项⽬需求去考虑究竟哪⼀个更适合项⽬开发,⽐如:⼀个项⽬中⽤到的复杂查询基本没有,就是简单的增删改查,这样选择hibernate效率就很快了,因为基本的sql语句已经被封装好了,根本不需要你去写sql语句,这就节省了⼤量的时间,但是对于⼀个⼤型项⽬,复杂语句较多,这样再去选择hibernate就不是⼀个太好的选择,选择mybatis 就会加快许多,⽽且语句的管理也⽐较⽅便。

开发⼯作量的对⽐:

Hibernate和MyBatis都有相应的代码⽣成⼯具。可以⽣成简单基本的DAO层⽅法。针对⾼级查询, Mybatis需要⼿动编写SQL语句,以及ResultMap。⽽Hibernate有良好的映射机制,开发者⽆需关⼼ SQL的⽣成与结果映射,可以更专注于业务流程

sql优化⽅⾯:

Hibernate的查询会将表中的所有字段查询出来,这⼀点会有性能消耗。Hibernate也可以⾃⼰写SQL来指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性。⽽Mybatis的SQL是⼿动编写的,所以可以按需求指定查询的字段。

Hibernate HQL语句的调优需要将SQL打印出来,⽽Hibernate的SQL被很多⼈嫌弃因为太丑了。 MyBatis的SQL是⾃⼰⼿动写的所以调整⽅便。但Hibernate具有⾃⼰的⽇志统计。Mybatis本身不带⽇ 志统计,使⽤Log4j进⾏⽇志记录。

对象管理的对⽐:

Hibernate 是完整的对象/关系映射解决⽅案,它提供了对象状态管理(state management)的功能, 使开发者不再需要理会底层数据库系统的细节。也就是说,相对于常⻅的 JDBC/SQL 持久层⽅案中需 要管理 SQL 语句,Hibernate采⽤了更⾃然的⾯向对象的视⻆来持久化 Java 应⽤中的数据。

换句话说,使⽤ Hibernate 的开发者应该总是关注对象的状态(state),不必考虑 SQL 语句的执⾏。这部分细节已经由 Hibernate 掌管妥当,只有开发者在进⾏系统性能调优的时候才需要进⾏了解。⽽MyBatis在这⼀块没有⽂档说明,⽤户需要对对象⾃⼰进⾏详细的管理。

缓存机制对⽐:

  • 相同点:都可以实现⾃⼰的缓存或使⽤其他第三⽅缓存⽅案,创建适配器来完全覆盖缓存⾏为。
  • 不同点:Hibernate的⼆级缓存配置在SessionFactory⽣成的配置⽂件中进⾏详细配置,然后再在具体的表-对象映射中配置是哪种缓存。

总体而言,MyBatis适用于对SQL有较高要求、更倾向于手动控制SQL语句和数据库操作的开发者,适用于已有数据库表结构,希望更灵活地控制SQL的场景。而Hibernate适用于对ORM有较高要求、更倾向于采用面向对象的方式进行数据库操作的开发者,适用于新项目或数据库表结构与Java对象需要完全映射的场景。选择使用哪个框架应该根据具体项目的需求、开发团队的技术水平和个人偏好来决定。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgajbhb
系列文章
更多 icon
同类精品
更多 icon
继续加载