Mybatis学习笔记

Mybatis笔记

快速入门

1.创建数据表

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) NOT NULL auto_increment,
  `username` varchar(32) NOT NULL COMMENT '用户名称',
  `birthday` datetime default NULL COMMENT '生日',
  `sex` char(1) default NULL COMMENT '性别',
  `address` varchar(256) default NULL COMMENT '地址',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


insert  into `user`(`id`,`username`,`birthday`,`sex`,`address`) values (41,'老王','2018-02-27 17:47:08','男','北京'),(42,'小二王','2018-03-02 15:09:37','女','北京金燕龙'),(43,'小二王','2018-03-04 11:34:34','女','北京金燕龙'),(45,'传智播客','2018-03-04 12:04:06','男','北京金燕龙'),(46,'老王','2018-03-07 17:37:26','男','北京'),(48,'小马宝莉','2018-03-08 11:44:00','女','北京修正');


2.创建实体类

在com.itcast.domain下创建,成员变量的值与数据表字段名一致

package com.itcast.domain;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", birthday=" + birthday +
                ", sex='" + sex + '\'' +
                ", address='" + address + '\'' +
                '}';
    }

    public Integer getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public User() {
    }
}

3.创建Mapper接口(Dao)

在com.itcast.dao下创建

package com.itcast.dao;

import com.itcast.domain.User;

import java.util.List;

/**
 * 用户的持久层接口
 */
public interface IUserDao {
    /**
     *查询所有用户
     *@return
     */
    List<User> findAll();
}

4.maven中导入包

在pom.xml里加上

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

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

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

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

5.创建Mapper.xml

在resources文件夹下创建,但建议Mapper.xml在该文件夹下的目录结构和Mapper接口的包结构一致。

这里的Mapper接口在com.itcast.dao下,所以Mapper.xml放在…/resources/com/itcast/dao/ 文件夹下。

<?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.itcast.dao.IUserDao">
    <!-- namespace放接口的全类名 -->
    <select id="findAll" resultType="com.itcast.domain.User">
        <!-- id是接口的方法名,resultType是创建的实体类的全类名,下面放SQL语句-->
        select * from user
    </select>
</mapper>

6.创建SqlMapConfig.xml

在recourse文件夹下创建SqlMapConfig.xml,文件也可以取其他名字。

<?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>
    
    <!--
	加上这个标签之后,在Mapper.xml里写resultType属性就可以直接写类名称了,而不用写com.xxx.xxx这一大串。
    <typeAliases>
        <package name="实体类所在的包名"></package>
    </typeAliases>
	-->
    
    <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <!--com.mysql.jdbc.Driver过期了 -->
                <property name="url" value="jdbc:mysql://localhost:3306/eesy?serverTimezone=UTC"/>
                <!-- 实验证明URL后面不加“?serverTimezone=UTC”会报异常 -->
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/itcast/dao/IUserDao.xml" />
        <!-- recourses放Mapper.xml的位置 -->
        <!--
		或者用<package name="Mapper接口所在的包名"/>
		这样子做,如果想新建一个Mapper接口,再新建一个Mapper.xml,就不需要再写多一个mapper标签
		但,前提是,Mapper接口的包结构需要和Mapper.xml在resources文件夹下的目录结构一致
		-->
    </mappers>
</configuration>

补充:property也可以实现动态替换:(摘自w3school: https://www.w3cschool.cn/mybatis/7zy61ilv.html )

这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来传递。例如:

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

其中的属性就可以在整个配置文件中使用来替换需要动态配置的属性值。比如:

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

这个例子中的 username 和 password 将会由 properties 元素中设置的相应值来替换。 driver 和 url 属性将会由 config.properties 文件中对应的值来替换。这样就为配置提供了诸多灵活选择。

属性也可以被传递到 SqlSessionBuilder.build()方法中。例如:

    SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, props);

    // ... or ...

    SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, environment, props);

如果属性在不只一个地方进行了配置,那么 MyBatis 将按照下面的顺序来加载:

  • 在 properties 元素体内指定的属性首先被读取。
  • 然后根据 properties 元素中的 resource 属性读取类路径下属性文件或根据 url 属性指定的路径读取属性文件,并覆盖已读取的同名属性。
  • 最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。

因此,通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的是 properties 属性中指定的属性。

7.创建log4j.properties

放在recourse文件夹下

### 设置###
log4j.rootLogger = debug,stdout,D,E

### 输出信息到控制抬 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n

### 输出DEBUG 级别以上的日志到文件 ###
log4j.appender.D = org.apache.log4j.FileAppender
log4j.appender.D.File = C://Users/Prince/Documents/debug.log
						# 目录要存在,否则无法运行
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

### 输出ERROR 级别以上的日志到文件 ###
log4j.appender.E = org.apache.log4j.FileAppender
log4j.appender.E.File = C://Users/Prince/Documents/error.log
						# 目录要存在,否则无法运行
log4j.appender.E.Append = true
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

8.创建测试类

public class MybatisTest {
    public static void main(String[] args) throws Exception {
        //1.读取配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2.创建SqlSessionFactory工厂
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(in);
        //3.使用工厂生产SqlSession对象
        SqlSession session = factory.openSession();
        //4.使用SqlSession创建Dao接口的代理对象
        IUserDao userdao = session.getMapper(IUserDao.class);
        //5.使用代理对象执行方法
        List<User> users = userdao.findAll();
        for(User user:users){
            System.out.println(user);
        }
        //6.释放资源
        session.close();
        in.close();
    }
}

输出结果:

User{id=41, username='老王', birthday=Wed Feb 28 01:47:08 CST 2018, sex='男', address='北京'}
User{id=42, username='小二王', birthday=Fri Mar 02 23:09:37 CST 2018, sex='女', address='北京金燕龙'}
User{id=43, username='小二王', birthday=Sun Mar 04 19:34:34 CST 2018, sex='女', address='北京金燕龙'}
User{id=45, username='传智播客', birthday=Sun Mar 04 20:04:06 CST 2018, sex='男', address='北京金燕龙'}
User{id=46, username='老王', birthday=Thu Mar 08 01:37:26 CST 2018, sex='男', address='北京'}
User{id=48, username='小马宝莉', birthday=Thu Mar 08 19:44:00 CST 2018, sex='女', address='北京修正'}

使用注解配置Mapper

1.删除Mapper.xml
2.在Mapper接口(Dao)里的方法加上@Select注解,value为SQL语句
3.在SqlMapConfig.xml里的mapper标签删除recourse属性,加上class属性,值为Mapper接口的全类名

增加数据

基于快速入门中的案例:

​ 1.在IUserDao接口(Mapper)添加一个方法

void insertUser(User user);

​ 2.修改IUserDao.xml

<!--在mapper标签里面添加下面标签-->
<!--id属性写方法名,parameterType属性写参数的全类名-->
<!--通过#{xxx}可以拿到传的参数对象里面的getXxx()-->
    <insert id="insertUser" parameterType="com.itcast.domain.User">
        insert into user (username,birthday,sex,address) values (#{username},#{birthday},#{sex},#{address});
    </insert>

​ 3.测试代码

    @Test
    public void test() throws Exception{
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(in);
        SqlSession session = sqlSessionFactory.openSession();
        IUserDao mapper = session.getMapper(IUserDao.class);//创建代理对象
        //------------上面是模板---------------
        //-------创建User
        User user = new User();
        user.setUsername("大王");
        user.setSex("男");
        user.setBirthday(new Date());
        user.setAddress("广东");
        mapper.insertUser(user);//使用方法
        //-------------------------
        session.commit();//提交事务[注:如果不提交事务,则会自动回滚]
        session.close();
        in.close();
    }

扩展:增加数据后返回id值,可以将xml中的代码改为如下,会把id值返回到User对象的id属性

    <insert id="insertUser" parameterType="com.itcast.domain.User">
        <!--
			keyColumn:数据库中对应的字段名
			keyProperty:User对象对应的属性
			resultType:返回值类型
			order:可选BEFORE和AFTER,代表是在插入语句执行之前执行还是之后
		-->
        <selectKey keyColumn="id" keyProperty="id" resultType="int" order="AFTER">
            select last_insert_id();
        </selectKey>
        insert into user (username,birthday,sex,address) values (#{username},#{birthday},#{sex},#{address});
    </insert>

修改和删除数据

和增加数据那里类似,在IUserDao接口中添加方法

    void updateUser(User user); //更新数据
    void deleteUser(Integer userId);  //删除数据

然后在IUserDao.xml(Mapper.xml)里面的mapper标签内添加上

    <update id="updateUser" parameterType="com.itcast.domain.User">
        update user set username = #{username},birthday=#{birthday},sex=#{sex},address=#{address} where id = #{id};
    </update>
						<!-- parameterType可以写int,integer,INT,INTEGER-->
    <delete id="deleteUser" parameterType="java.lang.Integer">
        delete from user where id = #{id}; <!-- 因为只有一个参数,所以#{id}大括号里面的内容可以更换为任意值,作为占位符-->
    </delete>

测试代码

    @Test
    public void updateTest() throws Exception{
        InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        SqlSession session = sqlSessionFactory.openSession();
        IUserDao mapper = session.getMapper(IUserDao.class);
        User user = new User();
        user.setId(49);
        user.setUsername("小王");
        user.setSex("女");
        user.setBirthday(new Date());
        user.setAddress("广东");
//      mapper.updateUser(user);
        mapper.deleteUser(49);
        session.commit();
        session.close();
        inputStream.close();
    }

查询数据

在IUserDao接口中添加方法

    User selectUserById(int userId);//根据id查询一个
    List<User> selectUserByName(String name); //根据名称查询

然后在IUserDao.xml(Mapper.xml)里面的mapper标签内添加上

    <select id="selectUserById" parameterType="int" resultType="com.itcast.domain.User">
        select * from user where id = #{id};
    </select>
<!-- 因为只有一个参数,所以#{id}大括号里面的内容可以更换为任意值,作为占位符,下面的#{name}也是-->
    <select id="selectUserByName" parameterType="String" resultType="com.itcast.domain.User">
        select * from user where username like #{name};
    </select>
<!--这两个方法,一个返回值类型是User,一个是List<User>
实验证明:selectUserById的返回值可以改为List<User>,而selectUserByName的返回值不能改为User,因为这个方法不像上面那个方法,这个有可能会查出多条数据,所以必须用List集合作为返回值,如果查出来的记录只有一条,那是没问题的!
报的异常如下:
org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 2
-->

<!-- 参数只能传一个,如果要多条件查询需要传递多个参数的话,不妨把这些参数都封装成一个类,一起传递过来-->

如果有多个参数可以用param1/param2/arg0/arg1

测试代码

    @Test
    public void selectTest() throws IOException {
        //--------------------客套代码-------------------------
        SqlSession session = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("SqlMapConfig.xml")).openSession();
        IUserDao mapper = session.getMapper(IUserDao.class);
        //--------------------查询代码-------------------------
        User user = mapper.selectUserById(41);
        System.out.println(user);
        List<User> users = mapper.selectUserByName("%王%");//模糊查询,查询名字中含有“王”这个字的数据
        System.out.println(users);
        //---------------------------------------------------
        session.close();
    }

OGNL表达式

​ Object Graphic Navigation Language
​ 对象 图 导航 语言

它是通过对象的取值方法来获取数据。在写法上把get给省略了。
比如:我们获取用户的名称
	类中的写法:user.getUsername();
	OGNL表达式写法:user.username
mybatis中为什么能直接写username,而不用user.呢:
	因为在parameterType中已经提供了属性所属的类,所以此时不需要写对象名

在xml里面的#{}里面都是OGNL表达式

其实在Mapper.xml里面除了#{}之外还可以用${},里面传的内容是一样的,两者有以下区别:

#{}表示一个占位符号:
通过#{}可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换,
#{}可以有效防止sql注入。#{}可以接收简单类型值或pojo属性值。如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称。

${}表示拼接sql串:
通过${}可以将parameterType传入的内容拼接在sql中且不进行jdbc类型转换,${}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,${}括号中只能是value。

个人理解:“可以接收简单类型值或pojo属性值” 的意思应该是parameterType属性的值可以是 “int” ,也可以是 “com.xxx.xxx”。

实体类属性名和数据库列名不对应(resultMap)

在查询数据中,必须要保证查询出来的数据库列名和实体类属性名一样,才能在查询之后将对应数据封装到对象中。

如果数据库列名和实体类属性名不一样,用两种解决方式

​ 1.在写SQL语句的时候【起别名】来保证实体类属性名和数据库列名对应,如:

    <select id="findAll" resultType="com.itcast.domain.User">
        select id as userId,username as userName,address as userAddress,sex as userSex,birthday as userBirthday from user;
    </select>

这种方式只在SQL的层面上做修改,运行效率更高

​ 2.通过resultMap标签,将实体类属性名和数据库列名对应起来,如:

<!--在mapper标签里面写-->
<!--id属性随便起(下面需要用到),type属性绑定实体类的全类名-->
	<resultMap id="userMap" type="com.itcast.domain.User">
        <!-- 主键字段的对应 -->
        <id property="userId" column="id"></id>
        <!--非主键字段的对应-->
        <!-- prperties代表的是实体类中的属性,column代表的是数据库的列名-->
        <result property="userName" column="username"></result>
        <result property="userAddress" column="address"></result>
        <result property="userSex" column="sex"></result>
        <result property="userBirthday" column="birthday"></result>
    </resultMap>

然后将这个resultMap运用到需要用到它的地方,如:

<!--运用到查询需要用到的select标签里面,需要删掉resultType属性(因为用不到了),添加上resultMap属性,属性值为上面你给起的id-->
	<select id="findAll" resultMap="userMap">
        select * from user;
    </select>

这种方式运行时因为需要多解析一些东西,所有效率没有起别名的高,但是能提高开发效率。

Mybatis连接池

mybatis提供了3种方式的配置,配置方式为SqlMapConfig.xml的dataSource标签的type属性,如前面写过的:

<dataSource type="POOLED">
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <!--com.mysql.jdbc.Driver过期了 -->
    <property name="url" value="jdbc:mysql://localhost:3306/eesy?serverTimezone=UTC"/>
    <!-- 实验证明URL后面不加“?serverTimezone=UTC”会报异常 -->
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</dataSource>
<!--
type属性有3中取值:
	1.POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现
 	2.UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
	3.JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。
                            注意:如果不是web或者maven的war工程,是不能使用的。
                            我们课程中使用的是tomcat服务器,采用连接池就是dbcp连接池。
-->

Mybatis事务控制

默认是不自动提交事务的,所以在增删改之后一定要执行SqlSession里的commit()方法。

关闭事务(开启自动提交)的方法,就是在创建SqlSession对象时传一个boolean类型的参数,true值。

//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
//3.使用工厂生产SqlSession对象
SqlSession session = factory.openSession(true);//true为自动提交,不指定或false为不自动提交

动态SQL

在Mapper.xml中设定的SQL可以通过标签来进行动态

if标签

举个栗子:

//IUserDao接口多写一个方法:
    /**
     * 动态sql语句测试,条件查询
     * @param user user对象,如果姓名不为null,则根据姓名查询,什么不为null就根据什么查询
     * @return 集合
     */
    List<User> selectUserByCondition(User user);
<!--Mapper.xml中-->
<select id="selectUserByCondition" parameterType="com.itcast.domain.User" resultType="com.itcast.domain.User">
    select * from user where true   <!--执行时一定会包含的SQL语句-->
    <if test="username != null">  
        and username = #{username}  <!--执行时如果满足if标签里的test属性里的条件则再拼接上此语句-->
    </if>
    <if test="sex != null">
        and sex = #{sex}			<!--这里也是这样-->
    </if>
    <if test="birthday != null">
        and birthday = #{birthday}
    </if>
    <if test="address != null">
        and address = #{address}
    </if>
</select>
<!--个人猜想:test标签里面用的变量名和类里面的一样-->

where标签

上面的sql语句要where true后面的接上if标签的原因是防止sql语句中出现" where and "这种错误。也可以使用where标签改写。

这个“where”标签会知道如果它包含的标签中有返回值的话,它就插入一个‘where’。此外,如果标签返回的内容是以 AND 或 OR 开头的,则它会剔除掉。

<select id="selectUserByCondition" parameterType="com.itcast.domain.User" resultType="com.itcast.domain.User">
    select * from user 
    <where>
        <if test="username != null">  
            and username = #{username} 
        </if>
        <if test="sex != null">
            and sex = #{sex}	
        </if>
        <if test="birthday != null">
            and birthday = #{birthday}
        </if>
        <if test="address != null">
            and address = #{address}
        </if>
    </where>
</select>
<!--实验发现,每一个if标签的语句必须前面包含and
举个栗子:
如果根据username和sex两个条件查询,有and的情况下的sql语句是
	select * from user WHERE username = ? and sex = ?
    他会自动丢掉一个and
而if标签里面的SQL语句没有and的情况下,SQL语句为:
	select * from user WHERE username = ? sex = ?
	会报语法错误异常!
-->

foreach标签

用来解决select * from XXX where id in (1,2,3,4); 这种SqL语句问题

//IUserDao接口新加一个方法
    /**
     * 动态sql语句测试,用SQL语句的IN关键字条件查询所有要查询的ID,在Mapper.xml中通过where嵌套if再嵌套foreach标签实现
     * @param ids 要查询的ID集合
     * @return
     */
    List<User> selectUserByCondition3(List<Integer> ids);
<!--    条件查询,通过where+if+foreach标签-->
<select id="selectUserByCondition3" parameterType="List" resultType="com.itcast.domain.User">
    select * from user
    <where>
        <if test="list != null">
            <!--
			collection:要遍历的集合的名字(我这里传的是List集合,所以我填的是list,填其他的会报错,如果IUerDao里面对应的方法传的是一个类,类里面封装一个集合,则collection属性传的是类里面对应集合的属性名)。
			open:以什么语句开始。
			close:以什么语句结束。
			item:为集合每次迭代得到的元素起一个名字,在foreach标签体通过#{名字}可以获取到每一次迭代到的元素。
			index:为迭代过程中每次迭代到的元素的下标起一个名字。
			separator:分割符,表示迭代时每个元素之间以什么分隔。
			-----------------------------------
			如:select * from user where id in (41,42,46);
			◆	select * from user :外面
			◆	where :<where>标签
			◆	id in ( :开始
			◆	41,42,46 :每次迭代到的元素,每迭代一个就用“,”分隔
			◆	) :结束
			-->
            <foreach collection="list" open = "and id in (" item="uid" separator="," close=")">
                #{uid}  
            </foreach>
        </if>
    </where>
</select>

测试方法:

@Test
public void selectByConditionTest3() throws IOException {
    SqlSession session = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("SqlMapConfig.xml")).openSession(true);
    IUserDao mapper = session.getMapper(IUserDao.class);
    //-----------------------------------------------
    List<User> users = mapper.selectUserByCondition3(List.of(41,42,46));
    System.out.println(users);
}

sql和include标签

上面每一条sql语句前面都是select * from user,如果嫌重复的话可以使用sql标签,如:

<!--在mapper标签里面写上-->
<sql id="sql"> <!--id属性随便定-->
    select * from user
</sql>

<!--在SQL语句里面把“select * from user”部分替换为-->
<include refid="sql"/>  <!--refid属性关联sql标签的id属性-->

set标签

按条件更新数据,如

    /**
     * 条件更新,传入一个user对象,哪个对象不为空就更新哪个属性
     * @param user id属性必须存在
     */
    void updateByCondition(User user);
<!--    条件更新-->
    <update id="updateByCondition" parameterType="com.itcast.domain.User">
        update user set
        <if test="username != null and username != ''">
            username = #{username},
        </if>
        <if test="sex != null and sex!= '' ">
            sex = #{sex},
        </if>
        <if test="birthday != null">
            birthday = #{birthday},
        </if>
        <if test="address != null and address!= '' ">
            address = #{address}
        </if>
        where id = #{id};
    </update>

这样的代码有个问题就是,如果address为null的话,没有拼接上最后一个if标签里的SQL语句,那么就会多出一个逗号!

update user set username = ?,sex = ?,birthday = ?,where id = ?;

使用 set 标签可以将动态的配置 set关键字,和剔除追加到条件末尾的任何不相关的逗号。

    <update id="updateByCondition" parameterType="com.itcast.domain.User">
        update user
        <set>
            <if test="username != null and username != ''">
                username = #{username},
            </if>
            <if test="sex != null and sex!= '' ">
                sex = #{sex},
            </if>
            <if test="birthday != null">
                birthday = #{birthday},
            </if>
            <if test="address != null and address!= '' ">
                address = #{address}
            </if>
        </set>
        where id = #{id};
    </update>

choose标签

有时候我们并不想应用所有的条件,而只是想从多个选项中选择一个。MyBatis 提供了 choose 元素,按顺序判断 when 中的条件出否成立,如果有一个成立,则 choose 结束。当 choose 中所有 when
的条件都不满则时,则执行 otherwise 中的 sql。类似于 Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。

if 是与(and)的关系,而 choose 是或(or)的关系。

代码懒得写了,整体框架张这样:

<select id="" parameterType="" resultType="">
    SELECT * from user
    <where>
        <choose>
            <when test="条件">
                AND ...
            </when>
            <when test="条件">
                AND ...
            </when>
            <otherwise>
                AND ...
            </otherwise>
        </choose>
    </where>
</select>

trim标签

trim标记是一个格式化的标记,主要用于拼接sql的条件语句(前缀或后缀的添加或忽略),可以完成set或者是where标记的功能。trim属性主要有以下四个:

  • prefix:在trim标签内sql语句加上前缀
  • suffix:在trim标签内sql语句加上后缀
  • prefixOverrides:指定去除多余的前缀内容,如:prefixOverrides=“AND | OR”,去除trim标签内sql语句多余的前缀"and"或者"or"。
  • suffixOverrides:指定去除多余的后缀内容。

具体参见 https://blog.csdn.net/qq_39623058/article/details/88779301

多表查询

一对一association标签

用在一对一查询那里,嵌套在resultMap标签里面。假如有Account(订单)表和User(用户)表,每一个订单只关联着一个用户,在查询Account的同时将User查询出来(Account类里封装一个User类)


<resultMap id="selectAllAccountLinkedUser2Map" type="com.itcast.domain.Account">
    <id property="id" column="accid" />
    <result property="uid" column="uid" />
    <result property="money" column="money" />
    <!-- property:属性值-->
    <association property="user" javaType="com.itcast.domain.User">
        <!--里面标签注明写,怎么用,和外面一致-->
        <id column="id" property="id"></id>
        <result property="username" column="username" />
        <result property="sex" column="sex" />
        <result property="birthday" column="birthday" />
        <result property="address" column="address" />
    </association>
</resultMap>

<!--查询所有Account表的数据,使用多表查询(外连接)将所有数据查询出来-->
<select id="selectAllAccountLinkedUser2" resultMap="selectAllAccountLinkedUser2Map">
    select user.*,account.ID as accid,account.uid,account.MONEY from user right join account on user.id = account.UID;
</select>

一对多collection标签

用在一对多那里,也是嵌套在resultMap标签里面,假如有Account(订单)表和User(用户)表,一个用户可以拥有多个订单,查询User的同时将所有的订单查询出来(User类里面封装List)

<resultMap id="selectAllUserLinkedAccountMap" type="com.itcast.domain.User">
    <result property="id" column="id"></result>
    <result property="username" column="username"></result>
    <result property="sex" column="sex"></result>
    <result property="birthday" column="birthday"></result>
    <result property="address" column="address"></result>
    <!-- property:属性值
		ofType:List里面存放的数据类型 ◆注意,collection标签用ofType,associaction标签用javaType◆
	-->
    <collection property="accounts" ofType="com.itcast.domain.Account">
        <result property="id" column="accid"></result>
        <result property="uid" column="uid"></result>
        <result property="money" column="money"></result>
    </collection>
</resultMap>

<!--查询所有User表的数据,使用多表查询(外连接)将所有数据查询出来-->
<select id="selectAllUserLinkedAccount" resultMap="selectAllUserLinkedAccountMap">
    SELECT user.*,account.ID as accid,account.uID,account.MONEY FROM user left join account on user.id = account.UID;
</select>

多对多的查询方法

比如Role(角色)表和User(用户)表,还有User_Role表(中介表),一个角色被多个用户所拥有,一个用户也拥有多种角色,那么就在Role类里封装List,User类里封装List,查询的时候也就相当于两次一对多。

但是写Sql语句时需要两次外连接,以一次性将Role和User查询出来

-- 查询所有角色,两次左外连接
select * from role left join user_role on role.id=user_role.rid left join user on user_role.UID = user.id;
-- 查询所有用户,两次右外连接(其实就是将上面的SQL语句的left改为right)
select user.*,user_role.*,role.* from role right join user_role on role.id=user_role.rid right join user on user_role.UID = user.id;

延迟加载

延迟加载:在真正使用数据时才发起查询,不用的时候不查询。按需加载(懒加载)
立即加载:不管用不用,只要一调用方法,马上发起查询。

一对多,多对多:通常情况下我们都是采用延迟加载。
多对一,一对一:通常情况下我们都是采用立即加载。

延迟加载需打开全局设置里的开关,SqlMapConfig.xml的configuration标签里加上:

<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="true"/>
</settings>
<!-- 设置参数:
	lazyLoadingEnabled:延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。可选值为true和false,默认为false.
 	aggressiveLazyLoading:当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载。可选值为true和false,默认为false.
-->

不过还需要在pom.xml那里导入坐标,要不然Cannot enable lazy loading because CGLIB is not available

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.5</version>
</dependency>

设置延迟加载之后,存放在List里是不会加载的,一旦拿出来用,才会加载。

一对一延迟加载

假如有Account(账单)表和User(用户)表,一个账单关联着一个用户,在Mapper接口里准备2个接口,一个查询Account表,一个条件查询User表,在Mapper.xml里:

<!-- 根据id值查询用户 -->
<select id="selectUsersById" resultType="com.itcast.domain.User" parameterType="int">
    select * from user where id = #{uid};
</select>

<resultMap id="selectAllAccountsLinkedUserButDelayMap" type="com.itcast.domain.Account">
    <result property="id" column="id"></result>
    <result property="uid" column="uid"></result>
    <result property="money" column="money"></result>
    <!-- property:属性名,当查询出User数据时封装到这里
 		column:Account表的外键,和User表的id对应
		javaType:返回值类型
		select:调用的方法,全类名.方法名
	◆个人理解:是当需要加载时,就会调用select里指定的方法,将column指定的数据库字段里的值当做参数传递,并将返回值放入property指定的属性里。
	-->
    <association property="user" column="uid" javaType="com.itcast.domain.User" select="com.itcast.Dao.IUserDao.selectUsersById"></association>
</resultMap>

<!-- 查询所有账单 -->
<select id="selectAllAccountsLinkedUserButDelay" resultMap="selectAllAccountsLinkedUserButDelayMap">
    select * from account;
</select>

一对多延迟加载

和一对一的类似,只不过一对一的是associaction标签,一对多的是collection标签而已。注意collection用的是odType属性。

缓存

什么是缓存:在内存中的临时数据

为什么使用缓存:减少与数据库的交互次数,提高执行效率。

因为缓存会引发内存中的数据与数据库中不同步的问题,所以并不是所有的数据都适合使用缓存。

适用于缓存的数据:经常查询且不经常改变的,还有数据的正确与否对最终结果影响不大的。

不适用于缓存的数据:教程改变的数据,数据的正确与否对最终结果影响很大的。例如商品的库存、银行的汇率、股市的牌价。

一级缓存

它指的是Mybatis中SqlSession对象的缓存。
当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供一块区域中。
该区域的结构是一个Map。当我们再次查询同样的数据,mybatis会先去sqlsession中
查询是否有,有的话直接拿出来用。
当SqlSession对象消失时,mybatis的一级缓存也就消失了。

public void selectUserByIdCacheTest() throws IOException {
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    SqlSessionFactory build = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("SqlMapConfig.xml"));
    SqlSession session = build.openSession(true);
    IUserDao mapper = session.getMapper(IUserDao.class);
    User user1 = mapper.selectUserById(41);
    User user2 = mapper.selectUserById(41);
    System.out.println(user1 == user2);
}

输出结果:

[DEBUG] 2020-04-03 13:41:23,969 method:org.apache.ibatis.datasource.pooled.PooledDataSource.forceCloseAll(PooledDataSource.java:306)
PooledDataSource forcefully closed/removed all connections.
[DEBUG] 2020-04-03 13:41:23,970 method:org.apache.ibatis.datasource.pooled.PooledDataSource.forceCloseAll(PooledDataSource.java:306)
PooledDataSource forcefully closed/removed all connections.
[DEBUG] 2020-04-03 13:41:23,971 method:org.apache.ibatis.datasource.pooled.PooledDataSource.forceCloseAll(PooledDataSource.java:306)
PooledDataSource forcefully closed/removed all connections.
[DEBUG] 2020-04-03 13:41:23,971 method:org.apache.ibatis.datasource.pooled.PooledDataSource.forceCloseAll(PooledDataSource.java:306)
PooledDataSource forcefully closed/removed all connections.
[DEBUG] 2020-04-03 13:41:24,138 method:org.apache.ibatis.transaction.jdbc.JdbcTransaction.openConnection(JdbcTransaction.java:132)
Opening JDBC Connection
[DEBUG] 2020-04-03 13:41:24,467 method:org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:380)
Created connection 1237825806.
[DEBUG] 2020-04-03 13:41:24,470 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139)
==>  Preparing: select * from user where id = ?; 
[DEBUG] 2020-04-03 13:41:24,529 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139)
==> Parameters: 41(Integer)
[DEBUG] 2020-04-03 13:41:24,575 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139)
<==      Total: 1
true

当SqlSession关闭时:

public void selectUserByIdCacheTest() throws IOException {
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    SqlSessionFactory build = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("SqlMapConfig.xml"));
    SqlSession session = build.openSession(true);
    IUserDao mapper = session.getMapper(IUserDao.class);
    User user1 = mapper.selectUserById(41);
    session.close();
    session = build.openSession(true);
    mapper = session.getMapper(IUserDao.class);
    User user2 = mapper.selectUserById(41);
    System.out.println(user1 == user2);
}

输出:

[DEBUG] 2020-04-03 13:45:11,853 method:org.apache.ibatis.datasource.pooled.PooledDataSource.forceCloseAll(PooledDataSource.java:306)
PooledDataSource forcefully closed/removed all connections.
[DEBUG] 2020-04-03 13:45:11,854 method:org.apache.ibatis.datasource.pooled.PooledDataSource.forceCloseAll(PooledDataSource.java:306)
PooledDataSource forcefully closed/removed all connections.
[DEBUG] 2020-04-03 13:45:11,854 method:org.apache.ibatis.datasource.pooled.PooledDataSource.forceCloseAll(PooledDataSource.java:306)
PooledDataSource forcefully closed/removed all connections.
[DEBUG] 2020-04-03 13:45:11,854 method:org.apache.ibatis.datasource.pooled.PooledDataSource.forceCloseAll(PooledDataSource.java:306)
PooledDataSource forcefully closed/removed all connections.
[DEBUG] 2020-04-03 13:45:11,985 method:org.apache.ibatis.transaction.jdbc.JdbcTransaction.openConnection(JdbcTransaction.java:132)
Opening JDBC Connection
[DEBUG] 2020-04-03 13:45:12,340 method:org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:380)
Created connection 1237825806.
[DEBUG] 2020-04-03 13:45:12,344 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139)
==>  Preparing: select * from user where id = ?; 
[DEBUG] 2020-04-03 13:45:12,414 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139)
==> Parameters: 41(Integer)
[DEBUG] 2020-04-03 13:45:12,463 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139)
<==      Total: 1
[DEBUG] 2020-04-03 13:45:12,466 method:org.apache.ibatis.transaction.jdbc.JdbcTransaction.close(JdbcTransaction.java:88)
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@49c7b90e]
[DEBUG] 2020-04-03 13:45:12,466 method:org.apache.ibatis.datasource.pooled.PooledDataSource.pushConnection(PooledDataSource.java:334)
Returned connection 1237825806 to pool.
[DEBUG] 2020-04-03 13:45:12,468 method:org.apache.ibatis.transaction.jdbc.JdbcTransaction.openConnection(JdbcTransaction.java:132)
Opening JDBC Connection
[DEBUG] 2020-04-03 13:45:12,469 method:org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:369)
Checked out connection 1237825806 from pool.
[DEBUG] 2020-04-03 13:45:12,469 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139)
==>  Preparing: select * from user where id = ?; 
[DEBUG] 2020-04-03 13:45:12,470 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139)
==> Parameters: 41(Integer)
[DEBUG] 2020-04-03 13:45:12,475 method:org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:139)
<==      Total: 1
false

从输出结果可以看出,当SqlSession没关闭时,返回值为true,并且通过日志发现只执行一次Sql语句;
当SqlSession被关闭后重新开启时,返回值为false,并且通过日志发现执行了两次Sql语句。

◆◆SqlSession对象清空缓存还有一种方法:clearCache();

★注:当调用SqlSession的修改、添加、删除、cimmit()、close()的方法时,都会清空1级缓存★

一级缓存默认是开启的,如果想关闭,则修改SqlMapConfig.xml配置文件:

<settings>
	<setting name="localCacheScope" value="STATEMENT"/>
</settings>
<!--localCacheScope:一级缓存范围,SESSION和STATEMENT两种取值,默认是SESSION,如果改为STATEMENT,这样每次执行完一个Mapper中的语句后都会将一级缓存清除。-->

二级缓存

它指的是Mybatis中SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建的SqlSession共享其缓存。

二级缓存是默认不开启的,需要完成以下步骤:

1.让Mybatis框架支持二级缓存(在SqlMapConfig.xml中配置)

<!--cacheEnabled:所有映射器中配置的缓存的全局开关默认值就是true,所以可以不用写下面的内容-->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

2.让当前的映射文件支持二级缓存(在Mapper.xml中配置)

3.让当前的操作支持二级缓存(在select标签中配置)

<mapper namespace="com.itcast.dao.IUserDao">
    <cache />  <!--让当前的映射文件支持二级缓存-->
    <select id="..." parameterType="..." resultType="..." useCache="true">...</select>
    <!--使用useCache属性,让当前的操作支持二级缓存-->
</mapper>

注意:二级缓存存放的是数据,而不是对象(和一级缓存不一样),所以缓存之后同一个查询得到的对象不会一样,但是数据是一样的。

注解开发

环境搭建

SqlMapConfig.xml里

<mappers>
    <mapper class="com.itcast.dao.AnnoDao" /> <!--xml配置用resource属性,注解配置用class属性-->
    <!--或者用<package name="com.itcast.dao"/>,注解和xml都可以-->
</mappers>

@Select

在AnnoDao接口里定义方法

public interface AnnoDao {
    @Select("select * from user")
    public List<User> selectAll();
}

测试代码

public class AnnoDaoTest {
    private AnnoDao annoDao = null;
    private SqlSession sqlSession = null;
    @Before
    public void init() throws IOException {
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("SqlMapConfig.xml"));
        sqlSession = sqlSessionFactory.openSession(true);
        annoDao = sqlSession.getMapper(AnnoDao.class);
    }
    @After
    public void destroy(){
        sqlSession.close();
    }
    @Test
    public void selectAllTest(){
        List<User> users = annoDao.selectAll();
        for (User user : users) {
            System.out.println(user);
        }
    }
}

■注:注解配置是不需要像xml配置那样指定参数类型和返回值类型的,因为注解本来就是装载在方法上,通过反射是可以获取得到的。

■细节:一个Dao接口,如果使用了注解配置,那么在resources文件夹下com/…/…(与包名结构一样)文件夹就不能再有“接口名.xml”,否则会报异常。所以在同一个接口下,要么都使用注解,要么都使用xml

@Insert

插入数据,AnnoDao接口里

@Insert("insert into user(username,birthday,sex,address) values (#{username},#{birthday},#{sex},#{address})") //和xml配置的语法一样
void insertUser(User user);

测试代码

@Test
public void insertUserTest(){
    User user = new User();
    user.setUsername("马化腾");
    user.setSex("女");
    user.setBirthday(new Date());
    user.setAddress("深圳");
    annoDao.insertUser(user);
}

@Update

@Update("update user set username = #{username},birthday = #{birthday},sex = #{sex},address = #{address} where id = #{id}")
void updateUser(User user);
@Test
public void updateUserTest(){
    User user = new User();
    user.setId(49);
    user.setUsername("麻花疼");
    user.setSex("女");
    user.setBirthday(new Date());
    user.setAddress("广州");
    annoDao.updateUser(user);
}

@Delete

@Delete("delete from user where id = #{id};")
void deleteUser(int id);
@Test
public void deleteUserTest(){
annoDao.deleteUser(49);
}

数据库字段名和类的属性名不对应的解决方法

在xml配置中,如果有不对应的可以使用resultMap标签来映射,在注解开发中,可以使用@Results注解。

@Select("select * from user")
@Results(id="userMap",value={
    /*
    id属性:表示是否为id字段,默认为false,相当于resultMap标签里的id标签。
    property:对应类的属性名称
    column:对应数据库的字段名称
    */
    @Result(id=true,column = "id",property = "userId"),
    @Result(column = "username",property = "userName"),
    @Result(column = "address",property = "userAddress"),
    @Result(column = "sex",property = "userSex"),
    @Result(column = "birthday",property = "userBirthday"),
})
List<User> findAll();

//当在其他方法中,不需要复制粘贴这些内容,只需要加上@ResultMap注解
@Select("select * from user  where id=#{id} ")
@ResultMap(value = {"userMap"})   //value绑定上面@Results的id属性
User findById(Integer userId);

多表查询

一对一

有Account(订单)表和User(用户)表,每一个订单对应着一个用户。需要用到@One注解

@Select("select * from user where id = #{id}")
User selectUserById(int id);//根据id查询用户

/**
     * 查询所有的订单,并将与之关联的用户也查询出来
     * @return
     */
@Select("select * from account")
@Results(value = {
    @Result(id = true,property="id",column = "id"),
    @Result(property="uid",column = "uid"),
    @Result(property="money",column = "money"),
    /*
    	one属性:值只能为@One注解,@One中的属性
    	select:类名.方法名,自动执行里面指定的方法,传入column里指定的数据库字段里的参数,将方法的返回值存入property指定的属性中。
    	fetchType:有FetchType.EAGER,FetchType.LAZY,FetchType.DEFAULT三种取值,EAGER代表立即加载,LAZY代表延迟加载。
    */
    @Result(property = "user",column = "uid",one = @One(select = "com.itcast.Dao.AnnoDao.selectUserById",fetchType = FetchType.EAGER))
})
List<Account> selectAllAccountsLinkedUser();

测试时候发现一个问题,应该是版本的bug问题,使用的mybatis3.2.6版本,我执行时给我报一个 Cannot use both @One and @Many annotations in the same @Result异常!而且那个版本的@One注解的fetchType属性的名字是lazy。我把他改成3.4.5版本才解决问题。

一对多

有Account(订单)表和User(用户)表,每一个用户拥有多个订单。需要用到@Many注解

@Select("select * from account where uid = #{uid}")
List<Account> selectAccountsByUid(int uid);

@Select("select * from user")
@Results(value = {
    @Result(id = true,property = "id",column = "id"),
    @Result(property = "username",column = "username"),
    @Result(property = "birthday",column = "birthday"),
    @Result(property = "sex",column = "sex"),
    @Result(property = "address",column = "address"),
    @Result(property = "accounts",column = "id",many=@Many(select = "com.itcast.Dao.AnnoDao.selectAccountsByUid",fetchType = FetchType.LAZY))
})
List<User> selectAllUserLinkedAccounts();

◆◆感觉这里的多表查询和上面“延迟加载”那里还是蛮像的,查询时不需要用到内外连接,只是在配置ResultMap标签或Results注解时,引用别的方法,把调用别的方法的返回值存到property指定的属性里。

二级缓存开启方法

一级缓存是默认开启的,但是二级缓存需要以下步骤:

1、开启全局配置

<!--cacheEnabled:所有映射器中配置的缓存的全局开关默认值就是true,所以可以不用写下面的内容-->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

2、在Dao接口加上注解

@CacheNamespace(blocking = true) //blocking属性默认为false,当设置为true时开启二级缓存
public interface AnnoDao {
    ....
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值