Mybatis

目录

MyBatis快速入门

Mapper 代理开发

MyBatis核心配置文件

配置文件完成增删改查

环境准备

查询所有数据

查询详情

多条件查询

多条件动态查询

单个条件(动态SQL)

添加数据

修改

删除一行数据 

Mybatis参数传递 

注解完成增删改查


Mybatis概念
MyBatis 是一款优秀的持久层框架,用于简化 JDBC 开发
MyBatis 本是 Apache 的一个开源项目iBatis,2010年这个项目由apache software foundation
迁移到了googlecode,并且改名为MyBatis 。2013年11月迁移到Github
官网:https://mybatis.org/mybatis-3/zh/index.html
持久层:
负责将数据到保存到数据库的那一层代码。将操作数据库的Java代码作为持久层。
而Mybatis就是对jdbc代码进行了封装。
JavaEE三层架构:表现层、业务层、持久层三层架构
框架:
框架就是一个半成品软件,是一套可重用的、通用的、软件基础代码模型
在框架的基础之上构建软件编写更加高效、规范、通用、可扩展

JDBC 缺点

硬编码
    注册驱动、获取连接上
    图标1的代码有很多字符串,而这些是连接数据库的四个基本信息,
    以后如果要将Mysql数据库换成其他的关系型数据库的话,这四个地方都需要修改,如果放在此处就意味着要修改我们的源代码。
SQL语句
    上图标2的代码。如果表结构发生变化,SQL语句就要进行更改。这也不方便后期的维护。
操作繁琐
    手动设置参数
    手动封装结果集上图标4的代码是对查询到的数据进行封装,
    而这部分代码是没有什么技术含量,而且特别耗费时间的。

Mybatis 优化

硬编码可以配置到配置文件
操作繁琐的地方mybatis都自动完成

下图是持久层框架的使用占比。

MyBatis快速入门

需求:查询user表中所有的数据
创建user表,添加数据
create database mybatis; 
use mybatis;

drop table if exists tb_user;

create table tb_user(
id int primary key auto_increment, 
username varchar(20),
password varchar(20), 
gender char(1),
addr varchar(30));

INSERT INTO tb_user VALUES (1, 'zhangsan', '123', '男', '北京'); 
INSERT INTO tb_user VALUES (2, '李四', '234', '女', '天津'); 
INSERT INTO tb_user VALUES (3, '王五', '11', '男', '西安');
创建模块,导入坐标
在创建好的模块中的 pom.xml 配置文件中添加依赖的坐标
注意:需要在项目的 resources 目录下创建logback的配置文件
<dependencies>
    <!--mybatis 依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.5</version>
    </dependency>

    <!--mysql 驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.46</version>
    </dependency>

    <!--junit 单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
        <scope>test</scope>
    </dependency>

    <!-- 添加slf4j日志api -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.20</version>
    </dependency>

    <!-- 添加logback-classic依赖 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>

    <!-- 添加logback-core依赖 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-core</artifactId>
        <version>1.2.3</version>
    </dependency>

</dependencies>
编写 MyBatis 核心配置文件 -- > 替换连接信息 解决硬编码问题
在模块下的 resources 目录下创建mybatis的配置文件 mybatis-config.xml ,内容如下:
<!--mybatis-config.xml-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://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:///mybatis?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="1234"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!--加载SQL的映射文件-->
        <mapper resource="UserMapper.xml"/>
    </mappers>
</configuration>
编写 SQL 映射文件 --> 统一管理sql语句,解决硬编码问题
在模块的 resources 目录下创建映射配置文件 UserMapper.xml,内容如下:
<!-- UserMapper.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--
    namespace:名称空间
    id:SQL的唯一标识
    resultType:返回的类型
-->
<mapper namespace="test">
    <select id="selectAll" resultType="com.green.pojo.User">
        select * from tb_user;
    </select>
</mapper>
编码
    在pojo 包下创建 User类
public class User { 
    private int id;
    private String username;
    private String password; 
    private String gender; 
    private String addr;
    //省略了 setter 和 getter
}
//编写 MybatisDemo 测试类
package com.green;

import com.green.pojo.User;
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 java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * Mybatis 快速入门
 */
public class MyBatisDemo {
    public static void main(String[] args) throws IOException {
        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3、执行sql
        List<User> users = sqlSession.selectList("test.selectAll");//名称空间.Mapper标识
        System.out.println(users);

        //释放资源
        sqlSession.close();
    }
}

解决SQL映射文件的警告提示:

在入门案例映射配置文件中存在报红的情况。问题如下:

 

产生的原因:Idea和数据库没有建立连接,不识别表信息。但它并不影响程序的执行。
解决方式:在Idea中配置MySQL数据库连接。

Mapper 代理开发

Mapper代理开发概述
之前写的代码是基本使用方式,它也存在硬编码的问题,如下:

这里调用 selectList() 方法传递的参数是映射配置文件中的 namespace.id值。
这样写也不便于后期的维护。如果使用Mapper 代理方式(如下图)则不存在硬编码问题。

 

通过上面的描述可以看出 Mapper 代理方式的目的:
解决原生方式中的硬编码
简化后期执行SQL

 使用Mapper代理要求

定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下。如下图

设置SQL映射文件的namespace属性为Mapper接口全限定名

 

在 Mapper 接口中定义方法,方法名就是SQL映射文件中sql语句的id,
并保持参数类型和返回值类型一致

 

 案例代码实现

//在 com.green.mapper 包下创建 UserMapper接口,代码如下:

package com.green.mapper;

import com.green.pojo.User;

import java.util.List;

public interface UserMapper {
    List<User> selectAll();
}
//在 resources 下创建 com/green/mapper 目录,并在该目录下创建 UserMapper.xml 映射配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--
    namespace:名称空间 必须是对应接口的全限定名
    id:SQL的唯一标识
    resultType:返回的类型
-->
<mapper namespace="com.green.mapper.UserMapper">
    <select id="selectAll" resultType="com.green.pojo.User">
        select * from tb_user;
    </select>
</mapper>
//在 com.green 包下创建 MybatisDemo2 测试类,代码如下:
package com.green;

import com.green.mapper.UserMapper;
import com.green.pojo.User;
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 java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * Mybatis 代理开发
 */
public class MyBatisDemo2 {
    public static void main(String[] args) throws IOException {
        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3、执行sql
        //List<User> users = sqlSession.selectList("test.selectAll");//名称空间.Mapper标识
        //3.1获取UserMapper接口的代理对象
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> users = userMapper.selectAll();

        System.out.println(users);

        //释放资源
        sqlSession.close();
    }
}
<!--
注意:
如果Mapper接口名称和SQL映射文件名称相同,并在同一目录下,
则可以使用包扫描的方式简化SQL映射文件的加载。
也就是将核心配置文件的加载映射配置文件的配置修改为
-->
<mappers>
        <!--加载SQL的映射文件-->
        <!--<mapper resource="com/green/mapper/UserMapper.xml"/>-->
        <!--mapper代理方式-->
        <package name="com.green.mapper"/>
</mappers>

MyBatis核心配置文件

 多环境配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--
    environments:配置数据库连接的环境信息。可以配置多个environment,通过default属性来切换不同的environment
    -->
    <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:///mybatis?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>

        <environment id="test">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!--数据库的连接信息-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!--加载SQL的映射文件-->
        <!--<mapper resource="com/green/mapper/UserMapper.xml"/>-->
        <!--mapper代理方式-->
        <package name="com.green.mapper"/>
    </mappers>
</configuration>

类型别名

在映射配置文件中的 resultType属性需要配置数据封装的类型(类的全限定名)。
而每次这样写是特别麻烦的,Mybatis提供了类型别名 (typeAliases) 可以简化这部分的书写。
首先需要先在核心配置文件中配置类型别名,也就意味着给pojo包下所有的类起了别名(别名就是类名),
不区分大小写。内容如下:
<typeAliases>
        <!--name属性的值是实体类所在包-->
        <package name="com.green.pojo"/>
</typeAliases>
<!--
通过上述的配置,就可以简化映射配置文件中 resultType 属性值的编写
-->
<mapper namespace="com.green.mapper.UserMapper">
    <select id="selectAll" resultType="user">
        select * from tb_user;
    </select>
</mapper>
细节:配置各个标签时,需要遵循前后顺序

 

配置文件完成增删改查

如上图所示产品原型,里面包含了品牌数据的查询、按条件查询、添加、删除、批量删除、修改等功能,
而这些功能其实就是对数据库表中的数据进行CRUD操作。
使用Mybatis完成品牌数据的增删改查操作。
查询
    查询所有数据
    查询详情
    条件查询
添加
修改
    修改全部字段
    修改动态字段
删除
    删除一个
    批量删除

环境准备

数据库表(tb_brand)及数据准备
实体类Brand
测试用例
安装MyBatisX插件
-- 删除tb_brand表
drop table if exists tb_brand;
-- 创建tb_brand表
create table tb_brand
(
-- id 主键
    id           int primary key auto_increment,
-- 品牌名称
    brand_name   varchar(20),
-- 企业名称
    company_name varchar(20),
-- 排序字段
    ordered      int,
-- 描述信息
    description  varchar(100),
--  状态:0:禁用  1:启用
    status       int
);
-- 添加数据
insert into tb_brand(brand_name, company_name, ordered, description, status)
values ('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),
       ('华为', '华为技术有限公司', 100, '华为致力于把数字世界带入每个人、每个家庭、每个组织,构建万物互联的智能世界', 1),
       ('小米', '小米科技有限公司', 50, 'are you ok', 1);

-- 查询
select *
from tb_brand
实体类 Brand
在 com.green.pojo 包下创建 Brand 实体类
package com.green.pojo;

public class Brand {
    // id 主键
    private Integer id;
    // 品牌名称
    private String brand_name;
    // 企业名称
    private String company_name;
    // 排序字段
    private Integer ordered;
    // 描述信息
    private String description;
    //  状态:0:禁用  1:启用
    private Integer status;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getBrand_name() {
        return brand_name;
    }

    public void setBrand_name(String brand_name) {
        this.brand_name = brand_name;
    }

    public String getCompany_name() {
        return company_name;
    }

    public void setCompany_name(String company_name) {
        this.company_name = company_name;
    }

    public Integer getOrdered() {
        return ordered;
    }

    public void setOrdered(Integer ordered) {
        this.ordered = ordered;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    @Override
    public String toString() {
        return "Brand{" +
                "id=" + id +
                ", brand_name='" + brand_name + '\'' +
                ", company_name='" + company_name + '\'' +
                ", ordered=" + ordered +
                ", description='" + description + '\'' +
                ", status=" + status +
                '}';
    }
}
编写测试用例
测试代码需要在 test/java 目录下创建包及测试用例。项目结构如下:

 

安装 MyBatisX 插件
    MybatisX 是一款基于 IDEA 的快速开发插件,为效率而生。
主要功能
    XML映射配置文件和接口方法相互跳转
    根据接口方法生成 statement
安装方式
    点击 file ,选择 settings ,就能看到如下图所示界面
注意:安装完毕后需要重启IDEA

 

 插件效果

红色头绳的表示映射配置文件,蓝色头绳的表示mapper接口。
在mapper接口点击红色头绳的小鸟图标会自动跳转到对应的映射配置文件,
在映射配置文件中点击蓝色头绳的小鸟图标会自动跳转到对应的mapper接口。
也可以在mapper接口中定义方法,自动生成映射配置文件中的 statement 

 

 查询所有数据

1、编写接口方法:Mapper接口 List<Brand> selectAll();
参数:无
    查询所有数据功能是不需要根据任何条件进行查询的,所以此方法不需要参数。
结果:List
    会将查询出来的每一条数据封装成一个 Brand 对象,而多条数据封装多个 Brand 对象,
    需要将这些对象封装到List集合中返回。
2、编写SQL语句:SQL映射文件:
<select id="selectAll" resultType="brand">
        select *
        from tb_brand;
</select>
3、执行方法、测试

 编写接口方法

package com.green.mapper;

import com.green.pojo.Brand;

import java.util.List;

public interface BrandMapper {
    /**
     * 查询所有
     * @return
     */
    List<Brand> selectAll();
}

编写SQL语句

在 reources 下创建 com/green/mapper 目录结构,
并在该目录下创建名为 BrandMapper.xml 的映射配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">


<!--
    namespace:名称空间
    id:SQL的唯一标识
    resultType:返回的类型
-->
<mapper namespace="com.green.mapper.BrandMapper">

    <select id="selectAll" resultType="brand">
        select *
        from tb_brand;
    </select>
</mapper>
package com.green.pojo;

public class Brand {
    // id 主键
    private Integer id;
    // 品牌名称
    private String brandName;
    // 企业名称
    private String companyName;
    // 排序字段
    private Integer ordered;
    // 描述信息
    private String description;
    //  状态:0:禁用  1:启用
    private Integer status;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getBrandName() {
        return brandName;
    }

    public void setBrandName(String brandName) {
        this.brandName = brandName;
    }

    public String getCompanyName() {
        return companyName;
    }

    public void setCompanyName(String companyName) {
        this.companyName = companyName;
    }

    public Integer getOrdered() {
        return ordered;
    }

    public void setOrdered(Integer ordered) {
        this.ordered = ordered;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    @Override
    public String toString() {
        return "Brand{" +
                "id=" + id +
                ", brandName='" + brandName + '\'' +
                ", companyName='" + companyName + '\'' +
                ", ordered=" + ordered +
                ", description='" + description + '\'' +
                ", status=" + status +
                '}';
    }
}

 编写测试方法

在 MybatisTest 类中编写测试查询所有的方法
package com.green.test;

import com.green.mapper.BrandMapper;
import com.green.pojo.Brand;
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;
import java.util.List;

public class MyBatisTest {
    @Test
    public void testSelectAll() throws IOException {
        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3获取BrandMapper接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //4、执行方法
        List<Brand> brands = brandMapper.selectAll();
        System.out.println(brands);

        //5、释放资源
        sqlSession.close();
    }
}

 

从上面结果看到了问题,有些数据封装成功了,而有些数据并没有封装成功。
为什么这样呢?这个问题可以通过两种方式进行解决:
    给字段起别名
    使用resultMap定义字段和属性的映射关系

 起别名解决上述问题

从上面结果可以看到 brandName 和 companyName 这两个属性的数据没有封装成功,
查询 实体类 和 表中的字段 发现,在实体类中属性名是 brandName 和 companyName ,
而表中的字段名为 brand_name 和 company_name 
解决方法:可以在写sql语句时给这两个字段起别名,将别名定义成和属性名一致即可。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
    namespace:名称空间
    id:SQL的唯一标识
    resultType:返回的类型
-->
<mapper namespace="com.green.mapper.BrandMapper">

    <!--
        数据库表的字段名称  和  实体类的属性名称 不一样,则不能自动封装数据
            * 起别名:对不一样的列名起别名,让别名和实体类的属性名一样
                * 缺点:每次查询都要定义一次别名
                    * sql片段
                        * 缺点:不灵活
            * resultMap:
                1. 定义<resultMap>标签
                2. 在<select>标签中,使用resultMap属性替换 resultType属性
    -->
    <resultMap id="brandResultMap" type="brand">
        <!--
            id:完成主键字段的映射
                column:表的列名
                property:实体类的属性名
            result:完成一般字段的映射
                column:表的列名
                property:实体类的属性名
        -->
        <result column="brand_name" property="brandName"/>
        <result column="company_name" property="companyName"/>
    </resultMap>

    <select id="selectAll" resultMap="brandResultMap">
        select *
        from tb_brand;
    </select>

    <!--
        id:唯一标识
        type:映射的类型,支持别名
    -->

    <!--<select id="selectAll" resultType="brand">
        select *
        from tb_brand;
    </select>-->

    <!--起别名:将别名定义成和属性名一致-->
    <!--<select id="selectAll" resultType="brand">
        select id, brand_name as brandName, company_name as companyName, ordered, description,
        status from tb_brand;
    </select>-->

    <!--
        sql片段
    -->
    <!--<sql id="brand_column">
         id, brand_name as brandName, company_name as companyName, ordered, description, status
     </sql>

     <select id="selectAll" resultType="brand">
         select
             <include refid="brand_column" />
         from tb_brand;
     </select>-->

</mapper>

 查询详情

查看详情功能实现步骤:
1、编写接口方法:Mapper接口
    参数:id
        查看详情就是查询某一行数据,所以需要根据id进行查询。而id以后是由页面传递过来。
    结果:Brand
        根据id查询出来的数据只要一条,而将一条数据封装成一个Brand对象即可
2、编写SQL语句:SQL映射文件
3、执行方法、进行测试

编写接口方法

//在 BrandMapper 接口中定义根据id查询数据的方法
/**
     * 查询详情:根据id查询商品信息
     * @param id
     * @return
     */
    Brand selectById(int id);

编写SQL

//在 BrandMapper.xml 映射配置文件中编写statement,使用resultMap而不是使用resultType
<!--
        * 参数占位符:
            1. #{}:会将其替换为 ?,为了防止SQL注入
            2. ${}:拼sql。会存在SQL注入问题
            3. 使用时机:
                * 参数传递的时候:#{}
                * 表名或者列名不固定的情况下:${} 会存在SQL注入问题

         * 参数类型:parameterType:可以省略
         * 特殊字符处理:
            1. 转义字符:
            2. CDATA区:
    -->
<select id="selectById" resultMap="brandResultMap">
        select * from tb_brand where id = #{id};
</select>

编写测试方法

//MybatisTest类中 定义测试方法
@Test
    public void testSelectById() throws IOException {
        //接收参数
        int id = 1;

        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3获取BrandMapper接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //4、执行方法
        Brand brand = brandMapper.selectById(id);
        System.out.println(brand);

        //5、释放资源
        sqlSession.close();
    }

 运行结果

 参数占位符

控制台显示的SQL语句,看到使用?进行占位。说明在映射配置文件中的写的 #{id}最终会被?进行占位
mybatis提供了两种参数占位符:
#{} :执行SQL时,会将 #{} 占位符替换为?,将来自动设置参数值。
从上述例子可以看出使用#{} 底层使用的是PreparedStatement

${} :拼接SQL。底层使用的是 Statement ,会存在SQL注入问题。
如下图将映射配置文件中的 #{}替换成${}来看效果
<select id="selectById" resultMap="brandResultMap">
        select * from tb_brand where id = ${id};
</select>

 重新运行看结果

parameterType使用
对于有参数的mapper接口方法,在映射配置文件中应该配置ParameterType来指定参数类型。
只不过该属性都可以省略。
<select id="selectById" parameterType="int" resultMap="brandResultMap"> 
select *
from tb_brand where id = ${id};
</select>

 SQL语句中特殊字段处理

以后肯定会在SQL语句中写一下特殊字符,比如某一个字段大于某个值,如下图
可以看出报错了,因为映射配置文件是xml类型的问题,而 > < 等这些字符在xml中有特殊含义,
所以此时需要将这些符号进行转义,可以使用以下两种方式进行转义
1. 转义字符:
2. CDATA区:

 

 多条件查询

经常会遇到如上图所示的多条件查询,将多条件查询的结果展示在下方的数据列表中。
而做这个功能需要分析最终的SQL语句应该是什么样,思考两个问题
1、条件表达式
2、如何连接
条件字段 企业名称 和 品牌名称 需要进行模糊查询,所以条件应该是:

 

查看详情功能实现步骤:
1、编写接口方法:Mapper接口
    参数:所有查询条件
    结果:List<Brand>
2、编写SQL语句:SQL映射文件
3、执行方法、进行测试

 编写接口方法

在 BrandMapper 接口中定义多条件查询的方法。
而该功能有三个参数,需要考虑定义接口时,参数应该如何定义。
定义参数的方式有三种:
1、散装参数
Mybatis针对多参数有多种实现使用@Param("参数名称") 标记每一个参数,
在映射配置文件中就需要使用 #{参数名称} 进行占位
2、对象参数
将多个参数封装成一个 实体对象 ,将该实体对象作为接口的方法参数。
该方式要求在映射配置文件的SQL中使用 #{内容} 时,里面的内容必须和实体类属性名保持一致。
3、Map集合参数
将多个参数封装到map集合中,将map集合作为接口的方法参数。
该方式要求在映射配置文件的SQL中使用 #{内容}时,里面的内容必须和map集合中键的名称一致。
/**
     * 条件查询
     * *参数接收 (三种实现方式)
     * 1、散装参数:如果方法中有多个参数,需要使用@Param(“SQL参数占位符名称”)
     * 2、对象参数:对象的参数名称要和参数占位符一致
     * 3、map集合参数
     */

    //1、散装参数:如果方法中有多个参数,需要使用@Param(“SQL参数占位符名称”)
    //List<Brand> selectByCondition(@Param("status") int status, @Param("companyName") String companyName, @Param("brandName") String brandName);

    /*将多个参数封装成一个 实体对象 ,将该实体对象作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容} 时,
    里面的内容必须和实体类属性名保持一致。*/
    //2、对象参数:对象的参数名称要和参数占位符一致
    //List<Brand> selectByCondition(Brand brand);

    /*将多个参数封装到map集合中,将map集合作为接口的方法参数。该方式要求在映射配置文件的SQL中使用 #{内容}时,
    里面的内容必须和map集合中键的名称一致。*/

    //3、map集合参数
    List<Brand> selectByCondition(Map map);

 编写SQL语句

在BrandMapper.xml映射配置文件中编写statement ,使用resultMap而不是使用resultType
 <!--条件查询-->
    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        where status = #{status}
          and company_name like #{companyName}
          and brand_name like #{brandName}
    </select>

 编写测试方法

 /**
     * 条件查询
     *
     * @throws IOException
     */
    @Test
    public void testSelectByCondition() throws IOException {
        //接收参数
        int status = 1;
        String companyName = "华为";
        String brandName = "华为";

        //处理参数 ,模糊匹配
        companyName = "%" + companyName + "%";
        brandName = "%" + brandName + "%";

        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3获取BrandMapper接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //4、执行方法
        //方法一:散装参数方法
        //List<Brand> brands = brandMapper.selectByCondition(status, companyName, brandName);

        /*方法二:对象参数方法
        //封装对象
        Brand brand = new Brand();
        brand.setStatus(status);
        brand.setCompanyName(companyName);
        brand.setBrandName(brandName);
        List<Brand> brands = brandMapper.selectByCondition(brand);
        System.out.println(brands);*/

        //方法三:Map集合参数
        Map map = new HashMap();
        map.put("status", status);
        map.put("companyName", companyName);
        map.put("brandName", brandName);

        List<Brand> brands = brandMapper.selectByCondition(map);
        System.out.println(brands);

        //5、释放资源
        sqlSession.close();
    }

多条件动态查询

用户在输入条件时,可能不会所有的条件都填写,这个时候的SQL语句就不能那样写
例如用户只输入 当前状态 时,SQL语句就是
select * from tb_brand where status = #{status}
而用户如果只输入企业名称时,SQL语句就是
select * from tb_brand where company_name like #{companName}
而用户如果输入了 当前状态 和 企业名称 时,SQL语句又不一样
select * from tb_brand where status = #{status} and company_name like #{companName}
针对上述的需要,Mybatis对动态SQL有很强大的支撑:
if
choose (when, otherwise) 
trim (where, set)
foreach
if 标签:条件判断
    test 属性:逻辑表达式
<!--动态SQL查询-->
    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        where
        <if test="status != null">
            status = #{status}
        </if>

        <if test="companyName != null and companyName != ''">
            and company_name like #{companyName}
        </if>

        <if test="brandName != null and brandName != ''">
            and brand_name like #{brandName}
        </if>
    </select>
如上的这种SQL语句就会根据传递的参数值进行动态的拼接。
如果此时status和companyName有值那么就会值拼接这两个条件。
执行结果如下:

 

但是它也存在问题,如果此时给的参数值是
        Map map = new HashMap();
        //map.put("status", status);
        map.put("companyName", companyName);
        map.put("brandName", brandName);
拼接的SQL语句就变成了
select * from tb_brand where and company_name like ? and brand_name like ?
上面的语句中 where 关键字直接跟 and 关键字,这就是一条错误的SQL语句。

 

有两种解决方式
1、恒等式 1 = 1
    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        where 1 = 1
        <if test="status != null">
            and status = #{status}
        </if>

        <if test="companyName != null and companyName != ''">
            and company_name like #{companyName}
        </if>

        <if test="brandName != null and brandName != ''">
            and brand_name like #{brandName}
        </if>
    </select>

2、可以使用 where 标签解决
where 标签
作用:
    替换where关键字
    会动态的去掉第一个条件前的 and
    如果所有的参数没有值则不加where关键字

    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        /* where 1 = 1*/
        <where>
            <if test="status != null">
                and status = #{status}
            </if>

            <if test="companyName != null and companyName != ''">
                and company_name like #{companyName}
            </if>

            <if test="brandName != null and brandName != ''">
                and brand_name like #{brandName}
            </if>
        </where>
    </select>
注意:需要给每个条件前都加上 and 关键字

 单个条件(动态SQL

如上图所示,在查询时只能选择 品牌名称 、当前状态 、企业名称 这三个条件中的一个,
但是用户到底选择哪儿一个,并不能确定。这种就属于单个条件的动态SQL语句。
这种需求需要使用到 choose(when,otherwise)标签 实现, 
而 choose 标签类似于Java 中的switch语句
通过一个案例来使用这些标签

 编写接口方法

List<Brand> selectByConditionSingle(Brand brand);

编写SQL语句

    <select id="selectByConditionSingle" resultMap="brandResultMap">
        select *
        from tb_brand
        <where>
            <choose><!--相当于switch-->
                <when test="status != null"><!--相当于case-->
                    status = #{status}
                </when>
                <when test="companyName != null and companyName != '' "><!--相当于case-->
                    company_name like #{companyName}
                </when>
                <when test="brandName != null and brandName != ''"><!--相当于case-->
                    brand_name like #{brandName}
                </when>
            </choose>
        </where>
    </select>

 编写测试方法

 @Test
    public void testSelectByConditionSingle() throws IOException {
        //接收参数
        int status = 1;
        String companyName = "华为";
        String brandName = "华为";

        //处理参数 ,模糊匹配
        companyName = "%" + companyName + "%";
        brandName = "%" + brandName + "%";

        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3获取BrandMapper接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
        
        //方法三:Map集合参数
        Map map = new HashMap();
        map.put("status", status);
        //map.put("companyName", companyName);
        //map.put("brandName", brandName);

        List<Brand> brands = brandMapper.selectByConditionSingle(map);
        System.out.println(brands);

        //5、释放资源
        sqlSession.close();
    }

添加数据

添加 实现步骤:
1、编写接口方法:Mapper接口
    参数:除了id之外的所有数据
    结果:void
2、编写SQL语句:SQL映射文件
3、执行方法、进行测试
MyBatis事务:
openSession():默认开启事务,进行增删改操作后需要使用sqlSession.commit();手动提交事务
openSession(true):可以设置为自动提交事务(关闭事务)

 编写接口方法

    /**
     * 添加
     * @param brand
     */
    void add(Brand brand);

 编写SQL语句

<insert id="add">
    insert into tb_brand(brand_name, company_name, ordered, description, status)
    values (#{brandName},#{companyName},#{ordered},#{description},#{status});
</insert>

编写测试方法

 @Test
    public void testAdd() throws IOException {
        //接收参数
        int status = 1;
        String companyName = "波导手机";
        String brandName = "波导";
        String description = "手机中的战斗机";
        int ordered = 100;

        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //SqlSession sqlSession = sqlSessionFactory.openSession(true);//自动提交事务

        //3获取BrandMapper接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //方法二:对象参数方法
        //封装对象
        Brand brand = new Brand();
        brand.setStatus(status);
        brand.setCompanyName(companyName);
        brand.setBrandName(brandName);
        brand.setDescription(description);
        brand.setOrdered(ordered);

        //4、执行方法
        brandMapper.add(brand);

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

        //5、释放资源
        sqlSession.close();
    }

 添加-主键返回

在数据添加成功后,有时候需要获取插入数据库数据的主键(主键是自增长)。
比如:添加订单和订单项,如下图就是京东上的订单

在 insert 标签上添加如下属性:
useGeneratedKeys:是够获取自动增长的主键值。true表示获取
keyProperty :指定将获取到的主键值封装到哪儿个属性里
<insert id="add" useGeneratedKeys="true" keyProperty="id">
    insert into tb_brand(brand_name, company_name, ordered, description, status)
    values (#{brandName},#{companyName},#{ordered},#{description},#{status});
</insert>

 

 修改

如图所示是修改页面,用户在该页面书写需要修改的数据,点击 提交 按钮,
就会将数据库中对应的数据进行修改。注意一点,
如果哪儿个输入框没有输入内容,就保留之前的数据。
修改 实现步骤:
1、编写接口方法:Mapper接口
    参数:所有数据
    结果:void
2、编写SQL语句:SQL映射文件
3、执行方法、进行测试

 编写接口方法

void update(Brand brand);

编写SQL语句

<!--修改全部字段-->
    <update id="update">
        update tb_brand
        set brand_name   = #{brandName},
            company_name = #{companyName},
            ordered      = #{ordered},
            description  = #{description},
            status       = #{status}
        where id = #{id};
    </update>

 编写测试方法

@Test
    public void testUpdate() throws IOException {
        //接收参数
        int status = 1;
        String companyName = "波导手机";
        String brandName = "波导";
        String description = "波导手机,手机中的战斗机";
        int ordered = 200;
        int id = 5;

        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        //SqlSession sqlSession = sqlSessionFactory.openSession();
        SqlSession sqlSession = sqlSessionFactory.openSession(true);//自动提交事务

        //3获取BrandMapper接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //方法二:对象参数方法
        //封装对象
        Brand brand = new Brand();
        brand.setStatus(status);
        brand.setCompanyName(companyName);
        brand.setBrandName(brandName);
        brand.setDescription(description);
        brand.setOrdered(ordered);
        brand.setId(id);

        //4、执行方法
        brandMapper.update(brand);

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

        //5、释放资源
        sqlSession.close();
    }

修改动态字段 

set 标签可以用于动态包含需要更新的列,忽略其它不更新的列。

编写SQL语句

<update id="update">
        update tb_brand
        <set>
            <if test="brandName !=null and brandName != ''">
                set brand_name = #{brandName},
            </if>
            <if test="companyName !=null and companyName != ''">
                company_name = #{companyName},
            </if>
            <if test="ordered !=null">
                ordered = #{ordered},
            </if>
            <if test="description !=null and description != ''">
                description = #{description},
            </if>
            <if test="status !=null">
                status = #{status}
            </if>
        </set>
        where id = #{id};
    </update>

删除一行数据 

删除 实现步骤:
1、编写接口方法:Mapper接口
    参数:id
    结果:void
2、编写SQL语句:SQL映射文件
3、执行方法、进行测试

编写接口方法

/**
     * 根据id删除数据
     * @param id
     */
    void deleteById(int id);

编写SQL语句

<!--删除一行数据-->
    <delete id="deleteById">
        delete
        from tb_brand
        where id = #{id};
    </delete>

 编写测试方法

@Test
    public void testDeleteById() throws IOException {
        //接收参数
        int id = 6;

        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        //SqlSession sqlSession = sqlSessionFactory.openSession();
        SqlSession sqlSession = sqlSessionFactory.openSession(true);//自动提交事务

        //3获取BrandMapper接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //4、执行方法
        brandMapper.deleteById(id);

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

        //5、释放资源
        sqlSession.close();
    }

批量删除

如上图所示,用户可以选择多条数据,然后点击上面的 删除 按钮,就会删除数据库中对应的多行数据。
删除 实现步骤:
1、编写接口方法:Mapper接口
    参数:id数组
    结果:void
2、编写SQL语句:SQL映射文件
3、执行方法、进行测试

 编写接口方法

/**
     * 批量删除
     * @param ids
     */
    //void deleteByIds(@Param("ids") int[] ids);
    void deleteByIds(int[] ids);

编写SQL语句

在 BrandMapper.xml 映射配置文件中编写删除多条数据的 statement 。
编写SQL时需要遍历数组来拼接SQL语句。Mybatis 提供了 foreach 标签供我们使用
foreach 标签
用来迭代任何可迭代的对象(如数组,集合)。
collection 属性:
    mybatis会将数组参数,封装为一个Map集合。
    默认:array = 数组
    使用@Param注解改变map集合的默认key的名称
item 属性:本次迭代获取到的元素。
separator 属性:集合项迭代之间的分隔符。foreach 标签不会错误地添加多余的分隔符。
也就是最后一次迭代不会加分隔符。
open 属性:该属性值是在拼接SQL语句之前拼接的语句,只会拼接一次
close 属性:该属性值是在拼接SQL语句拼接后拼接的语句,只会拼接一次
 <!--批量删除数据-->
    <!--
        mybatis会将数组参数,封装为一个Map集合。
            * 默认:array = 数组
            * 使用@Param注解改变map集合的默认key的名称
            separator:分隔符
    -->
    <delete id="deleteByIds">
        delete
        from tb_brand
        where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
        ;
    </delete>
假如数组中的id数据是{1,2,3},那么拼接后的sql语句就是:
delete from tb_brand where id in (1,2,3);

 编写测试方法

@Test
    public void testDeleteByIds() throws IOException {
        //接收参数
        int[] ids = {4,5};

        //1、加载mybatis的核心配置文件,获取SQLSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2、获取SQLSession对象,用它来执行sql
        //SqlSession sqlSession = sqlSessionFactory.openSession();
        SqlSession sqlSession = sqlSessionFactory.openSession(true);//自动提交事务

        //3获取BrandMapper接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //4、执行方法
        brandMapper.deleteByIds(ids);

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

        //5、释放资源
        sqlSession.close();
    }

Mybatis参数传递 

Mybatis 接口方法中可以接收各种各样的参数,如下:
    多个参数
    单个参数:单个参数又可以是如下类型
        POJO 类型
        Map 集合类型
        Collection 集合类型
        List 集合类型
        Array 类型
        其他类型

多个参数 

如下面的代码,就是接收两个参数,而接收多个参数需要使用 @Param 注解,
那么为什么要加该注解呢?
这个问题要弄明白就必须来研究Mybatis 底层对于这些参数是如何处理的。
User select(@Param("username") String username,@Param("password") String password);
MyBatis提供了ParamNameResolver 类来进行参数封装
//接口方法
User select(@Param("username") String username,@Param("password") String password);
<select id="select" resultType="user"> 
    select *
    from tb_user 
    where
        username=#{username}
        and password=#{password}
</select>
接口方法中定义多个参数,Mybatis 会将这些参数封装成 Map 集合对象,值就是参数值,
而键在没有使用 @Param注解时有以下命名规则:
以 arg 开头 :第一个参数就叫 arg0,第二个参数就叫 arg1,以此类推。如:
map.put("arg0",参数值1); 
map.put("arg1",参数值2);
以 param 开头 : 第一个参数就叫 param1,第二个参数就叫 param2,依次类推。如:
map.put("param1",参数值1); 
map.put("param2",参数值2);

 代码验证:

在 UserMapper 接口中定义如下方法
User select(String username,String password);
在 UserMapper.xml 映射配置文件中定义SQL
<select id="select" resultType="user"> 
    select *
    from tb_user 
    where
        username=#{arg0}
        and password=#{arg1}
</select>

//或者
<select id="select" resultType="user"> 
select *
    from tb_user 
    where
        username=#{param1}
        and password=#{param2}
</select>

行代码结果如下

在映射配合文件的SQL语句中使用用 arg 开头的和 param 书写,
代码的可读性会变的特别差,此时可以使用 @Param注解。
在接口方法参数上使用 @Param 注解,Mybatis 会将arg开头的键名替换为对应注解的属性值。
/*代码验证:
在 UserMapper 接口中定义如下方法,在 username 参数前加上 @Param 注解
*/
User select(@Param("username") String username, String password);

//Mybatis 在封装 Map 集合时,键名就会变成如下:

map.put("username",参数值1); 
map.put("arg1",参数值2); 
map.put("param1",参数值1); 
map.put("param2",参数值2);
在 UserMapper.xml 映射配置文件中定义SQL
<select id="select" resultType="user"> 
select *
    from tb_user 
    where
        username=#{username} 
        and password=#{param2}
</select>
运行程序结果没有报错。而如果将 #{} 中的 username 还是写成 arg0
<select id="select" resultType="user"> 
select *
    from tb_user 
    where
        username=#{arg0}
        and password=#{param2}
</select>
运行程序则可以看到错误

 

结论:以后接口参数是多个时,在每个参数上都使用 @Param 注解。这样代码的可读性更高。

单个参数 

POJO 类型
    直接使用。要求 属性名 和 参数占位符名称 一致
Map 集合类型
    直接使用。要求 map集合的键名 和 参数占位符名称 一致
Collection 集合类型
    Mybatis 会将集合封装到 map 集合中,如下:
        map.put("arg0",collection集合); 
        map.put("collection",collection集合;
可以使用 @Param 注解替换map集合中默认的 arg 键名。
List 集合类型
    Mybatis 会将集合封装到 map 集合中,如下:
        map.put("arg0",list集合); 
        map.put("collection",list集合); 
        map.put("list",list集合);
可以使用 @Param 注解替换map集合中默认的 arg 键名。
Array 类型
    Mybatis 会将集合封装到 map 集合中,如下
        map.put("arg0",数组); 
        map.put("array",数组);
可以使用 @Param 注解替换map集合中默认的 arg 键名。
其他类型
    比如int类型, 参数占位符名称 叫什么都可以

注解完成增删改查

使用注解开发会比配置文件开发更加方便。如下就是使用注解进行开发
@Select(value = "select * from tb_user where id = #{id}") 
public User select(int id);
注意:
注解是用来替换映射配置文件方式配置的,所以使用了注解,
就不需要再映射配置文件中书写对应的 statement
Mybatis 针对 CURD 操作都提供了对应的注解,已经做到见名知意。如下:
查询 :@Select
添加 :@Insert
修改 :@Update
删除 :@Delete
代码实现:
将之前案例中 UserMapper.xml 中的 根据id查询数据 的 statement 注释掉

 

在 UserMapper 接口的 selectById 方法上添加注解

 

运行测试程序也能正常查询到数据
官方文档入门的一段话:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,
Java 注解不仅力不从心,还会让本就复杂的 SQL 语句更加混乱不堪。 
因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。

选择何种方式来配置映射,以及是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 
换句话说,永远不要拘泥于一种方式,你可以很轻松地在基于注解和XML的语句映射方式间自由移植和切换。
注解完成简单功能,配置文件完成复杂功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值