MyBatis持久层框架

MyBatis🤞🤞

✨✨✨✨✨✨✨✨✨✨✨✨✨✨

1. 简介

(1)简介

什么是MyBatis?

  • MyBatis 是一款优秀的持久层框架。
  • MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程。
  • MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的实体类POJO,普通的 Java对象】映射成数据库中的记录。
  • MyBatis 本是apache的一个开源项目ibatis, 2010年这个项目由apache 迁移到了google code,并且改名为MyBatis 。
  • Mybatis官方文档 : http://www.mybatis.org/mybatis-3/zh/index.html
  • GitHub : https://github.com/mybatis/mybatis-3

为什么需要MyBatis?

  • Mybatis就是帮助我们将数据存入数据库中,和从数据库中取数据。
  • 传统的jdbc操作,有很多重复代码块 。比如 : 数据取出时的封装 , 数据库的建立连接等等… , 通过框架可以减少重复代码,提高开发效率。
  • MyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) -->对象关系映射。
  • 所有的事情,不用Mybatis依旧可以做到,只是用了它,所有实现会更加简单!

MyBatis的优点:

  • 简单易学。
    • 本身就很小且简单。没有任何第三方依赖,最简单安装只要两个jar文件+配置几个sql映射文件就可以。
  • 灵活:mybatis不会对应用程序或者数据库的现有设计强加任何影响。
    • sql写在xml里,便于统一管理和优化。通过sql语句可以满足操作数据库的所有需求。
  • 解除sql与程序代码的耦合:通过提供DAO层,将业务逻辑和数据访问逻辑分离。
    • 使系统的设计更清晰,更易维护,更易单元测试。sql和代码的分离,提高了可维护性。
  • 提供xml标签,支持编写动态sql。

什么是持久化?

  • 持久化是将程序数据在持久状态和瞬时状态间转换的机制。
  • 即把内存中的数据保存到可永久保存的存储设备中(如磁盘)。
  • JDBC就是一种持久化机制。文件IO也是一种持久化机制。
  • 为什么需要持久化服务呢?那是由于内存本身的缺陷引起的
    • 内存断电后数据会丢失。
    • 内存过于昂贵,与硬盘、光盘等外存相比,内存的价格要高2~3个数量级,而且维持成本也高,至少需要一直供电吧。

Hibernate 简介

  • 数据库交互框架(ORM框架)。ORM—> Object Relation Mapping 对象关系映射。黑箱操作,不需写SQL。
  • Hibernate 缺点:
    • 不能自己写SQL。
    • 全映射框架,做部分字段映射很难。

MyBatis将SQL写在配置文件中。

即:

  1. MyBatis将重要步骤抽取出来,可以定制,其他步骤自动化,
  2. 重要的步骤都写在配置文件中(易于修改)
  3. 完全解决数据库优化的问题
  4. MyBatis底层就是对原生JDBC的一个简单封装。
  5. 半持久化框架。既将java编码与SQL抽取分离,还不会失去自动化功能。
  6. MyBatis是一个轻量级框架

SQL和java分开,功能边界清晰,一个专注于数据,一个专注于业务。

(2)环境

  1. 数据库驱动:mysql-connector-java

  2. MyBatis包:org.mybatis

  3. 日志包(非必需):在关键环节就会有日志打印。:log4j

    1. 这个日志框架依赖一个 log4j.xml 的配置文件。 (放在类路径下)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
 
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">

 <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
   <param name="Encoding" value="UTF-8" />
   <layout class="org.apache.log4j.PatternLayout">
    <param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS} %m  (%F:%L) \n" />
   </layout>
 </appender>
 <logger name="java.sql">
   <level value="debug" />
 </logger>
 <logger name="org.apache.ibatis">
   <level value="info" />
 </logger>
 <root>
   <level value="debug" />
   <appender-ref ref="STDOUT" />
 </root>
</log4j:configuration>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

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

2. HelloWorld

(1)环境搭建

  • 创建java工程

  • 创建测试数据库

    1. 创建表。JavaBean,操作数据库的dao接口

(2)MyBatis操作数据库的一般步骤

  1. 导包
  2. 写配置
  3. 写测试

配置文件:两个

  1. 全局配置文件:指导MyBatis如何正确运行,例如:连接哪个数据库
    1. <configuration> 下配置环境<environments default="development"> 。环境可以配置多个。
    2. <environment> 配置基本属性。
<?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>
    <!--环境,可以配置多个-->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <!--配置连接池-->

            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_test?serverTimezone=UTC&amp;rewriteBatchedStatements=true"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="lu/dao/EmpDao.xml"/>
    </mappers>
</configuration>
  1. SQL映射文件:每一个方法都如何想数据库发送SQL语句,如何执行等。
    1. 将namespace为接口的全类名。相当于告诉MyBatis这个配置文件是实现哪个接口的
    2. 使用<select> 标签定义一个查询操作
      1. id:Dao接口中的方法名
      2. resultType:指定方法运行后的返回值类型(查询操作必须指定)
      3. 在SQL语句中可以使用#{id} 取值。取的是方法传过来的参数。
<?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;名称空间,写接口全类名,相当于告诉MyBatis这个配置文件是实现哪个接口的-->
<mapper namespace="lu.dao.EmpDao">
<!--    select 标签定义一个查询操作。
id:方法名。resultType:指定方法运行后的返回值类型(查询操作必须指定)
#{id}:代表取方法传过来的参数的名
-->
    <select id="getEmpById" resultType="lu.pojo.Emp">
    select * from emp where id = #{id}
  </select>
</mapper>
  1. 我们写的dao接口的实现文件,MyBatis默认是不知道的,需要在全局配置文件中注册。使用<mappers>标签
   <mappers>
        <mapper resource="lu/dao/EmpDao.xml"/>
    </mappers>

从 XML 中构建 SqlSessionFactory,SqlSessionFactory构建SqlSession(SQL会话,SqlSession是跟数据库的一次会话,就相当于是一个Connection)。

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

测试:

  1. 根据全局配置文件先创建一个:SqlSessionFactory
  2. 从SqlSessionFactory中获取SqlSession对象来操作数据库
  3. 用SqlSession拿到dao接口的实现
  4. 使用dao接口的实现去调用方法,去查询。
import lu.dao.EmpDao;
import lu.pojo.Emp;
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.Test;

import java.io.IOException;
import java.io.InputStream;

/**
 * @author 满眼星河
 * @create 2020-11-25-9:05
 */
public class MyTest {

    @Test
    public void test01() throws IOException
    {
        //1. 根据全局配置文件构建出一个SQLSessionFactory

        /**
         * SqlSessionFactory是SqlSession工厂,负责创建SqlSession对象。
         *
         * SqlSession:代表和数据库的一次会话。
         */
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession=null;
        Emp emp=null;
        try{
            //2. 获取和数据库的一次会话,相当于getConnection()。拿到连接
            sqlSession = sqlSessionFactory.openSession();

            //3. 使用SQLSession操作数据库,获取到dao接口的实现。(相当于获取EmpDao.xml)
            EmpDao empDao = sqlSession.getMapper(EmpDao.class);

            //4. 调用dao接口的方法去查询即可
             emp = empDao.getEmpById(1);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            sqlSession.close();
        }

        System.out.println(emp);
    }
}

配置文件的约束文件:dtd是约束文件,

http://mybatis.org/dtd/mybatis-3-config.dtd

MyBatis中的增删改不会自动提交。

  1. 解决方案一:需要手动提交。
sqlSession.commit();
  1. 解决方案二:在获取sqlsession时传入true参数,表示设置为自动提交。
SqlSession sqlSession = sqlSessionFactory.openSession(true);

3. MyBatis全局配置文件

(1)两个文件:

  1. 一个是全局配置文件:mybatis-config.xml
    1. 指导MyBatis正确运行的一些全局配置文件。
  2. SQL映射文件:EmpDao.xml
    1. 相当于是对EmpDao接口的实现。
        //class com.sun.proxy.$Proxy6
        EmpDao empDao = sqlSession.getMapper(EmpDao.class);
        System.out.println(empDao.getClass());

EmpDao是一个代理对象,MyBatis自动为其创建一个代理对象。

HelloWorld细节:

  1. 获取到的是接口的代理对象。
  2. SqlSessionFactory和SqlSession
    1. SqlSessionFactory是创建SqlSession的工厂。
    2. SqlSession:相当于Connection。和数据库的一次会话。

(2)全局配置文件:

<?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>
    <!--环境,可以配置多个-->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <!--配置连接池-->

            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_test?serverTimezone=UTC&amp;rewriteBatchedStatements=true"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="lu/dao/EmpDao.xml"/>
    </mappers>

</configuration>

配置文件中可以写如下配置:

这些标签的编写都是有顺序的,而且必须遵守,否则报错。

  1. properties属性:引入外部资源文件。在下面可以使用${}获取值。
 <!--1. 和Spring的Context的:property-placeholder:引用外部配置文件-->
    <!--
    resource:从类路径下引入资源
    url:可以引用磁盘文件或网络资源
    -->
    <properties resource="dbconfig.properties"></properties>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${driverclass}"/>
                <property name="url" value="${jdbcurl}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
  1. setting设置:

    MyBatis中查询操作,会将查询结果自动封装为一个指定的对象(前提是要求数据库字段和pojo类的属性名一致)

    1. 如果不一致,我们以前采取的措施是在SQL语句中给相应字段起别名。
    2. 现在我们可以在MyBatis的配置文件中使用setting进行设置。数据库中的“-”连接命名转为java中的驼峰命名。
      1. emp_Name ----> empName
    3. 数据库不区分大小写。
    <settings>
            <setting name="mapUnderscoreToCamelCase" value="true"/>
        </settings>
    
  2. typeAliases属性:指定别名。指定之后就可以在Empdao的实现文件中使用。

    1. 是package批量指定别名,默认就是类名。
    2. 可以是java类上使用注解:@Alias(“Empss”)指定别名。
    3. 推荐不起别名,就使用全类名。
    
        <typeAliases>
            <package name="lu.pojo" />
        </typeAliases>
    
    <typeAliases>
<!--        为一个java类其别名,默认是类名(不区分大小写),可以使用alias指定-->
        <typeAlias type="lu.pojo.Emp" alias="Emp"/>
    </typeAliases>

已经为许多常见的 Java 类型内建了相应的类型别名。它们都是大小写不敏感的,需要注意的是由基本类型名称重复导致的特殊处理。

  1. typeHandlers:类型处理器。

    1. 作用:无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器。(部分)
  2. objectFactory:对象工厂。查询SQL的返回的对象。MyBatis底层利用objectFactory通过反射来给我们创建对象。

  3. plugins:插件。是MyBatis中的一个强大的功能。

    1. MyBatis通过四大对象来工作。
      1. Executor:执行器,执行CRUD操作。
      2. ParameterHandler:参数处理器,给预编译对象设置参数。
      3. ResultSetHandler:结果集处理器。负责将查询出来的结果集封装成对应的javaBean对象。
      4. StatementHandler:预编译参数。相当于原生的PreparedStatement。
    2. 插件通过动态代理机制,可以介入四大对象的任何一个方法的执行。
  4. environments:配置环境。可以配置多个环境,

    1. environment:配置一个具体的环境。(需要一个事务管理器和一个数据源)
      1. transactionManager
      2. dataSource
    2. id是一个环境的唯一辨识,要切换使用的环境,在<environments default="development">中指定即可。
    3. 我们以后的数据源和事务管理都是spring来管理。
   <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${driverclass}"/>
                <property name="url" value="${jdbcurl}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
  1. databaseIdProvider:MyBatis用来做数据库移植性的。type="DB_VENDOR" 这是写死的。
    1. name:数据库厂商别名。
    2. value:给这个标识起个别名
      1. MySQL,SQL Server

    <databaseIdProvider type="DB_VENDOR">
        <property name="MySQL" value="mysql"/>
        <property name="SQL Server" value="sqlserver"/>
        <property name="Oracle" value="oracle"/>
    </databaseIdProvider>

精确匹配优先。

<!--    默认这个查询不区分环境的,使用databaseId指定具体的数据库-->
    <select id="getEmpById" resultType="Empss" databaseId="mysql">
        select * from emp where id = #{id}
    </select>
  1. mapper标签:将写好的SQL映射文件需要使用mapper注册进来。
    1. resource:在类路径下找SQL映射文件
    2. class:直接引用接口的全类名。必须将SQL映射文件放在和接口同包下,而且必须同名。另外一种用法:使用注解开发
    3. url:从磁盘路径下,或者网络路径下引入

    <mappers>
		<mapper resource="lu/dao/EmpDao.xml"></mapper>
        <mapper class="lu.dao.EmpDaoAnnotation"></mapper>
		<mapper url=""></mapper>
    </mappers>

批量注册:

name写Dao接口所在的包名。

注意:批量注册时,dao的实现文件(例如:EmpDao.xml)一定要放在和dao同包的路径下才能获取到。

注解:

​ 使用@Select之类的注解

public interface EmpDaoAnnotation {

    @Select("select * from emp where id = #{id}")
    Emp getEmpById(Integer id);
    
}

注解的形式只能使用class 注册。

    <mappers>
        <mapper class="lu.dao.EmpDaoAnnotation"></mapper>
    </mappers>

注解和配置配合使用:

  • 重要的dao写配置。
  • 简单的dao就直接写注解。

4. SQL映射文件

(1)Dao映射文件中能写的标签:

  1. cache:缓存相关
  2. cache-ref:缓存相关
  3. delete,update,insert,select:和增删改查有关
  4. resultMap:结果映射,自定义结果集的封装映射规则。
  5. SQL:抽取可重用的SQL语句。

select标签的属性

id:指定方法。不能写重载方法。

statementType:指定执行SQL语句的执行器类型。

  1. statement:相当于原生JDBC的statement
  2. prepareStatement:相当于原生JDBC的prepareStatement
  3. Callable:调用数据库的存储过程

实现获取自增主键:

<!--    让MyBatis自动的将自增的id赋值给传入的Emp对象的id属性-->
<!--
useGeneratedKeys="true" :相当于告诉MyBatis执行完SQL语句之后,再调用JDBC的getGeneratedKeys,拿到自增的主键。
keyProperty="id" : 将自增的属性赋值给Emp对象中的id属性。
-->
    <insert id="insertEmp" useGeneratedKeys="true" keyProperty="id">
        insert into Emp(name,deptId) values(#{name},#{deptId})
    </insert>

但是:有的数据库不支持自增主键。或者没有设置主键自增。

在使用全字段更新时,如果插入的id已有,就会报错,为了解决这个问题,就可以使用 selectKey 先查询出最大的id再加一,赋值JavaBean给指定的属性。

调用这个方法时,传入的Emp为:

Emp emp = new Emp(null, "BBB", 2);
int i = empdao.insertEmp(emp);
<insert id="insertEmp">

        <selectKey order="BEFORE" resultType="integer" keyProperty="id">select Max(id)+1 from emp</selectKey>

        insert into Emp(id,name,deptId) values(#{id},#{name},#{deptId})
    </insert>

selectKey标签的属性。

(2)查询

传参到底能传什么?

<select id="getEmpByIdAndName" resultType="nuc.pojo.Emp">
    select * from emp where id=#{id} and name=#{name}
</select>

多个参数时,#{} 取不出来。报错如下:

Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]

这样才是正确的:或者使用 param1, param2

<select id="getEmpByIdAndName" resultType="nuc.pojo.Emp">
    select * from emp where id=#{arg0} and name=#{arg1}
</select>

如果只有一个参数时,在SQL语句中取参数时,使用#{} ,大括号中无论写什么都能正确获取到参数。

现象:

  1. 单个参数:

    1. 基本类型:
      1. 取值:#{随便写}
    2. 复杂类型:
  2. 多个参数:(推荐在方法命名时使用 @Param 命名参数)

    1. 取值:#{参数名}是无效的。只能使用#{param1……}

      1. 原因:只要传入的多个参数,MyBatis会自动将这些参数封装在一个Map中。使用的key就是参数的索引和第几个标识。
      2. 例:map.put(“param1”,传入的参数值)
      3. 所以:#{key} 就是去这个map中取值。
    2. 我们可以告诉MyBatis,封装map的时候,使用我们指定的key。使用 @Param 注解

      Emp getEmpByIdAndName(@Param("id") Integer id, @Param("name") String name);
      
    3. 所以,@Param 为参数指定key。命名参数。

  3. 传入pojo:

    1. 取值:#{pojo的属性名}
  4. 传入Map:可以直接传入一个Map,将要使用的参数封装起来。

    1. Emp getEmpByMap(Map<String,Object> map);
      
    2. 所以,#{}的本意就是去Map中取值。

EmpDao接口:

package nuc.inter;
public interface EmpDao {
    Emp getEmpByMap(Map<String,Object> map);
}

EmpDao.xml实现文件:

<select id="getEmpByMap" resultType="nuc.pojo.Emp">
    select * from emp where id=#{testId} and name=#{testName}
</select>

传入Map作为参数的测试:

@Test
public void test01()
{
    SqlSession sqlSession = sqlSessionFactory.openSession();

    EmpDao empdao = sqlSession.getMapper(EmpDao.class);

    Map<String, Object> map = new HashMap<>();
    map.put("testId",2);
    map.put("testName","李四");


    Emp emp = empdao.getEmpByMap(map);
    System.out.println(emp);
}

多个参数的情况下:MyBatis会自动封装Map。

扩展:

Dao接口中的方法:

Emp method(@Param("id") Integer id, @Param("name") String name, @Param("emp") Emp emp);

取值:

#{id}
#{name}
#{emp.name}  取这个对象的name属性。

传参推荐使用Map。

在MyBatis中有两种取值方式:

  1. #{}:是参数预编译的方式,参数的位置都是?,参数后来都是预编译设置进去的。

  2. ${}:不是预编译方式,而是直接和SQL进行拼串。

    1. 使用场景:SQL语句只要参数位置是支持预编译的。可以动态传入表名
    select * from ${tableName} where id=${testId} and name=#{testName}
    
    1. 一般都使用#{},在不支持参数预编译的位置要使用${}。

5. 查询的返回结果

(1)查询多条记录,返回一个List

List getAllEmp();
  • resultType:如果返回的是集合,这里写集合里面的元素的类型
  • MyBatis会自动给我们封装为一个List
<!-- 
-->
    <select id="getAllEmp" resultType="nuc.pojo.Emp">
        select * from emp
    </select>

(2)查询单条记录,返回类型写Map

  • 列名作为key,值作为value
Map<String,Object> getEmpByIdReturnMap(Integer id);

(3)查询多条记录,封装为Map。

  • 在Dao接口的方法上使用注解:@MapKey(value="") 指定key
  • 多条记录时,select标签的返回值类型写Map中元素的类型。
@MapKey("id")
Map<Integer,Emp> getAllEmpReturnMap();
<select id="getAllEmpReturnMap" resultType="nuc.pojo.Emp">
    select * from emp
</select>

(4)自定义封装规则

MyBatis默认自动封装结果集:

  • 按照列名和属性名一一对应的规则。(不区分大小写)

  • 如果不一一对应

    • 开启驼峰命名法。满足驼峰命名规则
      • 数据库中是:aaa_BBB
      • JavaBean中是:aaaBBB
    • 不满足驼峰命名规则,就起别名。
    • 自定义结果集:哪一列和那个JavaBean的属性对应,我们自己定义。
  • 自定义封装:

    • resultType:是使用默认规则,属性和列名一一对应。
    • resultMap:resultMap="myEmp" 查出数据封装结果的时候,使用myEmp自定义的规则封装 。
    • 使用<resultMap> 标签自定义封装规则:
      1. id属性:唯一标识,让别名在后面可以使用。
      2. type:指定为哪个JavaBean自定义封装规则。写JavaBean的全类名
      3. 再使用<id> 标签指定主键的对应规则
        1. column:指定哪一列是主键列。
        2. property:指定JavaBean中哪个属性封装id这一列数据。
      4. 使用 <result> 标签定义普通列的对应规则。

自定义结果集代码示例:


<select id="getAllEmpReturnMap" resultMap="myEmp">
    select * from emp
</select>
    
    
    
<!--   resultMap:自定义结果集。
        id:唯一标识,让别名在后面可以使用
        type:指定为哪个JavaBean自定义封装规则。写JavaBean的全类名
 -->
    <resultMap id="myEmp" type="nuc.pojo.Emp">
<!--        id:用来指定主键列的对应规则
            column:指定哪一列是主键列
            property:指定JavaBean中哪个属性封装id这一列数据
            
-->
        <id column="id" property="id"></id>
<!--        普通列对应
            property:JavaBean的属性
            column:对应数据库中哪一列的值
-->
        <result property="name" column="Cname"></result>
    </resultMap>

6. 联合查询

(1)如果JavaBean中还有一个JavaBean

  1. 创建数据库表,搭建环境。创建key表和lock表,一把钥匙只能开一个锁,所有在key表中建立外键,引用lock表中的数据。

(2)代码实现:

JavaBean—>Key类:

package nuc.pojo;

/**
 *  查询钥匙的时候,顺便将这把钥匙能开的锁也查出来,封装进lock对象中。
 */
public class Key {
    private Integer id;
    private String keyName;

    /**
     * 表示当前这把钥匙能开哪个锁
     */
    private Lock lock;
    //省略getter、setter、toString
}

JavaBean—>Lock类:

package nuc.pojo;

public class Lock {
    private Integer id;
    private String LockName;
	//省略getter、setter、toString
}

KeyDao接口:

public interface KeyDao {

    /**
     * 将钥匙和锁的信息一起查询出来
     * @param id
     * @return
     */
    Key getKeyById(Integer id);
}

KeyDao.xml:

  • 使用“左外连接查询”

  • SELECT k.id kid,k.keyname,k.lockid,l.id lid,l.lockname FROM t_key k LEFT JOIN t_lock l ON k.`lockid`=l.`id` WHERE k.id=1;
    
  • 在多表查询时,结果中有两个字段是同名的,为了后面的查询操作方便封装,需要在SQL中起别名。

  • 在级联查询时,使用自定义封装:

    • 使用<resultMap>标签:
      • property:JavaBean中的属性
      • column:数据库表中的值
<?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="nuc.inter.KeyDao">

    <select id="getKeyById" resultMap="myKey">

        SELECT k.id kid,k.keyname,k.lockid,l.id lid,l.lockname FROM t_key k
            LEFT JOIN t_lock l ON k.`lockid`=l.`id`
            WHERE k.id=#{id};
    </select>

<!--    自定义封装规则,使用级联属性封装查询出来的结果
property:JavaBean中的属性
column:数据库表中的值
-->
    <resultMap id="myKey" type="nuc.pojo.Key">
        <id property="id" column="id"></id>
        <result property="keyName" column="keyname"/>
        <result property="lock.id" column="lid"/>
        <result property="lock.LockName" column="lockname"/>
    </resultMap>

</mapper>

上面使用的时级联赋值,MyBatis中推荐使用的是association 标签

实例:

  1. 也是在resultMap标签中定义:
    1. association标签的属性:
      1. property:指定JavaBean中的那个属性。
      2. javaType;指定这个属性的类型。
    2. 然后,也是使用id标签和result标签对这个属性进行赋值。
<!--    MyBatis推荐的association 标签-->
    <resultMap id="myKey" type="nuc.pojo.Key">
        <id property="id" column="id"></id>
        <result property="keyName" column="keyname"/>
<!--       下面的属性是一个对象,自定义这个对象的封装规则,association表示联合了一个对象-->
<!--        javaType:指定这个属性的类型-->
        <association property="lock" javaType="nuc.pojo.Lock">
<!--            在association标签体中定义这个Lock对象如何封装-->
            <id property="id" column="lid"/>
            <result property="LockName" column="lockname"/>
        </association>
    </resultMap>

(3)一个JavaBean中的属性是一个集合。

需求:查锁子,也要查询出该锁子的所有钥匙:

Lock类:

public class Lock {
    private Integer id;
    private String LockName;
    /**
     * 查询锁子的时候,将该锁子的所有钥匙都查出来,放在这个List中
     */
    private List<Key> keys;
    //省略set,get方法
}

Key类:

public class Key {
    private Integer id;
    private String keyName;

    /**
     * 表示当前这把钥匙能开哪个锁
     */
    private Lock lock;
     //省略set,get方法
}

LockDao接口:

public interface LockDao {

    /**
     * 查询所有锁子,并将该锁子的所有钥匙都查出来
     */
    Lock getLockById(Integer id);
}

LockDao.xml:

  1. JavaBean中的属性是一个集合。
  2. 需要使用自定义封装规则,在<resultMap> 中使用 <collection> 标签定义这个集合的封装规则。
    1. property:指定哪个属性是集合属性
    2. ofType:指定这个集合中元素的类型
<?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="nuc.inter.LockDao">

    <select id="getLockById" resultMap="myLock">
        SELECT k.id kid,k.keyname,k.lockid,l.id lid,l.lockname
           FROM t_key k
           LEFT JOIN t_lock l ON k.`lockid`=l.`id`
           WHERE l.id=#{id}
    </select>

    <resultMap id="myLock" type="nuc.pojo.Lock">
        <id property="id" column="lid"/>
        <result property="LockName" column="lockname"/>
<!--
property:指定哪个属性是集合属性
ofType:指定这个集合中元素的类型
-->
        <collection property="keys" ofType="nuc.pojo.Key">
            <id property="id" column="kid"/>
            <result property="keyName" column="keyname"/>
        </collection>
    </resultMap>

</mapper>

SQL查询结果:


扩展思考:

1-1:一个key开一个lock

1-n:一个lock有多个key

n-n:一个老师教多个学生,一个学生又有多个老师

问题:1-n,n-1,n-n:外键放在那个表中呢?

  • 1-n:外键放在n的那一段。
  • n-1:和1-n一样,只是反过来看。
  • n-n:建一个中间表来存储对应关系

老师表:

学生表:

学生-老师关系表:

7. 分步查询

查钥匙的时候,顺便查出锁子

写两个简单的select语句,先查询出钥匙,再根据查询结果中的lockid查询出对应的锁子。

LockDao.xml中简单的select语句,根据id查询锁子。

<?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="nuc.inter.LockDao">

    <select id="getLockByIdSimple" resultType="nuc.pojo.Lock">
        select * from t_lock where id=#{id}
    </select>

</mapper>

KeyDao.xml:

  1. 在这个简单查询中使用自定义封装规则。
  2. 在自定义封装中,使用association 标签说明这个属性是JavaBean,然后再使用select属性指定一个查询SQL,使用column给这个SQL传递参数。
  3. 可以看到过程是执行了两条SQL语句。
<?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="nuc.inter.KeyDao">

    <select id="getKeyByIdSimple" resultMap="myKey02">
        select * from t_key where id=#{id}
    </select>

    <resultMap id="myKey02" type="nuc.pojo.Key">
        <id property="id" column="id"/>
        <result property="keyName" column="keyname"/>
<!--        可以告诉MyBatis,让他自己去查询(调用一个查询)
            select="":指定一个查询SQL的唯一标识,MyBatis自动调用这个指定的SQL将查询出的结果封装进对应对象
            告诉MyBatis将哪一列的值传入进去 column="lid"
-->
        <association property="lock" select="nuc.inter.LockDao.getLockByIdSimple" column="lockid"/>
    </resultMap>
</mapper>

需求:在如下的案例中

  • 需求中通常只需要查询key的名,lock只是偶尔会用到,但是MyBatis还是会每次都查询锁子,造成了浪费。

  • 解决:只需开启全局按需加载功能:

    • 在MyBatis的配置文件中设置:

    • 开启延迟加载功能:lazyLoadingEnabled 置为true。

    • 开启属性按需加载:aggressiveLazyLoading置为false

    •     <settings>
      <!--       开启延迟加载功能-->
              <setting name="lazyLoadingEnabled" value="true"/>
      <!--        开启属性按需加载-->
              <setting name="aggressiveLazyLoading" value="false"/>
          </settings>
      
@Test
public void test04()
{
    SqlSession sqlSession = sqlSessionFactory.openSession();

    KeyDao keyDao = sqlSession.getMapper(KeyDao.class);
    Key key = keyDao.getKeyByIdSimple(1);
    //如果
    System.out.println(key.getKeyName());

    //解决数据库资源的浪费:按需加载--->需要的时候才去查询
    //只需开启全局按需加载功能

}

因为代码中只需要key的name,所有就只执行一条SQL,只查询key的信息。

当需要查询lock的信息时,只管拿即可,MyBatis会自动去查询。

​ 例:给上述测试代码加上:System.out.println(key.getLock().getLockName()); ,之后,MyBatis就会执行两条SQL去查询出锁子的信息。

设置了懒加载之后,也可以在resultMap的association标签中使用,fetchType属性开启立即加载。eager表示立即加载。写上之后,就会覆盖MyBatis的全局设置。

    <resultMap id="myKey02" type="nuc.pojo.Key">
        <id property="id" column="id"/>
        <result property="keyName" column="keyname"/>
<!--        可以告诉MyBatis,让他自己去查询(调用一个查询)
            select="":指定一个查询SQL的唯一标识,MyBatis自动调用这个指定的SQL将查询出的结果封装进对应对象
            告诉MyBatis将哪一列的值传入进去 column="lid"
-->
        <association property="lock" select="nuc.inter.LockDao.getLockByIdSimple" column="lockid" fetchType="eager"/>
    </resultMap>

collection的分步查询和association类似。

推荐:使用连接查询,因为分步查询的效率不如连接查询

8. 动态SQL

动态SQL时MyBatis最强大的功能,极大的简化我们拼装SQL的操作。

有如下四个基本语法:

  1. if
  2. choose (when, otherwise)
  3. trim (where, set)
  4. foreach

(1)if标签

  • if标签的 test=""属性后:编写判断条件
    • 例:<if test="id!=null">
  • 注意:这里不能写&&,可以写and,也可以使用转义字符。例:&amp;
<!--  
 test="id!=null" 取出传入的JavaBean属性的id的值,判断是否为空

 -->
    <select id="getTeacherByCondition" resultMap="myTeacher">
        select * from t_teacher where
        <if test="id!=null">
            id > #{id} and
        </if>
        <if test="teacherName!=null">
            teacherName like #{teacherName}
        </if>
    </select>

(2) where标签

  • 可以帮我们去掉前面的and

(3)trim标签,截取字符串

prefix="" :前缀,为我们下面的SQL整体添加一个前缀。

prefixOverrides="":取出多余的字符串,例:and

suffixOverrides="" :去掉多余的字符串,例:AMD

suffix="":为所有SQL添加后缀

推荐使用where标签,并将所有的and写在前面。

(4)foreach标签:遍历集合

  • collection:指定要遍历的集合
  • item:每次遍历出的元素起一个变量名,方便引用。
  • open:开始遍历时的拼接字符串
  • close:结束时拼接的字符串
  • separator:遍历的元素之间的分隔符。
  • index: 索引
    • 如果遍历的是list,index保存就是当前元素的索引
    • 如果遍历的是map,index保存的就是当前元素的key。
<select id="getTeacherList" resultType="myTeacher">
    select * from t_teacher where id IN
    <where>
        <foreach collection="ids" item="id_item" open=" (" close=")" separator=",">
            #{id_item}
        </foreach>
    </where>
</select>

(5)choose标签

  • 只进一个when标签。
<select id="getTeahcers" resultType="myTeacher">
    select * from t_teacher
    <where>
        <choose>
            <when test="id!=null">
                id = #{id}
            </when>
            <when test="name!=null">
                teachername=#{name}
            </when>
             <when test="birth!=null">
                birth_date=#{birth}
            </when>
            <otherwise>
                1=1
            </otherwise>
        </choose>
    </where>
</select>

(6)set标签,update语句使用

  • set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号
    <update id="updateTeacher">
        update t_teacher
        <set>
            <if test="teacherName!=null">
                teachername = #{teacherName},
            </if>
            <if test="className!=null">
                class_name=#{className},
            </if>
        </set>
        <where>
            id=#{id}
        </where>
    </update>

(7)bind标签

绑定一个表达式给一个变量。

(8)sql标签:抽取可重用的SQL语句

  • 搭配include标签使用,引入SQL。
<sql id="sql01">
 	select * from t_teacher
</sql>


<select id="getTeacherById" resultMap="myTeacher">
       <include refid="sql01"></include>
</select>

9. 扩展:OGNL

    OGNL:( Object Graph Navigation Language )对象图导航语言,这是一种强大的表达式语言,通过它可以非常方便的来操作对象属性。 类似于我们的EL,SpEL等。

有类似以下的类,属性如下:

class Person{
	lastName;
    email;
    Address;
    	city;
    	province;
    	street;
}

用点分隔各层属性,就是导航图。如:person.Address.street

OGNL 不仅可以访问属性,也可以访问方法,静态方法,构造方法,运算符,逻辑运算符。

在MyBatis中,除了可以使用传入的参数来判断外,还可以使用两个额外的参数。

  1. _parameter 代表传入的参数
    1. 单个参数:就代表传入的参数。
    2. 多个参数:就代表多个参数组成的Map
  2. _databaseId 代表当前环境:MySQL,Oracle等
    1. 配置的数据库移植,才可以使用。databaseIdProvider标签。(在MyBatis核心配置文件中配置)

10. 缓存机制

  • 缓存:暂时的存储一些数据,加快系统的查询速度。
  • MyBatis中的缓存机制,本质就是一个Map,能保存查询出的一些数据。
    • 一级缓存:线程级别的;本地缓存;SqlSession级别的缓存(和数据库的一次会话);
    • 二级缓存:全局缓存;其他的sqlSession也能使用。

查询的时候,先看缓存中有没有,没有的话再去数据库中查。

(1)一级缓存

默认存在的。只要之前查询过的数据,MyBatis就会将其保存在缓存中(Map中),下次直接从缓存中拿。

    @Test
    public void test01()
    {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        TeacherDao teacherDao = sqlSession.getMapper(TeacherDao.class);
        Teacher teacher01 = teacherDao.getTeacherById(1);

        Teacher teacher02 = teacherDao.getTeacherById(1);
        System.out.println(teacher01==teacher02);
    }

输出:只执行一次SQL语句

一级缓存失效的情况:

  1. 一级缓存是sqlSession级别的缓存,不同的sqlSession,使用不同的一级缓存。
    1. 一个sqlSession查询到的数据会保存在这个sqlSession的一级缓存中,第二次如果查询相同的数据,就从缓存中拿,而不是再去查询数据库。
/**
 * 一级缓存失效的几种情况:
 * 
 */
@Test
public void test02()
{
    //第一次会话
    SqlSession sqlSession01 = sqlSessionFactory.openSession();
    TeacherDao teacherDao01 = sqlSession01.getMapper(TeacherDao.class);
    Teacher teacher01 = teacherDao01.getTeacherById(1);

    //第二次会话
    SqlSession sqlSession02 =sqlSessionFactory.openSession();
    TeacherDao teacherDao02 = sqlSession02.getMapper(TeacherDao.class);
    Teacher teacher02 = teacherDao02.getTeacherById(1);
    
    System.out.println(teacher01==teacher02);//false
}

输出:执行了两次SQL,查询数据库两次。

  1. 在两次拿数据之间,执行任意一个增删改操作。一级缓存就会失效。原因是:增删改操作会将缓存清空。
 @Test
    public void test02()
    {
        //第一次会话
        SqlSession sqlSession01 = sqlSessionFactory.openSession();
        TeacherDao teacherDao01 = sqlSession01.getMapper(TeacherDao.class);
        Teacher teacher01 = teacherDao01.getTeacherById(1);

        //执行一次增删改操作
        Teacher teacher = new Teacher();
        teacher.setId(2);
        teacher.setTeacherName("Test");
        teacherDao01.updateTeacher(teacher);


        Teacher teacher02 = teacherDao01.getTeacherById(1);

        System.out.println(teacher01==teacher02);//false

        sqlSession01.commit();

    }

输出:在一个sqlSession中查询同样的数据,也会执行两次SQL语句。

  1. 手动清空缓存:
  @Test
    public void test02()
    {
        //第一次会话
        SqlSession sqlSession01 = sqlSessionFactory.openSession();
        TeacherDao teacherDao01 = sqlSession01.getMapper(TeacherDao.class);
        Teacher teacher01 = teacherDao01.getTeacherById(1);


        //手动清空缓存
        sqlSession01.clearCache();
        Teacher teacher02 = teacherDao01.getTeacherById(1);

        System.out.println(teacher01==teacher02);//false

    }

总结:每次查询,先去缓存中查看有没有该数据,如果没有才发起新的SQL。每个sqlSession拥有自己的缓存。


MyBatis的缓存就是一个Map,如下:

保存的数据的key:hashCode+查询的SqlId+编写的sql查询语句+参数,我们可以打开它的源码看一下。

(2)二级缓存

  • 二级缓存:全局作用域缓存,默认不开启,需要手动配置。(从一级缓存搬到二级缓存)
  • 在MyBatis的配置文件中开启缓存。
  • 二级缓存在sqlSession关闭或提交之后才会生效。

MyBatis中使用二级缓存的步骤:

  1. 在MyBatis配置文件中开启二级缓存。
<!--        开启二级缓存,并告诉MyBatis哪个Dao查询的时候需要用到二级缓存-->
        <setting name="cacheEnabled" value="true"/>
  1. 在Dao的实现文件中使用二级缓存。TeacherDao.xml文件中加上。
<cache/>
  1. POJO需要实现Serializable接口。(序列化接口)

测试代码:

@Test
    public void test_03()
    {
        //创建两个会话
        SqlSession sqlSession01 = sqlSessionFactory.openSession();
        SqlSession sqlSession02 = sqlSessionFactory.openSession();

        UserDao UserDao01 = sqlSession01.getMapper(UserDao.class);
        UserDao UserDao02 = sqlSession02.getMapper(UserDao.class);

        //第一个Dao查询一号用户
        User user1 = UserDao01.getUserById(1);
        sqlSession01.close();
        System.out.println("user1: "+user1);

        //第二个Dao也查询一号用户
        User user2 = UserDao02.getUserById(1);
        sqlSession02.close();
        System.out.println("user2: "+user2);
       System.out.println("user1是否与user2相同:"+(user1==user2));

    }

}

输出:

显然只执行一次SQL

二级缓存总结:

  1. 二级缓存是namespace级别的缓存,哪个Dao要用缓存,就配置在哪个Dao的实现文件中。

  2. 在Dao中的<cache> 标签中可以配置的属性:

    1. eviction:缓存回收策略:
      1. LRU – 最近最少使用的:移除最长时间不被使用的对象。
      2. FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
      3. SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
      4. WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
        默认的是 LRU。
    2. flushInterval:刷新间隔,单位毫秒
      1. 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
    3. size:引用数目,正整数
      1. 代表缓存最多可以存储多少个对象,太大容易导致内存溢出
    4. readOnly:只读,true/false
      1. true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
      2. false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是 false。
  3. 不会出现二级缓存和一级缓存中有同一个数据。

    1. 二级缓存:一级缓存关闭了之后,才有二级缓存的数据。
    2. 一级缓存:当二级缓存中没该数据时,就先看一级缓存,当一级缓存中也没有时,就去查询数据库。
      1. 数据库中查询出来的数据,就放在一级缓存中。
  4. 任何时候都是先看二级缓存,再看一级缓存,再数据库。

缓存原理:

sql标签(增删改查)的flushCache属性:

  • 增删改默认flushCache=true。sql执行以后,会同时清空一级和二级缓存。
  • 查询默认flushCache=false。
  • sqlSession.clearCache():只是用来清除一级缓存

11. 整合第三方缓存

因为MyBatis的缓存太过简陋,就是一个Map。

MyBatis将Cache做成了一个接口,可以使用其他第三方缓存进行代替。

(1)导入jar包

ehcache-core:ehcache的核心包

mybatis-ehcache:ehcache和MyBatis的整合包

依赖的日志包:

slf4j-api:

slf4j-log4j12:

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.6.11</version>
</dependency>

<dependency>
   <groupId>org.mybatis.caches</groupId>
   <artifactId>mybatis-ehcache</artifactId>
   <version>1.1.0</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.0-alpha1</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>2.0.0-alpha1</version>
    <scope>test</scope>
</dependency>

(2)ehcache的配置文件

  • ehcache的配置文件叫ehcache.xml,放在类路径下。
  • 使用ehcache之后,pojo类可以不用实现 Serializable 接口。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
 <!-- 磁盘保存路径 -->
 <diskStore path="E:\ehcache" />
 
 <defaultCache 
   maxElementsInMemory="100000" 
   maxElementsOnDisk="10000000"
   eternal="false" 
   overflowToDisk="true" 
   timeToIdleSeconds="120"
   timeToLiveSeconds="120" 
   diskExpiryThreadIntervalSeconds="120"
   memoryStoreEvictionPolicy="LRU">
 </defaultCache>
</ehcache>
 
<!-- 
属性说明:
l diskStore:指定数据在磁盘中的存储位置。
l defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
 
以下属性是必须的:
l maxElementsInMemory - 在内存中缓存的element的最大数目 
l maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
l eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
l overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
 
以下属性是可选的:
l timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
l timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
 diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
l diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
l diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
l memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
 -->

(3)在Mapper.xml文件中配置使用自定义的缓存

<!--    使用的是MyBatis的默认二级缓存,type:指定相应的自定义缓存类-->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>

【系列文章】
1. Git&GitHub(基础)
2. Git&GitHub(进阶)
3. Java多线程
4. JavaScript 总结
5. SpringMVC(一)
6. SpringMVC(二)
……

关注博主🤞🤞

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值