mybatis总结

1.Mybatis

1. 简介

  • MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了[google code](https://baike.baidu.com/item/google code/2346604),并且改名为MyBatis。

  • 2013年11月迁移到Github

  • 是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAOs)。

2. 持久化

  • 瞬时数据到持久性数据的转换(将需要持久性存储的数据存储到硬盘中)
  • 内存:断电及失,担忧的数据不能丢失,因此需要持久化
  • 在生活中类似:冷藏(没有用到的时候放到冰箱里持久性的保存起来)、罐头

3. 持久层

类似于Dao层

将瞬时数据转到持久性数据的代码层(Dao层)

4. 优点(为什么要使用Mybatis)

  • 简单易学
  • 灵活。 sql写在xml里,便于统一管理和优化。通过sql语句可以满足操作数据库的所有需求。
  • 解除sql与程序代码的耦合
  • 提供映射标签,支持对象与数据库的orm字段关系映射。
  • 提供对象关系映射标签,支持对象关系组建维护。
  • 提供xml标签,支持编写动态sql。

2.Mybatis程序

思路:搭建环境->导入Mybatis->编写代码->测试

1.创建一个Maven项目

创建Maven项目之后,删除src文件,使得该项目为一个空项目

2.导入对应的依赖

  • junit
  • mysql
  • mabaits
<!--导入依赖-->
<dependencies>
    <dependency>
        <!--junit测试-->
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!--mysql依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.25</version>
    </dependency>
    <!--mybatis依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.6</version>
    </dependency>
</dependencies>

3. 创建一个子项目Maven

因为父项目中已经导入了maven依赖,所以子项目之中不用导入依赖

在这里插入图片描述
在这里插入图片描述

4.编写mybatis配置文件

<?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 核心配置文件-->
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <!--mappers 元素则包含了一组映射器(mapper),这些映射器的 XML 映射文件包含了 SQL 代码和映射定义信息。-->
    <mappers>
        <mapper resource="UserMapper.xml"/>
    </mappers>
</configuration>

5.编写对应的工具类、实体类

public class MybatisUtils {
    public static SqlSessionFactory sqlSessionFactory;
    static{
        //使用mybatis第一步:获取SqlSessionFactory对象
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
             sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /*从 SqlSessionFactory 中获取 SqlSession
    既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。
    SqlSession 提供了在数据库执行 org.apache.ibatis.jdbc.SQL 命令所需的所有方法。
    你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句*/
    public static SqlSession getSqlSession(){
        return sqlSessionFactory.openSession();
    }

}

6. 编写Mapper接口类

public interface UserMapper {
    public List<User> getUSerList();
}

7.从XML中构建 SqlSessionFactory

<?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">
<!--namespace指定需要帮定的Dao/Mapper接口-->
<mapper namespace="com.zhou.mapper.UserMapper">
    <!--id要跟方法名一样,resultType要跟返回类型(若有泛型,跟泛型一致)一致-->
    <select id="getUSerList" resultType="com.zhou.pojo.User">
        <!--对单个表操作,可不加数据库名称,对多个表操作,必须加上数据库名称-->
        select * from mybatis.user
    </select>
</mapper>

8.测试

public class UserMapperTest {
    @Test
    public void test(){
        /*第一步:获得sqlSession对象*/
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        //getMapper
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> uSerList = mapper.getUSerList();
        for(User user : uSerList){
            System.out.println(user);
        }
        //关闭Session,可以使用try/finally关闭
        sqlSession.close();
    }
}

9.注意事项

  • 编写mybatis的配置文件时,一定要编写Mapper映射器

  • 在使用XML创建 SqlSessionFactory时,要注意id名与方法名一致,resultType要与返回类型一致,namespace指定的是对应的需要实现的Mapper/Dao接口

  • 在实际使用的时候,sqlSession一定要记得关闭

  • 关于XML文件的位置问题,可以将实现接口的XML文件放在resources文件夹下,如果放在对应的paggage下,则在编写工具类的时候,资源名称要更换,会出现资源Maven静态资源过滤问题,出现此问题,我们需要在xml配置文件中加上如下代码:

  • <resources>
       <resource>
           <directory>src/main/java</directory>
           <includes>
               <include>**/*.properties</include>
               <include>**/*.xml</include>
           </includes>
           <filtering>false</filtering>
       </resource>
       <resource>
           <directory>src/main/resources</directory>
           <includes>
               <include>**/*.properties</include>
               <include>**/*.xml</include>
           </includes>
           <filtering>false</filtering>
       </resource>
    </resources>
    
  • 使用测试的时候,结构要与实际系统结构相对应(报名、类型要一致)

    在这里插入图片描述

10.mybatis的执行流程

在这里插入图片描述

3.CRUD

注:在进行crud的时候,一定要进行事物的提交,否则进行的一切操作都视为无效

1.增

步骤:1. 在接口中编写方法

public int addUser(User user);

2.在对应的xml文件中编队对应的配置文件

<!--如果需要获取传递进来的参数,需要加上#{参数名},如果参数为对象可以直接写属性名#{属性名}-->
<insert id="addUser" parameterType="com.zhou.pojo.User">
    insert into mybatis.user(`id`,`name`,`pwd`) values(#{id},#{name},#{pwd})
</insert>

3.进行curd实际操作,并提交事务

public void addUser(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    int res = mapper.addUser(new User(5,"刘鑫","188888"));
    if(res > 0){
        System.out.println("添加用户成功!");
        sqlSession.commit();
    }else{
        System.out.println("添加用户失败!");
    }
    sqlSession.close();
}

2.删

public int delete(int id);
<delete id="delete" parameterType="int">
    delete
    from mybatis.user
    where id=#{id};
</delete>
public void delete(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    mapper.delete(5);
    sqlSession.commit();
    sqlSession.close();
}

3.改

public int update(User user);
<update id="update" parameterType="com.zhou.pojo.User">
    update mybatis.user
    set name = #{name},pwd=#{pwd}
    where id=#{id};
</update>
public void update(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    mapper.update(new User(3,"李丽","7878112"));
    sqlSession.commit();
    sqlSession.close();
}

4. 查

public User getUserById(int id);
<select id="getUserById" parameterType="int" resultType="com.zhou.pojo.User">
    select * from mybatis.user where id=#{id}
</select>
public void getUserById(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.getUserById(3);
    System.out.println(user);
    sqlSession.close();
}

5.模糊查询

方法一:在xml配置文件中添加通配符%#{value}%

<select id="getUser" parameterType="string" resultType="com.zhou.pojo.User">
    select * from mybatis.user where name like "%"#{value}"%"
</select>
public List<User> getUser(String value);
public void getUser(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> user = mapper.getUser("李");
    for(User u : user){
        System.out.println(u);
    }
    sqlSession.close();
}

方法二:在传递参数的时候添加

public void getUser(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> user = mapper.getUser("%李%");
    for(User u : user){
        System.out.println(u);
    }
    sqlSession.close();
}
<select id="getUser" parameterType="string" resultType="com.zhou.pojo.User">
    select * from mybatis.user where name like #{value}
</select>

6.Map接受参数

使用Map可以随意设置参数,但是xml中要获得参数map中一定要存在对应的键值

public void addUserMap(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    Map<String,Object > map = new HashMap<>();
    map.put("userId",8);
    map.put("userName","落叶");
    map.put("userPwd","12345125");
    mapper.addUser2(map);
    sqlSession.commit();
    sqlSession.close();
}
<insert id="addUser2" parameterType="map">
    insert into mybatis.user(`id`,`name`,`pwd`) values(#{userId},#{userName},#{userPwd})
</insert>
public int addUser2(Map<String,Object> map);

小结:

  • 所有的增删改操作都需要提交事务!
  • 接口所有的普通参数,尽量都写上@Param参数,尤其是多个参数时,必须写上!
  • 有时候根据业务的需求,可以考虑使用map传递参数!
  • 为了规范操作,在SQL的配置文件中,我们尽量将Parameter参数和resultType都写上

4.配置文件解析

1.核心配置文件

  • mybatis-config.xml 系统核心配置文件
  • MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。
  • 能配置的内容如下:
configuration(配置)
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识)
mappers(映射器)
<!-- 注意元素节点的顺序!顺序不对会报错 -->

2.envrionment元素

<!--通过default来设置默认使用哪个环境(此处可以设置development/test)-->
<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC">
            <property name="..." value="..."/>
        </transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>

    <environment id="test">
        <transactionManager type="JDBC">
            <property name="..." value="..."/>
        </transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
</environments>
  • 配置MyBatis的多套运行环境,将SQL映射到多个不同的数据库上,必须指定其中一个为默认运行环境(通过default指定)

  • 子元素节点:environment

    • dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。

    • 数据源是必须配置的。

    • 有三种内建的数据源类型

      type="[UNPOOLED|POOLED|JNDI]")
      
    • unpooled:这个数据源的实现只是每次被请求时打开和关闭连接。

    • pooled:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来 , 这是一种使得并发 Web 应用快速响应请求的流行处理方式。

    • jndi:这个数据源的实现是为了能在如 Spring 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的引用。

    • 数据源也有很多第三方的实现,比如dbcp,c3p0,druid等等…

    • 详情:点击查看官方文档

    • 这两种事务管理器类型都不需要设置任何属性。

    • 具体的一套环境,通过设置id进行区别,id保证唯一!

    • 子元素节点:transactionManager - [ 事务管理器 ]

      <!-- 语法 -->
      <transactionManager type="[ JDBC | MANAGED ]"/>
      
    • 子元素节点:数据源(dataSource)

3.properties

数据库这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过导入相应的properites文件。

第一步 ; 在资源目录下新建一个db.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8
username=root
password=123456

第二步 : 将文件导入properties 配置文件

<configuration>
    <!--导入properties文件-->
    <properties resource="db.properties"/>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

或者可以直接在xml文件中使用property属性定义

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8
<configuration>
    <!--导入properties文件-->
    <properties resource="db.properties">
    	<property name="username" value="root"/>
        <property name="password" value="******"/>
	</properties>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

4.typeAliases

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

<!--配置别名,注意顺序-->
<typeAliases>
   <typeAlias type="com.kuang.pojo.User" alias="User"/>
</typeAliases>

当这样配置时,User可以用在任何使用com.kuang.pojo.User的地方。

也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:

<typeAliases>
	<package name="com.kuang.pojo"/>
</typeAliases>

每一个在包 com.kuang.pojo 中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。

若有注解,则别名为其注解值。见下面的例子:

@Alias("user")		//设置User的别名为user
public class User {
  ...
}

5.其他配置

  • 设置(settings)相关 => 查看帮助文档

    • 懒加载(lazyLoadingEnabled)
    • 日志实现
    • 缓存开启关闭
  • 一个配置完整的 settings 元素的示例如下:

    <settings>
        <setting name="cacheEnabled" value="true"/>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="multipleResultSetsEnabled" value="true"/>
        <setting name="useColumnLabel" value="true"/>
        <setting name="useGeneratedKeys" value="false"/>
        <setting name="autoMappingBehavior" value="PARTIAL"/>
        <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
        <setting name="defaultExecutorType" value="SIMPLE"/>
        <setting name="defaultStatementTimeout" value="25"/>
        <setting name="defaultFetchSize" value="100"/>
        <setting name="safeRowBoundsEnabled" value="false"/>
        <setting name="mapUnderscoreToCamelCase" value="false"/>
        <setting name="localCacheScope" value="SESSION"/>
        <setting name="jdbcTypeForNull" value="OTHER"/>
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
    </settings>
    

类型处理器

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

对象工厂

  • MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。
  • 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过有参构造方法来实例化。
  • 如果想覆盖对象工厂的默认行为,则可以通过创建自己的对象工厂来实现。

6.声明周期和作用域

作用域(Scope)和生命周期

理解我们目前已经讨论过的不同作用域和生命周期类是至关重要的,因为错误的使用会导致非常严重的并发问题。

我们可以先画一个流程图,分析一下Mybatis的执行过程!

在程序开始执行的时候通过xml文件启动sqlSessionFactoryBulider创建sqSessionFactory,sqlSession在程序运行的时候一直存在,一直到程序结束,相当于全局变量,sqlSession相当于局部变量,通过sqlSeession获得对应的Mapper对数据库进行操作,在sql执行完毕之后,应使用close将sqlSeession释放

作用域理解

  • SqlSessionFactoryBuilder 的作用在于创建 SqlSessionFactory,创建成功后,SqlSessionFactoryBuilder 就失去了作用,所以它只能存在于创建 SqlSessionFactory 的方法中,而不要让其长期存在。因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
  • SqlSessionFactory 可以被认为是一个数据库连接池,它的作用是创建 SqlSession 接口对象。因为 MyBatis 的本质就是 Java 对数据库的操作,所以 SqlSessionFactory 的生命周期存在于整个 MyBatis 的应用之中,所以一旦创建了 SqlSessionFactory,就要长期保存它,直至不再使用 MyBatis 应用,所以可以认为 SqlSessionFactory 的生命周期就等同于 MyBatis 的应用周期。
  • 由于 SqlSessionFactory 是一个对数据库的连接池,所以它占据着数据库的连接资源。如果创建多个 SqlSessionFactory,那么就存在多个数据库连接池,这样不利于对数据库资源的控制,也会导致数据库连接资源被消耗光,出现系统宕机等情况,所以尽量避免发生这样的情况。
  • 因此在一般的应用中我们往往希望 SqlSessionFactory 作为一个单例,让它在应用中被共享。所以说 SqlSessionFactory 的最佳作用域是应用作用域。
  • 如果说 SqlSessionFactory 相当于数据库连接池,那么 SqlSession 就相当于一个数据库连接(Connection 对象),你可以在一个事务里面执行多条 SQL,然后通过它的 commit、rollback 等方法,提交或者回滚事务。所以它应该存活在一个业务请求中,处理完整个请求后,应该关闭这条连接,让它归还给 SqlSessionFactory,否则数据库资源就很快被耗费精光,系统就会瘫痪,所以用 try…catch…finally… 语句来保证其正确关闭。
  • 所以 SqlSession 的最佳的作用域是请求或方法作用域。

5.ResultMap

如果出现了数据库字段名与实体类属性名不一致的情况,会出现以下问题

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJrZKUlq-1648986433941)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220327100025699.png)]

此处定义的实体类属性名为password,而数据库中对应的字段名为pwd,导致查询出来的结果为空,因为数据库到java中的类型转换没有对应的转换,所以导致我们查询出来的结果为空

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BjdFM7XY-1648986433942)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220327095951843.png)]

解决上述问题有两种方式:

  • 修改sql语句,给字段pwd取一个别名为password

    <select id="getUserById" resultType="user">
        select id,name,pwd as password from mybatis.user where id = #{id}
    </select>
    
  • 使用ResultMap

    <!-- 此处的id要与对应的resultMap一直,type则为真正的要返回的类型 -->
    <resultMap id="userMap" type="user">
        <!--cloumn表示的数据库中的字段名 property则是表示的是实体类中的属性名-->
        <result column="pwd" property="password"></result>
    </resultMap>
    <!-- select中的原resultType属性不需要了,取而代之的是resultMap,名称与resultMap属性的id要一致 -->
    <select id="getUserById" resultMap="userMap">
        select * from mybatis.user where id=#{id}
    
    </select>
    

自动映射

  • resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来。
  • 实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap 能够代替实现同等功能的长达数千行的代码。
  • ResultMap 的设计思想是,对于简单的语句根本不需要配置显式的结果映射,而对于复杂一点的语句只需要描述它们的关系就行了。
<select id="selectUserById" resultType="map">
    select id , name , pwd
    from user
    where id = #{id}
</select>

上述语句只是简单地将所有的列映射到 HashMap 的键上,这由 resultType 属性指定。虽然在大部分情况下都够用,但是 HashMap 不是一个很好的模型。你的程序更可能会使用 JavaBean 或 POJO(Plain Old Java Objects,普通老式 Java 对象)作为模型。

ResultMap 最优秀的地方在于,虽然你已经对它相当了解了,但是根本就不需要显式地用到他们。

手动映射

1、返回值类型为resultMap

<select id="selectUserById" resultMap="UserMap">
    select id , name , pwd from user where id = #{id}
</select>

2、编写resultMap,实现手动映射!

<resultMap id="UserMap" type="User">
   <!-- id为主键 -->
   <id column="id" property="id"/>
   <!-- column是数据库表的列名 , property是对应实体类的属性名 -->
   <result column="name" property="name"/>		<!--如果名称一致我们不需要进行手动映射-->
   <result column="pwd" property="password"/>
</resultMap>

6.日志

如果一个 数据库相关的操作出现了问题,我们可以根据输出的SQL语句快速排查问题。

对于以往的开发过程,我们会经常使用到debug模式来调节,跟踪我们的代码执行过程。但是现在使用Mybatis是基于接口,配置文件的源代码执行过程。因此,我们必须选择日志工具来作为我们开发,调节程序的工具。

Mybatis内置的日志工厂提供日志功能,具体的日志实现有以下几种工具:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging

具体选择哪个日志实现工具由MyBatis的内置日志工厂确定。它会使用最先找到的(按上文列举的顺序查找)。如果一个都未找到,日志功能就会被禁用。

标准日志实现

指定 MyBatis 应该使用哪个日志记录实现。如果此设置不存在,则会自动发现日志记录实现。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gxHJrJmC-1648986433942)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220327123823057.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T8qae382-1648986433943)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220327123831370.png)]

log4j

简介:

  • Log4j是Apache的一个开源项目

  • 通过使用Log4j,我们可以控制日志信息输送的目的地:控制台,文本,GUI组件…

  • 我们也可以控制每一条日志的输出格式;

  • 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。使用步骤:

    1、导入log4j的包

<dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.17</version>
</dependency>

2、配置文件编写

#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file

#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n

#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/kuang.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n

#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

3、setting设置日志实现

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

4、在程序中使用Log4j进行输出!

//注意导包:org.apache.log4j.Logger
static Logger logger = Logger.getLogger(MyTest.class);
@Test
public void getUserById(){
    logger.info("info:进入selectUser方法");
    logger.debug("debug:进入selectUser方法");
    logger.error("error: 进入selectUser方法");
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user = mapper.getUserById(3);
    System.out.println(user);
    sqlSession.close();
}

5、测试,看控制台输出!

  • 使用Log4j 输出日志
  • 可以看到还生成了一个日志的文件 【需要修改file的日志级别】[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ReHSRSV1-1648986433944)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220327124816861.png)]

7.分页

1.limit实现分页

select * from 表名 limit startIndex(起始记录数),pageSize(页面显示记录数)
<select id="getUSerList" resultMap="userMap" parameterType="map">
    select *
    from mybatis.user limit #{startIndex},#{pageSize}
</select>
@Test
public void getUserList(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    Map<String,Integer> map = new HashMap<>();
    map.put("startIndex",2);
    map.put("pageSize",4);
    List<User> uSerList = mapper.getUSerList(map);
    for(User user: uSerList){
        System.out.println(user);
    }
    sqlSession.close();
}
public List<User> getUSerList(Map<String,Integer> map);

2.RowBounds实现分页

public void getUserRowBounds(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    RowBounds rowBounds = new RowBounds(3, 4);
    //使用seqSession的selectList方法
    List<User> objects = sqlSession.selectList("com.zhou.mapper.UserMapper.getUserRowBounds", null, rowBounds);
    for(User user: objects){
        System.out.println(user);
    }
    sqlSession.close();
}
<select id="getUserRowBounds" resultMap="userMap">
    select * from mybatis.user
</select>
public List<User> getUserRowBounds();

8.注解开发

  • mybatis最初配置信息是基于 XML ,映射语句(SQL)也是定义在 XML 中的。而到MyBatis 3提供了新的基于注解的配置。不幸的是,Java 注解的的表达力和灵活性十分有限。最强大的 MyBatis 映射并不能用注解来构建

  • sql 类型主要分成 :

    • @select ()
    • @update ()
    • @Insert ()
    • @delete ()

注意: 利用注解开发就不需要mapper.xml映射文件了 .

1、我们在我们的接口中添加注解

//查询全部用户
@Select("select id,name,pwd password from user")
public List<User> getAllUser();

2、在mybatis的核心配置文件中注入

<!--使用class绑定接口-->
<mappers>
    <mapper class="com.kuang.mapper.UserMapper"/>
</mappers>

3、实现

public void test(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> users = mapper.getUsers();
    for(User user : users){
        System.out.println(user);
    }
    sqlSession.close();
}

1.使用注解进行crud

//增
@Insert("insert into user(id,name,pwd) values(#{id},#{name},#{pwd})")
public int addUser(User user);

//删
@Delete("delete from user where id=#{uid}")
public int delete(@Param("uid") int id
                  
//改
@Update("update user set name=#{name},pwd=#{pwd} where id=#{id}")
public int updateUser(User user);
                  
//4.查
@Select("select * from mybatis.user where id=#{uid}")
public User getUserById(@Param("uid") int id);

@param注解

@Param注解用于给方法参数起一个名字。以下是总结的使用原则:

  • 在方法只接受一个参数的情况下,可以不使用@Param。
  • 在方法接受多个参数的情况下,建议一定要使用@Param注解给参数命名。
  • 如果参数是 JavaBean , 则不能使用@Param。
  • 不使用@Param注解时,参数只能有一个,并且是Javabean。

#与$的区别

  • #{} 的作用主要是替换预编译语句(PrepareStatement)中的占位符? 【推荐使用】预防sql注入

    INSERT INTO user (name) VALUES (#{name});
    INSERT INTO user (name) VALUES (?);
    
  • ${} 的作用是直接进行字符串替换

    INSERT INTO user (name) VALUES ('${name}');
    INSERT INTO user (name) VALUES ('kuangshen');
    

2.Lombok插件

使用Lombok插件可以直接自动生成方法

参考:(127条消息) 强大的lombok插件_love_caicai的博客-CSDN博客_lombok插件

导入相应的Maven依赖即可使用

9.一对多和多对一

多对一:

  • 在一个教室有一个老师和多个学生,在学生层面看来就是多个学生对应一个老师(反之,站在老师层面看来,则是一个老师对应多个学生)

测试:

pojo类:

public interface StudentMapper {
    public List<Student> getUserList();
    public List<Student> getUserList2();
}
public interface TeacherMapper {
    public Teacher getTeacher(int id);
}

编写mapper接口

public interface TeacherMapper {
    public Teacher getTeacher(int id);
}
public interface StudentMapper {
    public List<Student> getUserList();
    public List<Student> getUserList2();
}

编写对应的xml文件

<!--StudentMapper.xml-->
<mapper namespace="com.zhou.mapper.StudentMapper">

    <select id="getUserList" resultMap="UserList">
        select * from student
    </select>
    <resultMap id="UserList" type="Student">
        <association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
  	<!--association关联属性 property属性名 javaType属性类型 column在多的一方的表中的列名-->
    </resultMap>
    <!--
       这里传递过来的id,只有一个属性的时候,下面可以写任何值
       association中column多参数配置:column="{key=value,key=value}"
       其实就是键值对的形式,key是传给下个sql的取值名称,value是片段一中sql查询的字段名。
       -->

    <select id="getTeacher" resultType="Teacher">
        select * from teacher where id=#{id}
    </select>
    
    
    
    <!--
    按查询结果嵌套处理
    思路:
      	直接查询出结果,进行结果集的映射
    -->

    <select id="getUserList2" resultMap="UserList2">
        select s.id sid,s.name sname ,t.name tname
        from student s,teacher t
        where t.id=s.tid
    </select>
    <resultMap id="UserList2" type="Student">
        <!--由于上方的sql语句为sql中的字段取了别名,所以在这里column使用别名进行匹配-->
        <result property="id" column="sid"/>
        <result property="name" column="sname"/>
        <!--关联对象property 关联对象在Student实体类中的属性-->
        <association property="teacher" javaType="Teacher">  
            <!--因为Teacher类中有name属性,所以这列property属性可以设置为name-->
            <result column="tname" property="name"/>
        </association>
    </resultMap>
</mapper>
<!--TeacherMapper.xml-->
<mapper namespace="com.zhou.mapper.TeacherMapper">

    <select id="getTeacher" resultType="teacher">
        select * from teacher where id=#{id}

    </select>

</mapper>

在总配置xml文件中,需要注册对应的两个xml文件

由于两个xml文件是放在同一目录下,所以直接使用文件名进行注册,否则使用class属性注册(使用文件名全路径)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uklMnTSf-1648986433944)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220329181906343.png)]

<mappers>
    <mapper resource="StudentMapper.xml"/>
    <mapper resource="TeacherMapper.xml"/>
</mappers>

一对多

  • 与多对一相反,一个老师对应多个学生

pojo类:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private int id;
    private String name;
    private int tid;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
    private int id;
    private String name;
    private List<Student> students;
}

编写Mapper接口

public Student getStudentById(@Param("tid") int id);
public interface TeacherMapper {
    public Teacher getTeacher(@Param("tid") int id);
    public Teacher getTeacher2(int i);
}

编写对应的xml文件

<!--Student.xml-->
<mapper namespace="com.zhou.mapper.StudentMapper">
    <select id="getStudent" resultType="student">
        select * from mybatis.student

    </select>

</mapper>
<!--Teacher.xml-->
<mapper namespace="com.zhou.mapper.TeacherMapper">
    <!--思路:
       		1. 从学生表和老师表中查出学生id,学生姓名,老师姓名
       		2. 对查询出来的操作做结果集映射
            	集合的话,使用collection!
                   JavaType和ofType都是用来指定对象类型的
                   JavaType是用来指定pojo中属性的类型
                   ofType指定的是映射到list集合属性中pojo的类型。
   	-->
    <resultMap id="StudentList" type="Teacher">
        <result column="tid" property="id"></result>
        <result column="tname" property="name"></result>
        <collection property="students" ofType="Student">
            <result property="id" column="sid"></result>
            <result property="name" column="sname"></result>
            <result property="tid" column="tid"></result>
        </collection>
    </resultMap>

    <select id="getTeacher" resultMap="StudentList">
        select s.id sid,s.name sname, t.id tid,t.name tname
        from student s,teacher t
        where s.tid = t.id and t.id = #{tid}
    </select>


    <select id="getTeacher2" resultMap="StudentList2">
        select * from teacher where id=#{tid}
    </select>
    <resultMap id="StudentList2" type="Teacher">
        <result property="id" column="id"></result>
        <result property="name" column="name"></result>
        <collection property="students" javaType="ArrayList" ofType="Student" select="getStudentById" column="id">
            <result property="id" column="id"></result>
            <result property="name" column="name"></result>
            <result property="tid" column="tid"></result>
        </collection>

    </resultMap>
    <select id="getStudentById" resultType="Student">
        select * from student where tid=#{tid}
    </select>
</mapper>

小结

1、关联-association

2、集合-collection

3、所以association是用于一对一和多对一,而collection是用于一对多的关系

4、JavaType和ofType都是用来指定对象类型的

  • JavaType是用来指定pojo中属性的类型
  • ofType指定的是映射到list集合属性中pojo的类型。

注意说明:

1、保证SQL的可读性,尽量通俗易懂

2、根据实际要求,尽量编写性能更高的SQL语句

3、注意属性名和字段不一致的问题

4、注意一对多和多对一 中:字段和属性对应的问题

5、尽量通过日志来查看自己的错误

10.动态sql

随机获取Id

public class IdUtils {
    //UUID.randomUUID().toString()->   4effdd62-b2ac-474d-8536-17b5467b30e7
    //UUID.randomUUID().toString().replaceAll("-","")->  4effdd62b2ac474d853617b5467b30e7
    public static String getId(){
        return UUID.randomUUID().toString().replaceAll("-","");
    }
    @Test
    public void test(){
        System.out.println(IdUtils.getId());
        System.out.println(IdUtils.getId());
        System.out.println(IdUtils.getId());
        System.out.println(IdUtils.getId());
    }
}

使用if语句构建动态sql

pojo类:

public class Blog {
    private String id;
    private String title;
    private String author;
    private Date createTime;
    private int views;
}

mapper接口:

public List<Blog> getBlog(HashMap<String,String> map);

xml文件

<select id="getBlog" parameterType="map" resultType="Blog">
    select * from blog where 1=1
    <!--如果title不为空,则在sql语句后加上语句 "and title = #{title} -->
    <if test="title != null">
        and title = #{title}
    </if>
    <if test="author != null">
        and author = #{author}
    </if>
</select>

where

<select id="getBlog" parameterType="map" resultType="Blog">
    select * from blog
    <!--如果where条件下的第一个if不满足而满足第二个if则会自动将第二个if之前的and(or)去除-->
    <where>
        <if test="title != null">
            title = #{title}
        </if>
        <if test="author != null">
            and author = #{author}
        </if>
        <if test="views != null">
            and views = #{views}
        </if>
    </where>
</select>

*where* 元素知道只有在包含标记返回任何内容时才插入"WHERE"。此外,如果该内容以"AND"或"OR"开头,它知道将其剥离。

使用set进行update操作

<update id="updateBlog" parameterType="map">
    update mybatis.blog
    <set>
        <if test="author != null">
            author = #{author},
        </if>
        <if test="title != null">
            title = #{title},
        </if>
        <if test="views != null">
            views = #{views},
        </if>
    </set>
    where id = #{id}
</update>
<!--控制台日志输出-->
Preparing: update mybatis.blog SET author = ?, title = ? where id = ?

set元素与where元素一样,会自动的丢弃最后一组条件末尾的逗号

choose

我们不希望所有条件都适用,而是只想在众多选项中选择一种情况。与Java中的switch语句类似

<select id="getBlog" parameterType="map" resultType="Blog">
    select * from blog
    <where>
        <choose>
            <when test="author != null">
                author = #{author}
            </when>
            <when test="title != null">
                title = #{title}
            </when>
            <when test="views != null">
                views = #{views}
            </when>
            <otherwise>
                create_Time = #{creatTime}
            </otherwise>
        </choose>
    </where>
</select>

choose中的语句与switch一致,匹配一项则会退出,相当于每一个when标签之中有break语句存在

Sql片段

有时候可能某个 sql 语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽取出来,然后使用时直接调用。类似于工具类

<!--将公用的if语句放在sql标签之中,需要调用的时候使用include标签对sql标签的id进行连接即可-->
<sql id="if">
    <if test="title != null">
        title = #{title}
    </if>
    <if test="author != null">
        and author = #{author}
    </if>
    <if test="views != null">
        and views = #{views}
    </if>

</sql>

引用sql标签

<select id="getBlog" parameterType="map" resultType="Blog">
    select * from blog
    <where>
        <include refid="if"></include>
    </where>
</select>

注意:

①、最好基于 单表来定义 sql 片段,提高片段的可重用性

②、在 sql 片段中不要包括 where

Foreach

<select id="getBlogForeach" parameterType="map" resultType="blog">
    select * from mybatis.blog 
    <where>
        <!--
           collection:指定输入对象中的集合属性
           item:每次遍历生成的对象
           open:开始遍历时的拼接字符串
           close:结束时拼接的字符串
           separator:遍历对象之间需要拼接的字符串
           select * from blog where(id=2 or id=3)
        -->
        <foreach collection="ids" item="id" open="and (" close=")" separator="or">
            id = #{id}
        </foreach>
    </where>
</select>
public void query(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
    Map map = new HashMap();
    List<String> ids = new ArrayList<>();
    map.put("ids",ids);
    ids.add("1");
    List<Blog> blogForeach = mapper.getBlogForeach(map);
    for (Blog foreach : blogForeach) {
        System.out.println(foreach);
    }
    sqlSession.close();
}

11.缓存

1.简介

1、什么是缓存 [ Cache ]?

  • 存在内存中的临时数据。
  • 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。

2、为什么使用缓存?

  • 减少和数据库的交互次数,减少系统开销,提高系统效率。

3、什么样的数据能使用缓存?

  • 经常查询并且不经常改变的数据。

2.mybatis中的缓存

  • MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。

  • MyBatis系统中默认定义了两级缓存:一级缓存二级缓存

    • 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
    • 二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
    • 为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存

3.一级缓存

一级缓存也叫本地缓存:

  • 与数据库同一次会话期间查询到的数据会放在本地缓存中。
  • 以后如果需要获取相同的数据,直接从缓存中拿,没必须再去查询数据库;
//两次查询同样的记录
public void query(){
    SqlSession sqlsession = MybatisUtils.getSqlsession();
    UserMapper mapper = sqlsession.getMapper(UserMapper.class);
    User user = mapper.getUserById(1);
    System.out.println(user);
    System.out.println("=================================");
    user = mapper.getUserById(1);
    System.out.println(user);
    sqlsession.close();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SB4Vwa2y-1648986433945)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220401093758124.png)]

//查询不同的记录
public void query(){
    SqlSession sqlsession = MybatisUtils.getSqlsession();
    UserMapper mapper = sqlsession.getMapper(UserMapper.class);
    User user = mapper.getUserById(1);
    System.out.println(user);
    System.out.println("=================================");
    user = mapper.getUserById(2);
    System.out.println(user);
    sqlsession.close();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cM0DKXOP-1648986433945)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220401094013183.png)]

一级缓存失效的情况

一级缓存是SqlSession级别的缓存,是一直开启的,我们关闭不了它;

一级缓存失效情况:没有使用到当前的一级缓存,效果就是,还需要再向数据库中发起一次查询请求!

1.Sqlsession不同

public void query(){
    SqlSession sqlsession = MybatisUtils.getSqlsession();
    SqlSession sqlsession2 = MybatisUtils.getSqlsession();
    UserMapper mapper = sqlsession.getMapper(UserMapper.class);
    User user = mapper.getUserById(1);
    System.out.println(user);
    sqlsession.close();
    System.out.println("=================================");
    UserMapper mapper1 = sqlsession2.getMapper(UserMapper.class);
    User user1 = mapper1.getUserById(1);
    System.out.println(user);
    System.out.println(user == user1);
    sqlsession2.close();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZNcR9EI6-1648986433946)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220401100739031.png)]

每个sqlSession的缓存互相独立

2、sqlSession相同,两次查询之间执行了增删改操作!

每一次进行增删改操作,就会更新缓存的内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QFPQGb1i-1648986433946)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220401101546654.png)]

3.手动清除缓存信息

public void query(){
    SqlSession sqlsession = MybatisUtils.getSqlsession();
    UserMapper mapper = sqlsession.getMapper(UserMapper.class);
    User user = mapper.getUserById(1);
    System.out.println(user);
    System.out.println("清除缓存");
    sqlsession.clearCache();
    User user2 = mapper.getUserById(1);
    System.out.println(user2);
    System.out.println(user == user2);
    sqlsession.close();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qTsY4X74-1648986433947)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220401101651217.png)]

4. 二级缓存

  • 二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存

  • 基于namespace级别的缓存,一个名称空间,对应一个二级缓存;

  • 工作机制

    • 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
    • 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中;
    • 新的会话查询信息,就可以从二级缓存中获取内容;
    • 不同的mapper查出的数据会放在自己对应的缓存(map)中;

如何开启二级缓存

1.在全局配置文件【mybatis-config.xml】中进行添加设置语句

<setting name="cacheEnabled" value="true"/>

2、去每个对应的mapper.xml中配置使用二级缓存,

无参形式:

<cache/>
<!--如果使用无参形式,需要对实体类进行序列化操作也就是实现Serializable接口-->

有参形式:

<cache
 eviction="FIFO"
 flushInterval="60000"
 size="512"
 readOnly="true"/>
<!--
    这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只	   读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
-->
public void query() {
    SqlSession sqlsession = MybatisUtils.getSqlsession();
    SqlSession sqlsession2 = MybatisUtils.getSqlsession();
    UserMapper mapper = sqlsession.getMapper(UserMapper.class);
    User user = mapper.getUserById(1);
    System.out.println(user);
    sqlsession.close();
    System.out.println("=================================");
    UserMapper mapper1 = sqlsession2.getMapper(UserMapper.class);
    User user1 = mapper1.getUserById(1);
    System.out.println(user);
    System.out.println(user == user1);
    sqlsession2.close();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fm7jZK14-1648986433947)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220401102350950.png)]

在未开启二级缓存的时候,使用不同的sqlSession查询相同的数据会导致缓存失效,但是使用二级缓存则不会出现该情况

5.缓存原理实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KN7FYTIP-1648986433947)(C:\Users\周畅\AppData\Roaming\Typora\typora-user-images\image-20220401111505733.png)]

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值