MyBatis基础概念
什么是MyBatis?
MyBatis是一款优秀的支持自定义SQL查询、存储过程和高级映射的持久层框架,消除了几乎所有的JDBC代码和参数的手动设置以及结果集的检索。MyBatis可以使用XML或注解进行配置和映射,MyBatis通过将参数映射到配置的SQL形成最终执行的SQL语句,最后将执行SQL的结果映射成Java对象返回。
以下是MyBatis的一些重要特性和优势:
- 灵活的SQL映射:MyBatis允许开发者将SQL语句与Java方法进行映射,这样可以直接调用Java方法来执行数据库操作,避免了直接编写SQL语句的繁琐。
- 强大的动态SQL支持:MyBatis支持动态SQL,允许根据条件动态生成SQL语句,大大提高了SQL语句的可维护性和复用性。
- 支持存储过程和函数调用:MyBatis能够直接调用数据库的存储过程和函数,方便处理复杂的业务逻辑。
- 缓存机制:MyBatis支持多级缓存,可以提高查询性能,减少数据库访问次数。
- 映射注解支持:除了XML配置文件外,MyBatis还支持使用注解进行对象与数据库表之间的映射,简化了配置过程。
- 与Spring无缝整合:MyBatis可以很方便地与Spring框架整合,使得事务管理更加简单。
- 可扩展性: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语句中插入动态参数。它们在使用和处理参数值上有一些不同之处。
#{}
(预编译占位符):#{}
用于在SQL语句中插入参数,并且会对参数进行预编译,防止SQL注入问题。#{}
中的参数会被转义并添加到SQL语句中,然后通过PreparedStatement进行参数绑定。- 使用
#{}
时,MyBatis会自动将参数转换为合适的数据类型,无需手动转换。
${}
(拼接占位符):${}
用于在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支持注解吗?优缺点分析
结论:支持
优点:
- 简洁直观:使用注解方式,将SQL语句直接定义在Mapper接口的方法上,使得代码结构更加清晰,易于阅读和理解。
- 减少配置:不再需要编写繁杂的XML配置文件,减少了配置的复杂性,提高了开发效率。
- 更好的编译时检查:注解方式可以在编译时检查SQL语句的正确性,避免了运行时可能出现的SQL语法错误。
- 与Java代码紧密结合:使用注解方式,SQL语句直接嵌入在Java代码中,与Java代码紧密结合,便于代码的维护和重构。
缺点:
- 可读性较差:在一些复杂的SQL语句中,注解方式可能会导致SQL语句的可读性较差,不如XML配置方式直观。
- 代码耦合度较高:使用注解方式,SQL语句直接嵌入在Java代码中,可能导致代码的耦合度较高,不利于解耦和模块化开发。
- 不适合复杂场景:对于一些复杂的SQL映射,使用注解方式可能会显得不够灵活和易于维护,此时XML配置方式更为合适。
5.MyBatis是如何进行分页的?
MyBatis可以通过在SQL语句中使用分页参数来实现结果的分页查询。常见的分页方式是使用LIMIT
和OFFSET
子句(在MySQL等数据库中)或ROWNUM
(在Oracle等数据库中)来限制返回结果的数量和偏移量。MyBatis提供了两种主要的分页方式:基于RowBounds
对象和基于插件的分页。
1. 基于RowBounds
对象的分页:
RowBounds
是MyBatis中的一个简单的分页辅助对象,通过将RowBounds
对象作为参数传递给查询方法,可以实现结果的分页查询。RowBounds
对象有两个参数:offset
和limit
,用于指定结果集的偏移量和限制返回结果的数量。
示例代码:
int offset = 0; // 起始偏移量,即第几条记录开始查询
int limit = 10; // 每页记录数量,即每页返回多少条记录
RowBounds rowBounds = new RowBounds(offset, limit);
List<User> userList = sqlSession.selectList("getUserList", null, rowBounds);
在以上示例中,getUserList
是Mapper接口方法名,null
是传入查询参数的对象,rowBounds
是RowBounds
对象,它指定了偏移量为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,并根据传入的pageNum
和pageSize
参数生成分页SQL,查询结果会自动分页返回。
6. MyBatis分页插件的原理
MyBatis分页插件的原理是通过拦截器(Interceptor) 来实现的。拦截器是MyBatis框架的一个扩展点,可以拦截SQL的执行过程,在SQL执行前后进行一些额外的处理。
MyBatis的分页插件一般会拦截查询方法的执行,在查询方法执行前解析传入的分页参数,然后根据分页参数生成对应的分页SQL,最后将生成的SQL替换原始的SQL语句。这样,就实现了查询结果的分页。
具体实现步骤如下:
- 定义分页参数对象:通常是一个包含页码(pageNum)和每页记录数量(pageSize)的Java对象。
- 在查询方法执行前,拦截器会从线程上下文中获取传入的分页参数对象。
- 根据传入的分页参数对象,计算出查询结果的偏移量(offset)和限制数量(limit)。
- 利用偏移量和限制数量,生成对应的分页SQL,通常是在原始SQL语句外层再包裹一层分页查询的语句,比如使用
LIMIT
和OFFSET
子句(在MySQL等数据库中)或ROWNUM
(在Oracle等数据库中)来限制返回结果的数量和偏移量。 - 将生成的分页SQL替换原始的SQL语句,然后继续执行原始的查询方法。
- 查询结果会自动进行分页处理,并将分页后的结果返回。
以下是一个简单的示例代码:
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 只⽀持针对 ParameterHandler
、ResultSetHandler
、StatementHandler
、Executor
这4 种接⼝的插件, Mybatis 使⽤JDK的动态代理, 为需要拦截的接口⽣成代理对象以实现接⼝⽅法拦截功能, 每当执⾏这 4 种接口对象的⽅法时,就会进⼊拦截⽅法,具体就是 InvocationHandler 的 invoke()⽅法, 拦截那些你指定需要拦截的⽅法。
编写插件: 实现 Mybatis 的 Interceptor 接⼝并复写 intercept()⽅法, 然后在给插件编写注解, 指定要拦截哪⼀个接⼝的哪些⽅法即可, 在配置⽂件中配置编写的插件。
下面是编写 MyBatis 插件的步骤:
- 定义插件类并实现 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) {
// 可以读取在配置文件中配置的插件参数
}
}
- 添加
@Intercepts
注解,并在@Intercepts
注解中指定需要拦截的方法签名。在上面的例子中,我们指定了拦截 Executor 类中的query
方法,并且传入了MappedStatement.class
,Object.class
,RowBounds.class
,ResultHandler.class
作为方法的参数类型,这表示我们要拦截 Executor 类中的query
方法,并且方法的参数类型为MappedStatement
,Object
,RowBounds
,ResultHandler
。 - 实现
intercept
方法,在这个方法中编写自定义的拦截逻辑。可以在方法执行前后做一些额外的处理。 - 实现
plugin
方法,在这个方法中将当前 Interceptor 实例应用到 MyBatis 的目标对象中。使用Plugin.wrap(target, this)
来实现插件的包装。 - 如果需要在配置文件中配置插件的参数,可以实现
setProperties
方法,并读取配置文件中的参数。 - 最后,在 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
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
怎样阻止微信小程序自动打开
PHP中文网 06-13 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01