Mybatis框架的基本知识梳理

Mybatis框架的基本知识梳理

一、原始JDBC开发存在的问题

import org.junit.Test;

import java.math.BigDecimal;
import java.sql.*;

public class JdbcTest {

    @Test
    public void testJdbc(){

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try {
            //1.反射加载驱动
            Class.forName("com.mysql.jdbc.Driver");

            //2.获取数据库连接
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/java", "root", "root");

            //3.编写SQL
            String sql = "SELECT * FROM t_user WHERE user_id = ?";

            //4.获取执行SQL的载体对象,预编译SQL
            preparedStatement = connection.prepareStatement(sql);
            //填充占位符
            preparedStatement.setLong(1, 3);

            //5.执行SQL
            resultSet = preparedStatement.executeQuery();

            //6.处理结果
            if (resultSet.next()) {
                //user_id : 结果集的列名
                long userId = resultSet.getLong("user_id");
                String username = resultSet.getString("username");
                BigDecimal balance = resultSet.getBigDecimal("balance");
                System.out.println(userId + "," + username + "," + balance );
                // 数据的列取出后---》封装成对象
                每个列的值要和对象属性值对象
                 User user =new User();
                uset.setUserId(userId);
                user.setUsername(username);
                ....
                    
                  //DbUtils      
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {

            //7.释放资源
            //先开后关
            try {
                if(null != resultSet) {
                    resultSet.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if(null != preparedStatement) {
                    preparedStatement.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if(null != connection) {
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

    }
}

存在的问题

1.需要频繁的手动获取连接

2.需要手动封装查询结果集

3.需要手动释放资源

4.SQL硬编码

5.有自己封装的工具类,也有Dbutils等工具类API。但是都没有从根本上解决上面的问题

二、ORM框架

  • 通过ORM框架解决JDBC存在的问题

1、ORM

  • Object Relation Mapping:对象关系映射 Java对象与关系型数据库中每行数据的对应关系
Java对象数据库表
类名表名
属性名列名【字段名】
对象行【记录】

2、框架

  • 概述:就是一个半成品软件
    • 需要使用框架帮我们完成JavaEE应用的开发
  • 作用
    • 能够帮我们快速有效的开发JavaEE应用
    • 有自己对应用场景的完整解决方案
1.什么是框架

为了解决一个共性的问题(比较繁琐、但是有规律),而提供的一个解决方案。

​ 比如:现在遇到数据层查询比较繁琐,但是有规律,交给框架去做。

框架的本质:就是jar+配置文件

​ jar包里封装功能,提供一些api,提供一些接口、类、方法,只负责调用,因为都给我们封装好了

​ 配置文件:把一些不怎么变化的内容直接发到配置文件里,如果直接写在Java代码里,是需要编译的

2.框架是解决什么问题

数据层问题

3.框架的基本原理

1.sql语句的配置文件

​ DeptMapper.xml: 存储部门相关的sql语句

​ EmpMapper.xml: 存储员工相关的sql语句

2.数据层接口

​ DeptMapper.java 部门操作的数据层接口

​ EmpMapper.java 员工操作的数据层接口

3.总配置文件

​ 总的配置文件:主要是放jdbc参数的,因为mybatis底层就是jdbc

3、程序跑通

1.导入jar

​ mybatis.jar +jdbc.jar

2.准备mybatis相关的配置文件

3.调用jar包里的api实现查询功能、增加功能

三、Mybatis

1.概述

  • 是一款开源的、优秀的、支持定制SQL的ORM框架

  • 官网:https://mybatis.org/mybatis-3/zh/index.html

    MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

2. 实现步骤

  1. 创建数据库表
  2. 导入依赖
  3. 实体类
  4. Mapper接口(Dao接口)
  5. SQL映射文件
  6. 全局配置文件
  7. 测试

3. 具体实现

  • 创建数据库表
CREATE TABLE `t_user` (
  `user_id` bigint(100) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(20) DEFAULT NULL COMMENT '用户名',
  `password` varchar(32) DEFAULT NULL COMMENT '密码',
  `nick_name` varchar(20) DEFAULT NULL COMMENT '昵称',
  `is_admin` tinyint(4) DEFAULT NULL COMMENT '是否管理员 0:否  1:是',
  `phone` varchar(11) DEFAULT NULL COMMENT '手机',
  `gender` tinyint(4) DEFAULT NULL COMMENT '性别 0:保密 1:男 2:女',
  `birth` date DEFAULT NULL COMMENT '生日',
  `user_status` tinyint(4) DEFAULT NULL COMMENT '状态(是否激活) 0:否 1:是',
  `user_create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `user_update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `is_delete` tinyint(4) DEFAULT NULL COMMENT '是否删除 0:否 1:是',
  `is_member` tinyint(4) DEFAULT NULL COMMENT '是否会员 0:否 1:是',
  `balance` decimal(20,2) DEFAULT NULL COMMENT '账户余额',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='这是一个用户表';


insert  into `t_user`(`user_id`,`username`,`password`,`nick_name`,`is_admin`,`phone`,`gender`,`birth`,`user_status`,`user_create_time`,`user_update_time`,`is_delete`,`is_member`,`balance`) values 
(1,'aa','123456','666',1,'13566778899',1,'1990-07-18',0,'2021-10-15 10:56:40','2022-10-22 10:56:43',0,1,10000.00),
(2,'bb','123456','666',1,'13856789526',1,'1990-07-18',0,'2021-11-02 14:20:25','2022-11-03 09:25:58',0,1,1111.00),
(3,'cc','123456','666',1,'13512341234',0,'1990-07-18',0,'2021-11-03 10:05:37','2022-11-03 10:05:37',0,1,10000.00),
(5,'dd','123456','666',1,'13512341234',0,'1990-07-18',0,'2021-11-03 10:15:00','2022-11-03 10:15:00',0,1,10000.00),
(6,'ee','123456','666',1,'13512341234',2,'1990-07-18',0,'2021-11-03 10:40:01','2022-11-03 10:40:01',0,1,10000.00),
(7,'ff','123456','666',0,'13512341234',0,'1990-07-18',0,'2021-11-05 11:23:45','2022-11-05 11:23:45',0,1,10000.00);
  • 导入依赖
<dependencies>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.6</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.2</version>
    </dependency>

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

    <!-- lombok : 能够快速帮我们生成实体的getter/setter方法 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.22</version>
        <scope>provided</scope>
    </dependency>

</dependencies>
  • 实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private Long userId;
    private String username;   //成员变量
    private String password;
    private String nickName;
    private Integer isAdmin;
    private String phone;
    private Integer gender;
    private Date birth;
    private Integer userStatus;
    private Date userCreateTime;
    private Date userUpdateTime;
    private Integer isDelete;
    private Integer isMember;
    private BigDecimal balance;

}
  • Mapper接口
public interface UserMapper {

    /**
     * 查询所有
     * @return
     */
    List<User> selectAll();

}
  • SQL(Mapper)映射文件
<?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">
<!--
    mybatis代理接口开发的要求:
        1.mapper映射文件的namespace 写 mapper接口的全类名
        2.mapper映射文件的statementId 写 mapper接口的对应方法名
        3.mapper映射文件的resultType 写 mapper接口的对应方法返回值类型。如果是集合,写泛型
        4.mapper映射文件的parameterType 写 mapper接口的对应方法形参类型。高版本mybatis可以不写,但是不推荐
 -->
<mapper namespace="com.gl.ssm.mapper.UserMapper">
    <select id="selectAll" resultType="com.gl.ssm.pojo.User">
        <!-- 原生SQL -->
        SELECT * FROM t_user
    </select>
</mapper>
  • 全局配置文件
<?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/java"/>//数据库名称
                <property name="username" value="root"/>//数据库账号
                <property name="password" value="root"/>//数据库密码
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/gl/ssm/mapper/IUserMapper.xml"/>
    </mappers>
</configuration>
  • 测试代码
/**
 * mapper接口代理测试
 **/
@Test
public void test02() {
    SqlSession session = null;
    try {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        session = sqlSessionFactory.openSession();
        //获取mapper代理
        IUserMapper userMapper = session.getMapper(UserMapper.class);
        List<User> users = userMapper.selectAll();
        //mybatis能够帮我们自动完成查询结果集和实体间的映射,前提结果集列名和实体属性名相同
        for (User user : users) {
            System.out.println(user);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if(null != session) {
            session.close();
        }
    }
}

四、CURD操作数据库

增删改操作涉及事务,务必要提交

1.增加

  • Mapper接口
/**
 * 增加
 * @param user
 */
void insertUser(User user);
  • Mapper映射文件
<!-- void insertUser(User user);-->
<insert id="insertUser" parameterType="com.gl.ssm.pojo.User">
    INSERT INTO t_user (
      username,password,nick_name,is_admin,phone,gender,birth,user_status,
      user_create_time,user_update_time,is_delete,is_member,balance)
    VALUES
      (#{username}, #{password}, #{nickName}, #{isAdmin}, #{phone}, #{gender}, #{birth}, #{userStatus}, #{userCreateTime}, #{userUpdateTime}, #{isDelete}, #{isMember}, #{balance})
</insert>
  • 测试类
/**
 * 增加
 **/
@Test
public void test03() throws Exception {
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = factory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = new User();
    user.setUsername("lucy");
    user.setPassword("147852");
    user.setNickName("露西");
    user.setIsAdmin(0);
    user.setPhone("13566778899");
    user.setGender(0);
    user.setBirth(java.sql.Date.valueOf("2022-10-30"));
    user.setUserStatus(1);
    user.setUserCreateTime(new Date());
    user.setUserUpdateTime(new Date());
    user.setIsDelete(0);
    user.setIsMember(1);
    user.setBalance(new BigDecimal(2000));
    userMapper.save(user);

    //提交事务
    sqlSession.commit();

    if(null != sqlSession) {
        sqlSession.close();
    }

}
1.1 主键回填
  • 主键自增

    • 方式一【Mapper映射文件】
    <!--void saveReturnPrimaryKey(User user);-->
    <insert id="saveReturnPrimaryKey" parameterType="com.gl.ssm.pojo.User">
    
        <!--
            selectKey : 是指要执行的相关的SQL
                order: selectKey中SQL的执行顺序,after代表之后,before代表之前
                resultType : selectKey中SQL的返回值类型
                keyProperty  : selectKey中SQL的返回值要赋值给哪个JavaBean的属性
                keyColumn : selectKey中SQL的返回值对应的数据库表的列【这个值可以不写,会自动映射】
        -->
        <selectKey order="AFTER" resultType="java.lang.Long" keyProperty="userId" keyColumn="user_id">
            SELECT LAST_INSERT_ID()
        </selectKey>
        INSERT INTO t_user 
        	(username,password,nick_name,is_admin,phone,gender,birth,user_status,
          user_create_time,user_update_time,is_delete,is_member,balance)
        VALUES
            (#{username}, #{password}, #{nickName}, #{isAdmin}, #{phone}, #{gender}, #{birth},  #{userStatus},#{userCreateTime}, #{userUpdateTime}, #{isDelete}, #{isMember}, #{balance})
    </insert>
    
  • 方式二

  <!-- void saveReturnPrimaryKey2(User user); -->
  <!--
      useGeneratedKeys : 是否使用主键生成策略
          true:是
      这种方式可以获取批量插入的主键值
   -->
  <insert id="saveReturnPrimaryKey2" parameterType="com.gl.ssm.pojo.User"
      useGeneratedKeys="true" keyProperty="userId" keyColumn="user_id">
      INSERT INTO t_user
          (username,password,nick_name,is_admin,phone,gender,birth,user_status,
          user_create_time,user_update_time,is_delete,is_member,balance)
      VALUES
      (#{username}, #{password}, #{nickName}, #{isAdmin}, #{phone}, #{gender}, #{birth}, #{userStatus}, #{userCreateTime}, #{userUpdateTime}, #{isDelete}, #{isMember}, #{balance})
  </insert>

2.修改

<!--int update(User user);-->
<update id="update">
    UPDATE t_user
    SET PASSWORD = #{password}, nick_name = #{nickName}, user_update_time = #{userUpdateTime}
    WHERE user_id = #{userId}
</update>

3.删除

<!--boolean deleteById(Long userId);-->
<delete id="deleteById" parameterType="long">
    DELETE FROM t_user WHERE user_id = #{userId}
</delete>

4.主键是字符串实现主键回填

  • 实体类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Stu {

    private String stuId;
    private String stuName;

}
  • Mapper接口和映射文件
<?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="com.gl.ssm.mapper.StuMapper">

    <!-- int saveReturnPrimaryKey(Stu stu); -->
    <insert id="saveReturnPrimaryKey" parameterType="com.gl.ssm.pojo.Stu">
        <selectKey order="BEFORE" keyProperty="stuId" resultType="string" keyColumn="stu_id">
            SELECT REPLACE(UUID(), '-', '')
        </selectKey>
        insert into t_stu (stu_id, stu_name) values (#{stuId},#{stuName})
    </insert>

</mapper>

五、日志

1.日志体系

  • Slf4j:接口【门面】
    • Log4j
    • Logback
    • commons-logging

2.日志作用

  • 没日志:很难判断问题来源
  • 有日志:一般都可以判断问题来源。
    • 可以通过日志把问题错误信息给记录下来

3.使用

  • 引入依赖
<!-- 日志 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.7</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
  • 日志配置文件【log4j.properties】
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.err
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=ssm.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
log4j.rootLogger=debug, stdout, file
  • 日志级别
级别描述
ALL LEVEL打开所有日志记录开关;是最低等级的,用于打开所有日志记录。
DEBUG[输出调试信息;指出细粒度信息事件对调试应用程序是非常有帮助的。【开发使用】
INFO输出提示信息;消息在粗粒度级别上突出强调应用程序的运行过程。【线上使用】
WARN输出警告信息;表明会出现潜在错误的情形。
ERROR输出错误信息;指出虽然发生错误事件,但仍然不影响系统的继续运行。
FATAL输出致命错误;指出每个严重的错误事件将会导致应用程序的退出。
OFF LEVEL关闭所有日志记录开关;是最高等级的,用于关闭所有日志记录。
  • 直接使用API即可
package com.qf.java2107.test;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LoggerTest {

    Logger logger = LoggerFactory.getLogger(LoggerTest.class);

    @Test
    public void logTest() throws Exception {
        logger.debug("debug---->{}", "debug level");
        logger.info("info--->{}---{}", "aa", "bb");
        logger.warn("warn----->");
        logger.error("error---->");
    }
}

六、Mapper接口参数绑定

1.单个简单类型参数

基本数据类型及其包装类,String,int,

2.实体类型参数

JavaBean

Mapper映射文件获取参数值时,使用#{},{}中写JavaBean的属性名

3.Map入参

Mapper映射文件获取参数值时,使用#{},{}中写Map中key的名称

4.多个参数

mybatis会把参数封装成一个Map,第一个参数的key为arg0param1】,第二参数的key为arg1param2】,以此类推。

以上方式不推荐使用,可读性太差。

Mapper映射文件获取参数值时,使用@Param来为Mapper接口方法指定入参的参数名,使用#{}取值

七、ORM映射

把查询结果集跟JavaBean进行映射绑定

1.映射规则

  • 当查询结果集的列名跟JavaBean属性名相同时,自动映射

  • 当查询结果集的列名跟JavaBean属性名不相同时,需要手动映射

2.不满足映射规则

  • 如果满足驼峰

    • 使用别名进行映射【但是SQL太长】
    <!-- User selectById(Long userId); -->
    <select id="selectById" parameterType="Long" resultType="com.gl.ssm.pojo.User">
        SELECT * FROM t_user  WHERE user_id = #{userId}
    </select>
    
    • 全局配置文件【mybatis-config.xml】开启驼峰映射
    <settings>
        <!-- 开启驼峰映射,编写的SQL就不用使用别名了 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    
    <!-- User findByIdMapping2(Long userId); -->
    <select id="findByIdMapping2" parameterType="Long" resultType="com.gl.ssm.pojo.User">
        SELECT
            user_id,
            username,
          password,
            nick_name,
          is_admin,
            phone,
            gender,
            birth,
            user_status,
            user_create_time,
            user_update_time,
          is_delete,
            is_member,
            balance
        FROM
            t_user
        WHERE user_id = #{userId}
    </select>
    

3.ResultMap

  • 自定义结果集映射
    • mybatis内部已经集成了结果集映射,就是Mapper映射文件的resultType属性。resultType底层使用其实也是ResultMap
    • resultType和resultMap最好只使用一个
      • 如果满足驼峰并且不使用别名的情况下,可以使用resultType
      • resultMap可以在查询结果集封装中自定义使用。连表查询只能使用resultMap
<!--
    resultMap: 自定义结果集
      id : resultMap的名称,是一个唯一标识,用于被select的resultMap属性所引用
      type : 查询的结果集要映射到的实体类型
 -->
<resultMap id="ResultMap" type="com.gl.ssm.pojo.User">
    <!--
        id : 映射主键列,只是一个标识作用,也可以用result
            column : 查询的结果集的列名
            property : JavaBean的属性名
    -->
    <id column="uid" property="userId"/>
    <!-- result : 映射普通列 -->
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <result column="name" property="nickName"/>
    <result column="is_admin" property="isAdmin"/>
    <result column="phone" property="phone"/>
    <result column="sex" property="gender"/>
    <result column="birth" property="birth"/>
    <result column="user_status" property="userStatus"/>
    <result column="ctime" property="userCreateTime"/>
    <result column="user_update_time" property="userUpdateTime"/>
    <result column="is_delete" property="isDelete"/>
    <result column="is_member" property="isMember"/>
    <result column="balance" property="balance"/>
</resultMap>

<!-- User findByIdUseResultMap(Long userId); -->
<select id="findByIdUseResultMap" parameterType="Long" resultMap="myResultMap">
    SELECT
        user_id uid, username, password, nick_name name, is_admin, phone, gender sex, 
        birth, user_status, user_create_time ctime, user_update_time, is_delete, is_member, balance
    FROM
        t_user
    WHERE user_id = #{userId}
</select>

八、全局配置文件

1.properties

<!--
    properties : 加载外部properties文件
        resource : properties文件基于classpath的路径
-->
<properties resource="jdbc.properties" />

2.settings

<!-- 全局设置 -->
<settings>
    <!-- 开启驼峰映射 -->
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

3.typeAliases

  • 自定义别名
<!-- 别名设置 -->
<typeAliases>
    <!-- 单个取别名 -->
    <!--<typeAlias type="com.gl.ssm.pojo.User" alias="user"></typeAlias>
    <typeAlias type="com.gl.ssm.pojo.Student" alias="student"></typeAlias>-->
    <!-- 批量取别名,默认别名是类名,别名不区分大小写 -->
    <package name="com.gl.ssm.pojo"/>
</typeAliases>

4.plugins

4.1 分页插件概述
  • 可以屏蔽底层数据库的差异,实现同一API实现不同数据库的分页功能
  • https://gitee.com/free/Mybatis_PageHelper
4.2 使用步骤
  • 导入依赖
//分页插件依赖
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.10</version>
</dependency>
  • 全局配置文件
<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 配置方言,不配置,则使用数据库连接来判断 -->
        <property name="helperDialect" value="mysql"/>
        <!-- 合理化参数 -->
        <property name="reasonable" value="true"/>
	</plugin>
</plugins>
  • 使用

    • Mapper映射文件
    <!-- List<User> selectAll(); -->
    <select id="selectAll" resultType="User">
        select * from t_user
    </select>
    
    • 测试代码
    /**
     * 分页
     **/
    @Test
    public void Test() throws Exception {
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
        //分页设置跟查询之间不要出现其他操作
        //参数一:页码
        //参数二:显示条数
        PageHelper.startPage(3, 5);
        List<User> list = userMapper.selectAll();
    
        PageInfo<User> pageInfo = new PageInfo<>(list);
        System.out.println("当前页:" + pageInfo.getPageNum());
        System.out.println("当前页集合:" + pageInfo.getList());
        System.out.println("总页数:" + pageInfo.getPages());
        System.out.println("总条数:" + pageInfo.getTotal());
        System.out.println("显示条数:" + pageInfo.getPageSize());  //显示条数
        System.out.println("实际显示条数:" + pageInfo.getSize());  //实际显示条数
    
    }
    

5.environments

  • 环境配置
<!-- 环境配置 -->
<!--
    default 默认使用哪个环境,这个值是指environments下的某个environment子标签的id属性
 -->
<environments default="mysqldb">
    <!--
        mysql环境
            id :就是当前环境的唯一标识,可能被environments的default引用
    -->
    <environment id="mysqldb">
        <!--
            transactionManager : 事务管理器
                type : JDBC
         -->
        <transactionManager type="JDBC"></transactionManager>
        <!--
            dataSource : 数据源
                type : POOLED 池化

         -->
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
    <!-- oracle环境 -->
    <!--<environment id="oracledb">
        <transactionManager type="JDBC"></transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>-->
</environments>

6.mappers

  • 用来加载Mapper映射文件
<!-- 加载mapper映射文件 -->
<mappers>
    <!--
        mapper : 加载单个mapper映射文件
            resource :基于classpath路径下的mapper映射文件
            class : 基于mapper接口,mybatis注解开发方式。写mapper接口全类名
    -->
    <!--<mapper resource="com/gl/ssm/mapper/UserMapper.xml" ></mapper>-->
    <!--
        批量加载mapper映射文件
           要求:mapper映射文件跟mapper接口在编译后必须在同一个路径下
    -->
    <package name="com.gl.ssm.mapper"/>
</mappers>

九、连表查询

1.表与表之间的关系

  • 数据库层面

    • 一对多:部门对员工、公司对部门
    • 多对多:项目跟程序员、学生跟老师
    • 多对一:学生对班级、员工对部门
    • 一对一:人跟身份证、旅客跟护照
  • mybatis层面

    • 一对多【多对多】
    • 一对一【多对一】

2.部门表跟员工表为例

  • 一对多:查询部门关联查询员工
  • 一对一:查询员工关联查询部门
2.1 准备工作
  • 数据库表
CREATE TABLE `dept` (
  `deptid` INT(11) NOT NULL AUTO_INCREMENT,
  `deptname` VARCHAR(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

INSERT  INTO `dept`(`deptid`,`deptname`) VALUES 
(1,'研发部'),
(2,'市场部'),
(3,'财务部'),
(4,'测试部');


CREATE TABLE `emp` (
  `empid` INT(11) NOT NULL AUTO_INCREMENT,
  `empname` VARCHAR(20) NOT NULL,
  `sex` INT(1) DEFAULT NULL COMMENT '1:男 0:女',
  `birthday` DATE DEFAULT NULL,
  `hire_date` DATETIME DEFAULT NULL,
  `salary` INT(11) DEFAULT NULL,
  `address` VARCHAR(200) DEFAULT NULL,
  `deptid` INT(11) DEFAULT NULL,
  PRIMARY KEY (`empid`)
) ENGINE=INNODB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;


INSERT  INTO `emp`(`empid`,`empname`,`sex`,`birthday`,`hire_date`,`salary`,`address`,`deptid`) VALUES 
(1,'王八',0,'2000-10-31','2021-10-01 09:00:00',8000,'杭州江干',2),
(2,'李四',1,'1985-11-10','2006-12-12 18:10:10',9000,'李家村',1),
(3,'王五',0,'1991-10-02','2009-12-02 00:00:00',1500,'王家村',2),
(5,'AA',1,'2021-10-04','2021-11-11 00:00:00',15000,'杭州',2),
(7,'CC',1,'2019-05-12','2021-11-07 00:00:00',6000,'杭州',2),
(8,'DD',1,'2021-11-05','2021-11-05 00:00:00',6000,'杭州',1),
(9,'九妹儿',0,'2021-11-11','2005-12-06 00:00:00',11111,'远古时期',NULL),
(10,'萧炎',1,'1999-12-12','2021-02-03 00:00:00',5000,'牛田村',1),
(11,'萧媚',1,'1985-11-11','2006-12-12 00:00:00',9000,'李家村',1),
(12,'火灵儿',0,'1991-10-02','2009-12-02 00:00:00',1500,'王家村',2),
(13,'林动',1,'2021-11-05','2021-11-05 00:00:00',6000,'杭州',1),
(14,'凌青竹',1,'2021-11-05','2021-11-05 00:00:00',6000,'杭州',1),
(15,'纪宁',1,'2021-11-05','2021-11-05 00:00:00',6000,'杭州',2),
(16,'北冥',1,'2021-11-05','2021-11-05 00:00:00',6000,'杭州',1),
(17,'顾清风',0,'2021-11-11','2005-12-06 00:00:00',11111,'远古时期',NULL),
(18,'余生',0,'2021-11-11','2005-12-06 00:00:00',11111,'远古时期',2),
(19,'花解语',0,'2021-11-11','2005-12-06 00:00:00',11111,'远古时期',1),
(100,'张三',1,'1999-12-12','2021-02-03 00:00:00',5000,'牛田村',1);

2.2 一对多

Mapper接口和Mapper映射文件
  • Mapper接口
public interface DeptMapper {

    Dept findByIdAndEmps(Integer id);

}
  • Mapper映射文件
    <resultMap id="DeptAndEmpsResultMap" type="com.gl.ssm.pojo.Dept" extends="BaseResultMap">

        <!-- 映射一对多【集合】 -->
        <!--
            collection: 映射一对多【集合】
                property: 集合名
                ofType : 集合中的元素类型
         -->
        <collection property="emp" ofType="com.gl.ssm.pojo.Emp">
            <!-- 映射单个员工 -->
            <id column="empid" property="id"/>
            <result column="empname" property="empName"/>
            <result column="sex" property="sex"/>
            <result column="birthday" property="birthday"/>
            <result column="hire_date" property="hireDate"/>
            <result column="salary" property="salary"/>
            <result column="address" property="address"/>
            <result column="deptid" property="deptId"/>
        </collection>
    </resultMap>

    <resultMap id="BaseResultMap" type="com.qf.java2107.pojo.Department">
        <id column="id" property="id"/>
        <result column="dept_name" property="deptName"/>
    </resultMap>

    <!-- Department findByIdAndEmps(Integer id); -->
    <select id="findByIdAndEmps" parameterType="int" resultMap="DeptAndEmpsResultMap">
        SELECT
           d.id,
           d.dept_name,
           e.id emp_id,
           e.emp_name,
           e.gender,
           e.birthday,
           e.hire_date,
           e.salary,
           e.address,
           e.dept_id
        FROM
        t_department d, t_employee e
        WHERE d.id = e.dept_id
        AND d.id = #{id}
    </select>

2.3 一对一

  • 根据ID查询员工信息且关联部门信息
Mapper接口和Mapper映射文件
  • Mapper接口
public interface IEmployeeMapper {

    Employee findByIdAndDept(Integer id);

}
  • Mapper映射文件
<?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="com.gl.ss.mapper.EmpMapper">

    <resultMap id="BaseResultMap" type="com.gl.ssm.pojo.Emp">
        <id column="id" property="id"/>
        <result column="empname" property="empName"/>
        <result column="sex" property="sexr"/>
        <result column="birthday" property="birthday"/>
        <result column="hire_date" property="hireDate"/>
        <result column="salary" property="salary"/>
        <result column="address" property="address"/>
        <result column="deptid" property="deptId"/>
    </resultMap>

    <resultMap id="EmpAndDeptResultMap" type="com.gl.ssm.pojo.Emp" extends="BaseResultMap">
        <!--
            association : 映射实体
                property : 实体属性名
                javaType : 实体属性全类名
            -->
        <association property="department" javaType="com.gl.ssm.pojo.Dept">
            <id column="deptid" property="deptid"/>
            <result column="deptname" property="deptname"/>
        </association>
    </resultMap>

    <!-- Employee findByIdAndDept(Integer id); -->
    <select id="findByIdAndDept" parameterType="int" resultMap="EmpAndDeptResultMap">
        SELECT
           e.id empid,
           e.empname,
           e.sex,
           e.birthday,
           e.hire_date,
           e.salary,
           e.address,
           e.deptid,
           d.id deptid,
           d.deptname
        FROM
        t_emp e JOIN t_dept d
        ON e.deptid = d.deptid
        WHERE e.id = #{id}
    </select>
</mapper>

十、分步查询、延迟加载

1、分步查询

  • 是连表查询的另一种方式
    • 把连表查询的SQL进行拆分出多个单表查询的SQL

2、一对多

  • 查询部门信息关联查询员工信息

2.1 DeparmentMapper映射文件

<!-- =================分步查询====================== -->
<resultMap id="DeptAndEmpStepQueryResultMap" type="com.gl.ssm.pojo.Dept">
    <id column="deptid" property="deptid"/>
    <result column="deptname" property="deptname"/>
    <!-- 映射集合 -->
    <collection property="Emp" ofType="com.gl.ssm.pojo.Emp"
        select="com.gl.ssm.mapper.EmpMapper.findByDeptId" column="empid">
    </collection>
</resultMap>

<!--Department findByIdUseStepQuery(Integer empid);-->
<select id="findByIdUseStepQuery" parameterType="int" resultMap="DeptAndEmpStepQueryResultMap">
    SELECT deptid, deptname FROM t_dept WHERE deptid = #{deptid}
</select>

2.2 EmployeeMapper映射文件

<!--==================分步查询相关======================== -->
<!--List<Employee> findByDeptId(Integer deptid);-->
<select id="findByDeptId" parameterType="int" resultMap="BaseResultMap">
    SELECT * FROM t_emp WHERE deptid = #{deptid}
</select>

3、延迟加载【面试题】

也叫懒加载,也叫按需加载

当需要使用到关联的数据时,才去执行查询操作

实现原理:Cglib动态代理【基于继承】

延迟加载只会出现在分步查询中。一般延迟加载的数据都是大数据【如集合】

  • 即时加载:先执行完所有的SQL,再打印数据
  • 延迟加载,先执行需要获取数据的SQL,再打印数据。后面如果还需要获取数据,再执行SQL,再打印数据

十一、动态SQL

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

1、if

  • 条件判断
    • 成立,就拼接SQL

2、where(起始特殊形式的trim应用)

  • 功能相当于数据库的where关键字
<!-- List<Employee> findWithIf(Employee employee); -->
<select id="findWithIf" parameterType="com.gl.ssm.pojo.Emp" resultType="com.qf.java2107.pojo.Employee">
    SELECT * FROM t_emp
    <!--WHERE 1=1-->
    <!-- where 会忽略条件成立的最前面的多余的and或者or -->
    <where>
        <if test="empname != null and empname.trim() != ''">
            AND empname LIKE #{empname}
        </if>
        <if test="salary != null and salary > 0">
            AND salary = #{salary}
        </if>
        <if test="sex == 0 || sex == 1">
            AND sex = #{sex}
        </if>
        <if test="deptid != null">
            AND deptid = #{deptid} 
        </if>
    <!-- 除SQL之外的地方都是JavaBean的属性名 -->
    </where>
</select>

3、trim【理解】

  • 可以自定义的拼接或去掉SQL片段前后缀
    <!-- List<Emp> findWithTrim(Emp emp); -->
    <select id="findWithTrim" parameterType="com.gl.ssm.pojo.Emp" resultType="com.gl.ssm.pojo.Emp">
        SELECT * FROM t_emp
        <!--
            prefix : 要加的前缀
            prefixOverrides : 要去掉的前缀
            suffix  : 要加的后缀
            suffixOverrides : 要去掉的后缀
        -->
        <!--<trim prefix="where" prefixOverrides="and | or" suffix="" suffixOverrides="">
            <if test="empname != null and empname.trim() != ''">
                AND empname LIKE #{empname}
            </if>
            <if test="salary != null and salary > 0">
                AND salary = #{salary}
            </if>
            <if test="sex == 0 || sex == 1">
                AND sex = #{sex}
            </if>
            <if test="deptid != null">
                AND deptid = #{deptid}
            </if>
        </trim>-->

        <trim prefix="where" prefixOverrides="" suffix="" suffixOverrides="AND | OR">
            <if test="empname != null and empname.trim() != ''">
                 empname LIKE #{empname} AND
            </if>
            <if test="salary != null and salary > 0">
                 salary = #{salary} AND
            </if>
            <if test="sex == 0 || sex == 1">
                 sex = #{sex} AND
            </if>
            <if test="deptid != null">
                 deptid = #{deptid} AND
            </if>
        </trim>

    </select>

4、forEach

<!-- List<Emp> findByIds(List<Integer> ids); -->
<select id="findByIds" parameterType="list" resultType="com.gl.ssm.pojo.Emp">
    <!--SELECT * FROM t_emp WHERE id IN (1,25,3,62)-->
    SELECT * FROM t_emp
    <where>
        <if test="list != null and list.size() > 0">
            <!--id IN (1,25,3,62)-->
            <!--
                collection : 要遍历的集合,可以使用别名,但是如果使用了@Param指定入参key,那么就指定这个key
                item : 正在迭代的元素名,自己起名
                separator : 元素之间的分隔符
                open : 要遍历的元素开始之前的SQL片段
                close : 要遍历的元素结束之后的SQL片段
            -->
            <foreach collection="list" item="id" separator="," open="id IN (" close=")">
                #{id}
            </foreach>
        </if>
    </where>
</select>

5、choose…when…otherwise【了解】

<!-- List<Emp> findWithChoose(Emp emp); -->
<select id="findWithChoose" parameterType="com.gl.ssm.pojo.Emp" resultType="com.gl.ssm.pojo.Emp">
    SELECT * FROM t_emp
    <where>
        <choose>
            <when test="empname != null and empname.trim() != ''">
                AND empname LIKE #{empname}
            </when>
            <when test="salary != null and salary > 0">
                AND salary = #{salary}
            </when>
            <when test="sex == 0 or sex == 1">
                AND sex = #{sex}
            </when>
            <otherwise>
                deptid = 1
            </otherwise>
        </choose>
    </where>
</select>

6、sql…include

    <!-- List<Emp> findByIds(List<Integer> ids); -->
    <select id="findByIds" parameterType="list" resultType="com.gl.ssm.pojo.Emp">
        <include refid="BaseSelect"></include>
        <where>
            <if test="list != null and list.size() > 0">
                <foreach collection="list" item="id" separator="," open="id IN (" close=")">
                    #{id}
                </foreach>
            </if>
        </where>
    </select>
    <sql id="BaseSelect">
        SELECT
         <include refid="BaseColumn"></include>
         FROM t_emp
    </sql>

    <sql id="BaseColumn">
        empid,
        empname,
        sex,
        birthday,
        hire_date,
        salary,
        address,
        deptid
    </sql>

7、set

  • 仅用于更新,跟if配套使用
<!--  int updateWithSet(Emp emp); -->
<update id="updateWithSet" parameterType="Emp">
    update t_emp
    <set>
        <if test="empname != null and empname.trim() != ''">
            empname = #{empname},
        </if>
        <if test="salary != null and salary > 0">
           salary = #{salary},
        </if>
        <if test="sex == 0 or sex == 1">
            sex = #{sexr}
        </if>
    </set>
    where id = #{id}
</update>

十二、缓存机制【理解】

  • 作用
    • 提高查询效率
    • 减轻数据库访问压力

1、分类

  • 一级缓存
  • 二级缓存

2、区别

  • 一级缓存
    • 级别:SqlSession
    • 存储介质:内存
    • 存储类型:对象副本
    • 失效情况

  • 二级缓存
    • 级别:NameSpace
    • 存储介质:磁盘
    • 存储类型:散装数据
    • 触发条件

4、一级缓存

  • 一级缓存默认开启,我们无法关闭他

  • 在执行两次相同的查询时,第一次会向数据库发送SQL,并且写入一份到一级缓存中,那么后面的查询操作就直接从缓存中获取数据。不会向数据库发送SQL

  • 一级缓存失效的情况

    • 不是同一个SqlSession
    • 两次相同的查询中间执行增删改
    • 两次相同的查询中间手动清空缓存
    • 两次相同的查询中间手动提交事务
@Test
public void firstLevelCacheTest() throws Exception {
   EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class);
   Emp emp1 = empMapper.findById(10);
   System.out.println(emp1);


   //1.两个SqlSession
   //sqlSession = factory.openSession();
   //empMapper = sqlSession.getMapper(EmpMapper.class);

   //2.执行增删改
   //empMapper.deleteById(111111);

   //3.手动清空缓存
   //sqlSession.clearCache();

   //4.手动提交事务
   sqlSession.commit();

   Emp emp2 = empMapper.findById(10);
   System.out.println(emp2);

   System.out.println(emp1 == emp2);

}

5、二级缓存

  • 二级缓存默认开启,我们可以通过配置文件对其关闭
  • 二级缓存
    • 必须在SqlSession关闭之后,数据才会被写入到二级缓存中。
    • 存储介质是磁盘,所以写入的字节数据,那么要被写入的对象所在的类必须实现Serializable接口
  • 实现步骤
    • 全局配置文件开启
    • 在要使用二级缓存的namespace中配置一个<cache/>标签
    • 关闭SqlSession
/**
 * 二级缓存
 **/
@Test
public void secondLevelCacheTest() throws Exception {
    EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class);
    Emp emp1 = empMapper.findById(10);
    System.out.println(emp1);

    sqlSession.close();
    sqlSession = factory.openSession();
    empMapper = sqlSession.getMapper(EmpMapper.class);
    Emp emp2 = empMapper.findById(10);
    System.out.println(emp2);

    System.out.println(emp1 == emp2);

}

以后可以使用redis数据库实现缓存,分布式锁

十三、#{}和${}的区别

  • #{}

    • 在填充参数时,是通过?占位符的方式,能够避免SQL注入
    • 只是获取跟数据库表列相关的值
  • ${}

    • 在填充参数时,使用的是直接进行字符串拼接,会有SQL注入的风险

    • 使用其可以操作非数据库表列的取值

优先选择#{}取值,如果不行,则使用${}

十四、注解开发【会用】

  • 注解开发跟配置文件开发,选择一种
import com.gl.ssm.pojo.Dept;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface DeptMapper {

    @Insert("insert into t_dept (deptname) values (#{deptname})")
    @SelectKey(statement = "select last_insert_id()",
            keyProperty = "id",
            keyColumn = "deptid",
            before = false,
            resultType = int.class)
    int insert(Department dept);

    /**
     * @Results 等同于配置文件的resultMap标签
     *          id 等同于配置文件的resultMap标签中id属性
     *
     *  @Result : 映射单个属性,用boolean来区分是否是主键映射
     */
    @Results(
            id = "BaseResultMap",
            value = {
                @Result(id = true, column = "id", property = "id"),
                @Result(id = false, column = "deptname", property = "deptname")
            }
    )
    @Select("select id, deptname from t_dept")
    List<Department>selectAll();

    @ResultMap("BaseResultMap")  //引用其他已经定义好的ResultMap
    @Select("select id, dept_name from t_dept where id = #{id}")
    Department findById(Integer id);

}
  • 分步查询【IEmployeeAnnoMapper.class】
package com.gl.ssm.mapper;

import com.qf.java2107.pojo.Emp;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;

public interface EmpMapper {

    /**
     * 根据ID查询员工关联部门
     * @param id
     * @return
     */
    @Results({
            @Result(id = true, column = "id", property = "id"),
            @Result(id = false, column = "empname", property = "empname"),
            @Result(id = false, column = "sex", property = "sex"),
            @Result(id = false, column = "birthday", property = "birthday"),
            @Result(id = false, column = "hire_date", property = "hireDate"),
            @Result(id = false, column = "salary", property = "salary"),
            @Result(id = false, column = "address", property = "address"),
            @Result(id = false, column = "deptid", property = "deptid"),
            @Result(id = false, property = "dept", column = "deptid",
                    //one 一对一映射
                    one = @One(select = "com.gl.ssm.mapper.DeptMapper.findById",
                            fetchType = FetchType.LAZY))
    })
    @Select("select * from t_emp where id = #{id}")    //dept_id
    Employee findByIdUseStep(Integer id);

}

十五、源码分析【听一遍】

1、查询

  • 查询单个
    • DefaultSqlSession的selectOne方法
      • DefaultSqlSession的selectList方法,得到结果后,进行集合数量判断,如果是1,直接返回。>1,否则就报错。0返回null
        • CachingExecutor的query方法
          • BaseExecutor的query方法
            • BaseExecutor的queryFromDatabase方法
              • BaseExecutor的doQuery方法
                • 通过SimpleExecutor中去调用JDBC的execute()
  • 查询集合
    • DefaultSqlSession的selectList方法,得到结果后,直接返回
      • CachingExecutor的query方法
        • BaseExecutor的query方法
          • BaseExecutor的queryFromDatabase方法
            • BaseExecutor的doQuery方法
              • 通过SimpleExecutor中去调用JDBC的execute()

2、增删改方法

  • 修改
    • DefaultSqlSession的update方法
      • CachingExecutor的update方法:会清空缓存操作
        • BaseExecutor的doUpdate方法
          • 通过SimpleExecutor中去调用JDBC的execute()
  • 增加、删除
    • 先调用DefaultSqlSession的insert方法或者delete方法
      • 接下来就会执行DefaultSqlSession的update方法

十六、面试题

1.Mybatis应用到的设计模式

  • 构建者模式 SqlSessionFactoryBuilder().build()
  • 工厂模式 sqlSessionFactory.openSession();
  • 代理模式 getMapper

2.Mybatis的Mapper接口是否支持方法重载

  • 不支持
    • mapper接口的方法名就是mapper映射文件的statementId,是用来获取执行SQL的唯一标识

3.Mybatis的Mapper映射文件的标签

  • 除之前讲的之外
    • sql:抽取的SQL片段
    • include:引用抽取的SQL片段
    <!-- List<Employee> findByIds(List<Integer> ids); -->
    <select id="findByIds" parameterType="list" resultType="com.gl.ssm.pojo.Emp">
        <include refid="BaseSelect"></include>
        <where>
            <if test="list != null and list.size() > 0">
                <foreach collection="list" item="id" separator="," open="id IN (" close=")">
                    #{id}
                </foreach>
            </if>
        </where>
    </select>
    <sql id="BaseSelect">
        SELECT
         <include refid="BaseColumn"></include>
         FROM t_emp
    </sql>

    <sql id="BaseColumn">
        empid,
        empname,
        sex,
        birthday,
        hire_date,
        salary,
        address,
        deptid
    </sql>
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雨霖先森

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值