Mybatis学习笔记

1,Mybatis 概念

持久层框架:Mybatis(半自动,支持自定义的sql),Hibernate(全自动)

Mybatis: 是一个持久层(与数据库打交道)框架。ORM框架

数据临时(瞬时)状态:

数据永久状态:

持久化:将数据从临时状态转变成永久状态的过程

持久层:dao层,数据访问层。

2, ORM介绍

​ ORM:对象关系映射

3,第一个Mybatis案例

3.1 基于传统模式

新建一个Maven项目

导包

<dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.7</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.26</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

编写核心配置文件 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>
    <properties resource="database.properties"></properties>
    <environments default="dev">
        <environment id="dev">
            <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>
    <!--关联映射配置文件-->
    <mappers>
        <!--不是写包,也不是指定类。指定位置-->
        <mapper resource="mapper/UserMapper.xml"></mapper>
    </mappers>
</configuration>

编写映射配置文件 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">
<mapper namespace="a">
    <select id="a" resultType="com.syh.pojo.User">
        select * from user where id = #{id}
    </select>
    <insert id="addUser" parameterType="com.syh.pojo.User">
        insert into user values(null,#{username},#{password},#{sex})
    </insert>
</mapper>

测试

package com.syh.test;

import com.syh.mapper.UserMapper;
import com.syh.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 org.junit.Test;

import java.io.IOException;
import java.io.Reader;

/**
 * @Author Shan
 * @Create 2023/11/15 15:01
 */
public class Test01 {
    @Test
    public void test01() throws IOException {
        //
        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
        //构造SqlSessionFactory工程
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
        //得到一个SqlSession对象
        SqlSession sqlSession = factory.openSession(true);
        //得到Mapper接口对象
//        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//        User user = userMapper.getUserById(1);
//        System.out.println(user);
        Object o = sqlSession.selectOne("a.a", 1);
        System.out.println(o);
        //关闭资源
        sqlSession.close();
    }
}

3.2 注意点
<mappers>
    <mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>
<properties resource="database.properties"></properties>

在build中配置resources

其中的 resource 都会到 maven项目中 的 resource目录中查找。

如果你的映射配置文件写在java源代码中,需要在maven项目中指定resource的路径

<!--
	在build中配置resources,来防止我们资源导出失败的问题
	这就相当于将src/main/java路径下的properties,xml文件都置于resource下
-->
<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

这种方式对于配置文件的名字,命名空间以及statement的id没有要求,只需要

Object o = sqlSession.selectOne(“com.syh.mapper.UserMapper.getUserById1”, 1);

其中 com.syh.mapper.UserMapper.getUserById1 正确指定即可

关联mapper映射文件,必须指定其路径。不能指定类或包

<!--关联映射配置文件-->
<mappers>
	<!--        
	<package name="com.syh.mapper"/>
	<mapper class="com.syh.mapper.UserMapper"></mapper>
	-->
    <mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>
3.3 基于Mapper接口的方式

新建一个maven项目

导包

核心配置文件

<?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>
    <properties resource="database.properties"></properties>
    <environments default="dev">
        <environment id="dev">
            <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>
    <!--关联映射配置文件-->
    <mappers>
        <!--只能指定类,或包名。不能指定位置-->
        <package name="com.syh.mapper"/>
    </mappers>
</configuration>

映射配置文件

<?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">
<mapper namespace="com.syh.mapper.UserMapper">
    <select id="getUserById" resultType="com.syh.pojo.User">
        select * from user where id = #{id}
    </select>
    <insert id="addUser" parameterType="com.syh.pojo.User">
        insert into user values(null,#{username},#{password},#{sex})
    </insert>
</mapper>

3.3 注意点

这种方式是基于动态代理实现的,需要满足一定的约定

1,接口和配置文件放置同一目录下(前提要在build中配置resources)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

或resource同一目录下

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

或在关联映射配置文件指定xml位置

<mappers>
    <mapper class="com.syh.mapper.UserMapper"></mapper>
    <!--指定映射配置文件-->
    <mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>

2,接口文件名必须和映射配置文件名一致

3,接口方法名必须和映射文件中的statementID保持一致

接口

public interface UserMapper {
    User getUserById(int id);
    int addUser(User user);
}

映射配置文件

<!--namespace必须是接口的全类名-->
<mapper namespace="com.syh.mapper.UserMapper">
    <!-- id 必须和 接口中的方法名一致-->
    <select id="getUserById" resultType="com.syh.pojo.User">
        select * from user where id = #{id}
    </select>
     <!-- id 必须和 接口中的方法名一致-->
    <insert id="addUser" parameterType="com.syh.pojo.User">
        insert into user values(null,#{username},#{password},#{sex})
    </insert>
</mapper>

4,映射配置文件中的namespace必须是接口的全类名

<!--namespace必须是接口的全类名-->
<mapper namespace="com.syh.mapper.UserMapper">
    <select id="getUserById" resultType="com.syh.pojo.User">
        select * from user where id = #{id}
    </select>
    <insert id="addUser" parameterType="com.syh.pojo.User">
        insert into user values(null,#{username},#{password},#{sex})
    </insert>
</mapper>

测试

package com.syh.test;

import com.syh.mapper.UserMapper;
import com.syh.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 org.junit.Test;

import java.io.IOException;
import java.io.Reader;

/**
 * @Author Shan
 * @Create 2023/11/15 15:01
 */
public class Test01 {
    @Test
    public void test01() throws IOException {
        //
        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
        //构造SqlSessionFactory工程
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
        //得到一个SqlSession对象
        SqlSession sqlSession = factory.openSession(true);
        //得到Mapper接口对象
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = userMapper.getUserById(1);
        System.out.println(user);
//        Object o = sqlSession.selectOne("a.a", 1);
//        System.out.println(o);
        //关闭资源
        sqlSession.close();
    }
}

4,MyBatis执行SQL的两种方式

MyBatis 有两种执行 SQL 语句的方式,如下:

  1. 通过 SqlSession 发送 SQL
  2. 通过 SqlSession 获取 Mapper 接口,通过 Mapper 接口发送 SQL
  • SqlSession发送SQL

有映射器之后就可以通过 SqlSession 发送 SQL 了。我们以 getWebsite 这条 SQL 为例看看如何发送 SQL。

Website website = (Website)sqlSession.selectOne("net.biancheng.mapper.WebsiteMapper.getWebsite",1);

MyBatis 常用的查询方法有 2 种,分别为 selectOne 和 selectList。

1)selectOne

selectOne 方法表示使用查询并且只返回一个对象,必须指定查询条件。只能查询 0 或 1 条记录,大于 1 条记录则运行错误。常用格式如下(也有其它重载方法,根据需要选择)。

sqlSession.selectOne(String arg0, Object arg1)

2)selectList

selectList 方法表示使用查询并且返回一个列表。可以查询 0 或 N 条记录。常用格式如下。

sqlSession.selectOne(String arg0)

也可指定参数:

sqlSession.selectList(String arg0, Object arg1)

以上语法格式中,String 对象由一个命名空间加 SQL id 组合而成,它完全定位了一条 SQL,这样 MyBatis 就会找到对应的 SQL。Object 对象为需要传递的参数,也就是查询条件。

selectOne 实现的 selectList 都可以实现,即 list 中只有一个对象。但 selectList 能实现的,selectOne 不一定能实现。

如果 MyBatis 中只有一个 id 为 getWbsite 的 SQL,那么也可以简写为:

Website website = (Website )sqlSession.selectOne("getWbsite",1);

这是 MyBatis 前身 iBatis 所留下的方式。

  • Mapper接口发送 SQL

SqlSession 还可以获取 Mapper 接口,通过 Mapper 接口发送 SQL,如下所示。

WebsiteMapper websiteMapper = sqlSession.getMapper(WebsiteMapper.class);Website website = websiteMapper.getWebsite(1);

通过 SqlSession 的 getMapper 方法获取一个 Mapper 接口,然后就可以调用它的方法了。因为 XML 文件或者接口注解定义的 SQL 都可以通过“类的全限定名+方法名”查找,所以 MyBatis 会启用对应的 SQL 运行,并返回结果。

区别

上面分别讲解了 MyBatis 两种发送 SQL 的方式,一种用 SqlSession 直接发送,另外一种通过 SqlSession 获取 Mapper 接口再发送。笔者建议采用 Mapper 接口发送 SQL 的方式,理由如下:

  • 使用 Mapper 接口编程可以消除 SqlSession 带来的功能性代码,提高可读性,而 SqlSession 发送 SQL,需要一个 SQL id 去匹配 SQL,比较晦涩难懂。
  • 使用 Mapper 接口,类似 websiteMapper.getWebsite(1) 则是完全面向对象的语言,更能体现业务的逻辑。
  • 使用 websiteMapper.getWebsite(1) 方式,IDE 会提示错误和校验,而使用 sqlSession.selectOne(“getWebsite”,1L) 语法,只有在运行中才能知道是否会产生错误。

目前使用 Mapper 接口编程已成为主流,尤其在 Spring 中运用 MyBatis 时,Mapper 接口的使用就更为简单,所以本教程使用 Mapper 接口的方式讨论 MyBatis。

5,MyBatis核心对象

MyBatis 有三个基本要素:

  • 核心接口和类
  • MyBatis核心配置文件(mybatis-config.xml)
  • SQL映射文件(mapper.xml)

下面首先介绍 MyBatis 的核心接口和类,如下所示

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个 MyBatis 应用程序都以一个 SqlSessionFactory 对象的实例为核心。

首先获取 SqlSessionFactoryBuilder 对象,可以根据 XML 配置文件或者 Configuration 类的实例构建该对象。

然后获取 SqlSessionFactory 对象,该对象实例可以通过 SqlSessionFactoryBuilder 对象来获取。

有了 SqlSessionFactory 对象之后,就可以进而获取 SqlSession 实例。SqlSession 对象中完全包含以数据库为背景的所有执行 SQL 操作的方法,用该实例可以直接执行已映射的 SQL 语句。

  • SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 会根据配置信息或者代码生成 SqlSessionFactory,并且提供了多个 build() 方法重载,如图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

SqlSessionFactoryBuilder的生命周期和作用域

SqlSessionFactoryBuilder 的最大特点就是用过即丢。创建 SqlSessionFactory 对象之后,这个类就不存在了,因此 SqlSessionFactoryBuilder 的最佳范围就是存在于方法体内,也就是局部变量。

  • SqlSessionFactory

SqlSessionFactory 是工厂接口而不是现实类,他的任务就是创建 SqlSession。

所有的 MyBatis 应用都以 SqlSessionFactory 实例为中心,SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 对象来获取。有了它以后,顾名思义,就可以通过 SqlSession 提供的 openSession() 方法来获取 SqlSession 实例。源码如下。

public interface SqlSessionFactory {
    SqlSession openSession();
    SqlSession openSession(boolean autoCommit);
    SqlSession openSession(Connection connection);
    SqlSession openSession(TransactionIsolationLevel level);
    SqlSession openSession(ExecutorType execType);
    SqlSession openSession(ExecutorType execType, boolean autoCommit);
    SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
    SqlSession openSession(ExecutorType execType, Connection connection);
    Configuration getConfiguration();
}

SqlSessionFactory的生命周期和作用域

SqlSessionFactory 对象一旦创建,就会在整个应用程序过程中始终存在。没有理由去销毁或再创建它,并且在应用程序运行中也不建议多次创建 SqlSessionFactory。因此 SqlSessionFactory 的最佳作用域是 Application,即随着应用程序的生命周期一直存在。这种“存在于整个应用运行期间,并且只存在一个对象实例”的模式就是所谓的单例模式(指在运行期间有且仅有一个实例)。

  • SqlSession

SqlSession 是用于执行持久化操作的对象,类似于 JDBC 中的 Connection。它提供了面向数据库执行 SQL 命令所需的所有方法,可以通过 SqlSession 实例直接运行已映射的 SQL 语句。

void clearCache();
Configuration getConfiguration();
void rollback(boolean force);
void commit(boolean force);
int delete(String statement, Object parameter);
...

SqlSession 的用途主要有两种。

  1. 获取映射器。让映射器通过命名空间和方法名称找到对应的 SQL,并发送给数据库,执行后返回结果。
  2. 直接通过“命名空间(namespace)+SQL id”的方式执行 SQL,不需要获取映射器。这是 iBatis 版本留下的方式。

SqlSession生命周期和作用域

SqlSession 对应一次数据库会话。由于数据库会话不是永久的,因此 SqlSession 的生命周期也不是永久的,每次访问数据库时都需要创建 SqlSession 对象。

需要注意的是:每个线程都有自己的 SqlSession 实例,SqlSession 实例不能被共享,也不是线程安全的。因此 SqlSession 的作用域范围是 request 作用域或方法体作用域内。

6,Mybatis工具类封装

package com.cxs.utils;

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.InputStream;

public class MyBatisUtils {

  	//获得SqlSession工厂
    private static SqlSessionFactory factory;

  	//创建ThreadLocal绑定当前线程中的SqlSession对象
    private static final ThreadLocal<SqlSession> tl = new ThreadLocal<SqlSession>();

    static {
        try {
            InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
            factory = new SqlSessionFactoryBuilder().build(is);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //获得连接(从tl中获得当前线程SqlSession)
    private static SqlSession openSession(){
        SqlSession session = tl.get();
        if(session == null){
            session = factory.openSession();
            tl.set(session);
        }
        return session;
    }

    //释放连接(释放当前线程中的SqlSession)
    private static void closeSession(){
        SqlSession session = tl.get();
        session.close();
        tl.remove();
    }

    //提交事务(提交当前线程中的SqlSession所管理的事务)
    public static void commit(){
        SqlSession session = openSession();
        session.commit();
        closeSession();
    }

    //回滚事务(回滚当前线程中的SqlSession所管理的事务)
    public static void rollback(){
        SqlSession session = openSession();
        session.rollback();
        closeSession();
    }

    //获得接口实现类对象
    public static <T extends Object> T getMapper(Class<T> clazz){
        SqlSession session = openSession();
        return session.getMapper(clazz);
    }
}

7,核心配置文件

MyBatis 核心配置文件的结构如下。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><!-- 配置 -->
    <properties /><!-- 属性 -->
    <settings /><!-- 设置 -->
    <typeAliases /><!-- 类型命名 -->
    <typeHandlers /><!-- 类型处理器 -->
    <objectFactory /><!-- 对象工厂 -->
    <plugins /><!-- 插件 -->
    <environments><!-- 配置环境 -->
        <environment><!-- 环境变量 -->
            <transactionManager /><!-- 事务管理器 -->
            <dataSource /><!-- 数据源 -->
        </environment>
    </environments>
    <databaseIdProvider /><!-- 数据库厂商标识 -->
    <mappers /><!-- 映射器 -->
</configuration>

mybatis-config.xml 文件中的元素节点是有一定顺序的,节点位置必须按以上位置排序,否则会编译错误。

configuration 元素是整个 XML 配置文件的根节点,其角色就相当于是 MyBatis 的总管,MyBatis 所有的配置信息都会存放在它里面。

7.1 properties标签

properties 标签可以通过 resource 属性指定外部 properties 文件(database.properties),也可以通过 properties 子元素配置。

  1. 指定文件

使用 properties 指定外部文件,代码如下。

<properties resource="mybatisDemo/resources/database.properties"/>

database.properties 用于描述数据库连接的相关配置,例如数据库驱动、连接数据库的 url、数据库用户名、数据库密码等。

  1. properties子元素配置

通过 properties 子元素 property 配置 username 和 password 变量,然后在 environments 节点中引用这些变量,代码如下。

<properties>    
	<property name="username" value="root"/>    
	<property name="password" value="root"/>
</properties>

在 environments 节点中引用 username 和 password 变量。

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
</environments>

也可以不使用 properties 标签,直接将属性值写在 value 中。

7.2 settings标签

settings 标签用于配置 MyBatis 的运行时行为,它能深刻的影响 MyBatis 的底层运行,一般不需要大量配置,大部分情况下使用其默认值即可。

settings 的配置项很多,但是真正用到的不会太多,我们把常用的配置项研究清楚就可以了。settings 配置项说明如下表所示(表中红色字体的配置项为常用配置项)。

配置项作用配置选项默认值
cacheEnabled该配置影响所有映射器中配置缓存的全局开关true|falsetrue
lazyLoadingEnabled延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。在特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态true|falsefalse
aggressiveLazyLoading当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载true|false版本3.4.1 (不包含) 之前默认值为 true,之后为 false
multipleResultSetsEnabled是否允许单一语句返回多结果集(需要兼容驱动)true|falsetrue
useColumnLabel使用列标签代替列名。不同的驱动会有不同的表现,具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果true|falsetrue
useGeneratedKeys允许JDBC 支持自动生成主键,需要驱动兼容。如果设置为 true,则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)true|falsefalse
autoMappingBehavior指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射。 PARTIAL 表示只会自动映射,没有定义嵌套结果集和映射结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)NONE、PARTIAL、FULLPARTIAL
autoMappingUnkno wnColumnBehavior指定自动映射当中未知列(或未知属性类型)时的行为。 默认是不处理,只有当日志级别达到 WARN 级别或者以下,才会显示相关日志,如果处理失败会抛出 SqlSessionException 异常NONE、WARNING、FAILINGNONE
defaultExecutorType配置默认的执行器。SIMPLE 是普通的执行器;REUSE 会重用预处理语句(prepared statements);BATCH 执行器将重用语句并执行批量更新SIMPLE、REUSE、BATCHSIMPLE
defaultStatementTimeout设置超时时间,它决定驱动等待数据库响应的秒数任何正整数Not Set (null)
defaultFetchSize设置数据库驱动程序默认返回的条数限制,此参数可以重新设置任何正整数Not Set (null)
safeRowBoundsEnabled允许在嵌套语句中使用分页(RowBounds)。如果允许,设置 falsetrue|falsefalse
safeResultHandlerEnabled允许在嵌套语句中使用分页(ResultHandler)。如果允许,设置falsetrue|falsetrue
mapUnderscoreToCamelCase是否开启自动驼峰命名规则映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射true|falsefalse
localCacheScopeMyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速联复嵌套査询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlScssion 的不同调用将不会共享数据SESSION|STATEMENTSESSION
jdbcTypeForNull当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHERNULL、VARCHAR、OTHEROTHER
lazyLoadTriggerMethods指定哪个对象的方法触发一次延迟加载equals、clone、hashCode、toString
defaultScriptingLanguage指定动态 SQL 生成的默认语言org.apache.ibatis .script.ing.xmltags .XMLDynamicLanguageDriver
callSettersOnNulls指定当结果集中值为 null 时,是否调用映射对象的 setter(map 对象时为 put)方法,这对于 Map.kcySet() 依赖或 null 值初始化时是有用的。注意,基本类型(int、boolean 等)不能设置成 nulltrue|falsefalse
logPrefix指定 MyBatis 增加到日志名称的前缀任何字符串Not set
loglmpl指定 MyBatis 所用日志的具体实现,未指定时将自动査找SLF4J|LOG4J|LOG4J2|JDK_LOGGING |COMMONS_LOGGING |ST DOUT_LOGGING|NO_LOGGINGNot set
proxyFactory指定 MyBatis 创建具有延迟加栽能力的对象所用到的代理工具CGLIB|JAVASSISTJAVASSIST (MyBatis 版本为 3.3 及以上的)
vfsImpl指定 VFS 的实现类提供 VFS 类的全限定名,如果存在多个,可以使用逗号分隔Not set
useActualParamName允许用方法参数中声明的实际名称引用参数。要使用此功能,项目必须被编译为 Java 8 参数的选择。(从版本 3.4.1 开始可以使用)true|falsetrue

下面给出一个全量的配置样例,如下所示。

<settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="multipleResultSetsEnabled" value="true"/>
    <setting name="useColumnLabel" value="true"/>
    <setting name="useGeneratedKeys" value="false"/>
    <setting name="autoMappingBehavior" value="PARTIAL"/>
    <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
    <setting name="defaultExecutorType" value="SIMPLE"/>
    <setting name="defaultStatementTimeout" value="25"/>
    <setting name="defaultFetchSize" value="100"/>
    <setting name="safeRowBoundsEnabled" value="false"/>
    <setting name="mapUnderscoreToCamelCase" value="false"/>
    <setting name="localCacheScope" value="SESSION"/>
    <setting name="jdbcTypeForNull" value="OTHER"/>
    <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
7.3 常用settings设置
<settings>
    <!--打印日志:打印sql-->
    <setting name="logImpl" value="STDOUT_LOGGING"/>
    <!--
        PARTIAL:只会自动映射没有定义嵌套结果映射的字段
        NONE:不会自动映射
        FULL 会自动映射任何复杂的结果集(无论是否嵌套)。
    -->
    <setting name="autoMappingBehavior" value="PARTIAL"/>
    <!--
    是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。
    -->
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
7.4 typeAliases配置别名

为了不在任何地方都指定类的全限定名,我们可以使用 typeAliases 标签定义一个别名。

例如,在 net.bianchengbang.po 包中有一个 Student 类,则该类的全限定名称为 net.bianchengbang.po.Student。使用 typeAliases 标签定义别名,这样就不用每次都书写类的全限定名称了,代码如下。

<typeAliases>
    <typeAlias alias = "Student" type = "net.bianchengbang.po.Student"/>
</typeAliases>

如果需要对同一个包下的多个类定义别名,则可以定义为:

<typeAliases>
    <package name="net.biancheng.po"/>
</typeAliases>

这样 MyBatis 将扫描 net.biancheng.po 包里面的类,将其第一个字母变为小写作为其别名,例如 Student 别名为 student,User 别名为 user。

7.5 环境配置(environments)

MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。

不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境

<!--
        environments:环境配置(可以配置多个环境)
            default:默认使用哪个环境
         environment:一个具体的环境配置
            id:唯一标识,
            transactionManager:事务管理
                 type:MyBatis 支持两个事务管理器,即 JDBC 和 MANAGED。
				如果使用 JDBC 类型的事务管理器,则应用程序服务器负责事务管理操作,例如提交、回滚等。如果使用 MANAGED 类型的					事务管理器,则应用程序服务器负责管理连接生命周期。
            dataSource :数据源
                dataSource 中的 type 属性用于指定数据源类型,有以下 3 种类型。
                1)UNPOOLED
                UNPOOLED 没有数据库连接池,效率低下。MyBatis 需要打开和关闭每个数据库操作的连接,它有点慢,通常应用于简单的应用程序。
                2)POOLED
                对于 POOLED 数据源类型,MyBatis 将维护一个数据库连接池。并且对于每个数据库的操作,MyBatis 都会使用连接池中的连接,并在操作完成后将它们返回到池中。减少了创建新连接所需的初始连接和身份验证时间。
                3)JNDI
                对于 JNDI 的数据源类型,MyBatis 将从 JNDI 数据源中获取连接。
    -->
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC" />
            <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>
7.5 Mappers 标签

注册映射器

8,映射配置文件

8.1 输入参数

当输入参数为简单类型(8个基本类型+String)时:

  •          #{任意值}(但是一般建议与表字段相同)
    
<select id="getUserById" resultType="user">
    select * from user where id=#{id}
</select>

当输入参数为对象类型时:

  • #{对象的属性名},${对象的属性名}
<insert id="addUser">
    insert into user values(null,#{username},#{password},#{sex});
</insert>

当输入参数只有一个时:

  • #{任意值}
<select id="getUserById" resultType="user">
    select * from user where id=#{id} 
</select>

当输入参数有多个时:

  • 参数的名字为 arg0 arg1 ,param1 param2
<select id="getUser" resultType="user">
    select * from user where user_name=#{arg0}
    and password = #{arg1}
</select><select id="getUser" resultType="user">
    select * from user where user_name=#{param1}
    and password = #{param2}
</select>
  • 使用注解@Param(“name”) 写在接口方法中形参前,然后通过#{username}识别
User getUser(@Param("username") String username,
                 @Param("password") String password);
<select id="getUser" resultType="user">
    select * from user where user_name=#{username}
    and password = #{password}
</select>

当输入参数为多个并且有对象时:通过 #{user.属性}识别

User getUser(@Param("username") String username,
                 @Param("user") User user);
<select id="getUser" resultType="user">
    select * from user where user_name=#{username}
    and password = #{user.password}
</select>
8.2 #{}和${}的区别
  • #{}
<select id="getUser" resultType="user">
    select * from user where user_name=#{username}
    and password = #{password}
</select>

生成的SQL语句为

select * from user where user_name=? and password = ?

采用预处理,占位符的方式。可以有效的防止SQL注入

  • ${}

生成的SQL语句为

select * from user where user_name=admin and password = 123

采用字符串拼接的方式,不是预处理方式,不能有效的防止SQL注入

8.3 ResultType

resultType:返回类型

用户查询 select 命令上:会自动将查询出来的结果映射到返回类型的属性上

<select id="getUserList" resultType="user">
    select * from user;
</select>

如果列名和属性名不一致:

  • 列为下划线 属性为驼峰命名 入 user_name 和 userName

可以设置 下列,让其自动映射

<setting name="mapUnderscoreToCamelCase" value="true"/>
  • 如果属性和列名完全不一致,需要用到resultMap
8.4 ResultMap

自定义结果集映射

用法:

1,定义ResultMap

<resultMap id="userMap" type="com.syh.pojo.User">
    <!--
		只写属性和列名不一致的情况
		property:属性名
		column:列名
	-->
    <result property="name" column="username"></result>
</resultMap>

2,应用ResultMap

<!--
	resultMap : 引用定义好的resultMap的ID
-->
<select id="getUser" resultMap="userMap">
    select * from user where user_name=#{username}
    and password = #{password}
</select>
8.5 insert
获得添加后的自增主键值

接口

int addEmp(Emp emp);

xml

<!--
useGeneratedKeys:这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键
        默认是false
keyProperty:获取出来的主键值会赋值给传递过来的对象中的eid属性
-->
<insert id="addEmp" useGeneratedKeys="true" keyProperty="eid">
    insert into emp(ename,sex,did)
    values(#{ename},#{sex},#{did})
</insert>

测试

//测试添加员工后获取员工的id
@Test
public void test07(){
    SqlSession sqlSession = MybatisUtils.openSession(true);
    EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class);
    Emp emp = new Emp().setEname("admin").setSex("1").setDid(1);
    int i = empMapper.addEmp(emp);

    System.out.println(emp.getEid());
    sqlSession.close();

}
批量添加

接口

int addEmps(@Param("emps") List<Emp> emps);

xml

<!--批量添加-->
<insert id="addEmps">
    insert into emp(ename,sex,did)
    <foreach open=" values " collection="emps" item="emp" separator=",">
        (#{emp.ename},#{emp.sex},#{emp.did})
    </foreach>
</insert>

测试

//测试批量添加
@Test
public void test08(){
    SqlSession sqlSession = MybatisUtils.openSession(true);
    EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class);
//        Emp emp = new Emp().setEname("admin").setSex("1").setDid(1);
    List<Emp> emps = Arrays.asList(
            new Emp().setEname("admin1").setSex("1").setDid(1),
            new Emp().setEname("admin2").setSex("1").setDid(1),
            new Emp().setEname("admin3").setSex("1").setDid(1)
    );
    empMapper.addEmps(emps);
    sqlSession.close();
}
8.6 update
8.7 delete
8.8 sql片段
<!--基础列:sql片段-->
<sql id="base_column">
    eid,ename,sex,did
</sql>
<select id="getEmp" resultType="com.syh.pojo.Emp">
    select 
    <!--
	include:引用sql片段
		refid: sql片段的ID
	-->
    <include: refid="base_column"></include>
    from emp
</select>

9,高级映射

9.1 一对一
create table dept(
	did int  primary key auto_increment,
	dname varchar(20)  not null
);
create table emp(
	eid int   primary key auto_increment,
	ename varchar(20)  not null,
	sex char(1) default 1,
	did int 
);

需求:查询员工信息并将其所在的部门信息一并查出来

方式一,联查方式

实体类

package com.syh.pojo;

import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;

/**
 * @Author Shan
 * @Create 2023/11/20 8:47
 */
@Data
@ToString
@Accessors(chain = true)
public class Emp {
    private Integer eid;
    private String ename;
    private String sex;
    private Integer did;
    private String dname; // 添加一个部门名的属性
}

映射配置文件

<select id="getEmpById" resultType="emp">
    select emp.*,dept.dname from emp left join dept on emp.did = dept.did where emp.eid = #{eid}
</select>
<!--
	因为emp实体类中有dname这个属性,会自动映射
-->
方式二:嵌套结果集

员工实体类

package com.syh.pojo;

import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;

/**
 * @Author Shan
 * @Create 2023/11/20 8:47
 */
@Data
@ToString
@Accessors(chain = true)
public class Emp {
    private Integer eid;
    private String ename;
    private String sex;
    private Integer did;
    private Dept dept; // 添加一个部门对象属性
}

映射配置文件

<!--
	定义一个自定义结果映射
		id:唯一标识
		type:结果映射的类型 (可以使用别名)
	<id>:标识主键字段
-->
<resultMap id="empMap" type="emp">
    <id property="eid" column="eid"></id>
    <result property="ename" column="ename"/>
    <result property="sex" column="sex"/>
    <result property="did" column="did"/>
    <!--
		<association>: 标签实现对对象属性的嵌套映射
			property: emp中的属性名
			javaType: 因为属性是对象类型,需要指出对象的类型是什么
	-->
    <association property="dept" javaType="com.syh.pojo.Dept">
		<id property="did" column="did"></id>
        <result property="dname" column="dname"></result>
    </association>
    <!--
		注:因为默认的自动映射级别为 PARTIAL,所以所有的列都需要自己指定映射关系
 		  PARTIAL:只会自动映射没有定义嵌套结果映射的字段
          NONE:不会自动映射
          FULL 会自动映射任何复杂的结果集(无论是否嵌套)。
	-->
</resultMap>
<!--使用resultMap自定义映射-->
<select id="getEmpById" resultMap="empMap">
    select emp.*,dept.dname from emp left join dept on emp.did = dept.did where emp.eid = #{eid}
</select>

优化映射配置文件

EmpMapper.xml

<!--Emp表的基础映射-->
<resultMap id="BaseMap" type="emp">
    <id property="eid" column="eid"></id>
    <result property="ename" column="ename"/>
    <result property="sex" column="sex"/>
    <result property="did" column="did"/>
</resultMap>
<!--
    extends:结果映射的继承
-->
<resultMap id="userMap" type="emp" extends="BaseMap">

    <!--部门属性 dept 是一个对象,不能用result标签去映射,得用association标签
        association:实现对emp对象中的dept对象属性进行映射
 		resultMap:引用外部的基础映射
                如果是其他配置文件中的映射,需要写命名空间.resultMapID
    -->
    <association property="dept" javaType="dept" resultMap="com.syh.mapper.DeptMapper.BaseMap">

    </association>
</resultMap>

DeptMapper.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">
<mapper namespace="com.syh.mapper.DeptMapper">

    <!--dept表的基础映射-->
    <resultMap id="BaseMap" type="dept">
        <id property="did" column="did"></id>
        <result property="dname" column="dname"></result>
    </resultMap>

</mapper>

方式三:嵌套查询方式

EmpMapper.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">
<mapper namespace="com.syh.mapper.EmpMapper">
    <!--Emp表的基础映射-->
    <resultMap id="BaseMap" type="emp">
        <id property="eid" column="eid"></id>
        <result property="ename" column="ename"/>
        <result property="sex" column="sex"/>
        <result property="did" column="did"/>
    </resultMap>

    <resultMap id="userMap" type="emp" extends="BaseMap">
        <!--
        association:对对象属性的映射
            property:emp对象中的属性名
            javaType:属性的java类型(可以用别名)
            select:引用其他的一个查询命令(指定命名空间.statementId)
            column:将查getEmpById询出来的did列的值作为一个参数传递到 select 命令中
                这种方式不需要 resultMap
        -->
        <association fetchType="lazy" property="dept" javaType="dept"
                     select="com.syh.mapper.DeptMapper.getDeptById"
                     column="did" >

        </association>
    </resultMap>
    <select id="getEmpById" resultMap="userMap">
        select * from emp where eid = #{id}
    </select>

</mapper>

DeptMapper.java

package com.syh.mapper;

import com.syh.pojo.Dept;

/**
 * @Author Shan
 * @Create 2023/11/20 8:48
 */
public interface DeptMapper {

    Dept getDeptById(int did);
}

DeptMapper.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">
<mapper namespace="com.syh.mapper.DeptMapper">

    <!--dept表的基础映射-->
    <resultMap id="BaseMap" type="dept">
        <id property="did" column="did"></id>
        <result property="dname" column="dname"></result>
    </resultMap>

    <select id="getDeptById" resultType="com.syh.pojo.Dept">
        select * from dept where did = #{did}
    </select>

</mapper>

9.2 一对多
方式一:嵌套结果集
<resultMap id="deptMap" type="dept" extends="BaseMap">

    <!--有一个集合属性 List<Emp> emps;-->
    <!--
        collection : 专门对集合属性进行映射
            property:属性名
            javaType: 属性的java类型 如果不是自定义的数据类型可以不写
            ofType: 集合存储的类型,泛型的类型
    -->
    <collection property="emps" javaType="ArrayList" ofType="emp" resultMap="com.syh.mapper.EmpMapper.BaseMap">

    </collection>
</resultMap>

<select id="getDeptById2" resultMap="deptMap">
    select * from dept,emp
    where dept.did = emp.did and dept.did = #{did}
</select>
方式二:嵌套查询
<resultMap id="deptMap" type="dept" extends="BaseMap">

    <!--有一个集合属性 List<Emp> emps;-->
    <!--
        collection : 专门对集合属性进行映射
            property:属性名
            javaType: 属性的java类型 如果不是自定义的数据类型可以不写
            ofType: 集合存储的类型,泛型的类型
    -->
    <collection property="emps"
                javaType="ArrayList"
                ofType="emp"
                fetchType="lazy"
                select="com.syh.mapper.EmpMapper.getEmpByDid"
                column="did">

    </collection>
</resultMap>

<select id="getDeptById2" resultMap="deptMap">
    select * from dept where did = #{did}
</select>
<?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">
<mapper namespace="com.syh.mapper.EmpMapper">
    <!--Emp表的基础映射-->
    <resultMap id="BaseMap" type="emp">
        <id property="eid" column="eid"></id>
        <result property="ename" column="ename"/>
        <result property="sex" column="sex"/>
        <result property="did" column="did"/>
    </resultMap>
 
    <select id="getEmpByDid" resultType="com.syh.pojo.Emp">
        select * from emp where did = #{did}
    </select>

</mapper>

10,动态SQL

10.1 if
10.2 where
10.3 choose,when,otherwise
10.4 set
10.5 foreach

mapper接口

/**
     *  因为参数不是八大基础数据类型和字符串类型,
     *  所以需要@Param("ids") 定义一个名字
     *  或则 Available parameters are [arg0, collection, list]
     */
//    int deleteEmps(List<Integer> ids);
//    int deleteEmps(@Param("ids") Set<Integer> ids);
//    int deleteEmps(@Param("ids") Integer[] ids);
    int deleteEmps(@Param("ids") Map<String,Integer> map);

映射配置文件

<delete id="deleteEmps">

    delete from emp
    where
    <!--
        foreach:循环 List,Set,Map,数组
            collection:要循环的项,配合@Param("ids")或用
                   List: 参数名可以使用arg0,list,collection
                   Set:  参数名可以使用arg0,collection
                   数组: 参数名可以使用array, arg0
            index:索引
            open:开始
            close:结尾
            separator:分隔符
    -->
    <foreach open="eid in (" close=")"
             separator=","
             item="id"
             index="i"
             collection="ids">
        #{id}
    </foreach>


</delete>
10.6 bind

相当于定义了一个变量

​ name:自己取名

​ value: 对应的值

​ 通过 #{enamelike} 引用该值

<select id="getEmp03" resultType="com.syh.pojo.Emp">
    <bind name="enamelike"  value="'%'+ename+'%'" />
    select
    <include refid="base_column"/>
    from emp
    where ename like #{enamelike}
</select>
10.7 trim

trim表示也是动态sql

prefix:给嵌套的sql添加一个前缀

prefixOverrides:去除 SQL 语句前面的关键字或字符,下面案例会直接去掉第一个and关键字

替换where+if条件查询

<select id="getEmp" resultType="com.syh.pojo.Emp">
    select
    <include refid="base_column"></include>
    from emp
    <trim prefix="where" prefixOverrides="and">
        <if test="ename!='' and ename!=null">
            and ename like concat('%',#{ename},'%')
        </if>
        <if test="sex!='' and sex!=null">
            and sex = #{sex}
        </if>
    </trim>
</select>

替换set+if修改

<!--
    prefix:前缀,会给包含sql前面添加 set 
    suffix:后缀,会改包含的sql后面添加where 
    prefixOverrides:清除前面的第一个逗号,
    suffixOverrides:清除最后面的一个逗号,
-->
<update id="updateEmp">
    update emp
    <trim prefix="set" prefixOverrides="," suffixOverrides=","
          suffix="where">
        <if test="ename!='' and ename!=null">
            ename=#{ename},
        </if>
        <if test="sex!='' and sex!=null">
            sex=#{sex},
        </if>
        <if test="did!=null">
            ,did=#{did}
        </if>
    </trim>
    eid = #{eid}
</update>

11,缓存

12,分页

1, 传统分页limit
2, RowBounds分页

Mapper接口

List<User> getUserByRowBounds();

Mapper.xml

<select id="getUserByRowBounds" resultMap="userMap">
    select id, name, pwd from `user`
</select>

测试

@Test
public void getUserByRowBounds() {
    SqlSession sqlSession = null;
    try {
        sqlSession = MybatisUtils.getSqlSession();
        RowBounds rowBounds = new RowBounds(1,2);
        List<User> userList = sqlSession.selectList("com.zyy.dao.UserMapper.getUserByRowBounds",null, rowBounds);
        for (User user : userList) {
            logger.info(user);
        }
    } finally {
        if (sqlSession != null) {
            sqlSession.close();
        }
    }

}
3, 分页插件 PageHelper

1,导包

<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>

2,在核心配置文件中配置分页插件

<!--配置分页插件-->
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <property name="helperDialect" value="mysql" />
        <property name="offsetAsPageNum" value="true" />
        <property name="rowBoundsWithCount" value="true" />
        <property name="reasonable" value="true" />
    </plugin>
</plugins>

a. helperDialect: mysql:设置 MyBatis 使用的分页插件的数据库方言为 MySQL。这样 MyBatis 在生成分页查询语句时会使用 MySQL 的语法。PageHelper默认支持Oracle、MySQL、MariaDB、SQLite、Hsqldb、PostgreSQL等方言

b. offsetAsPageNum: true:将分页查询中的偏移量(offset)视为页码。当设置为 true 时,在使用分页插件进行分页查询时,offset 参数将被当作页码来使用。

c. rowBoundsWithCount: true:设置分页插件在进行分页查询时是否同时查询总记录数。当设置为 true 时,分页插件会在进行分页查询时额外执行一次查询,用于查询总记录数。

d. reasonable: true:用于在分页查询中进行合理化处理。当设置为 true 时,在查询的页码超出总页数时,会自动调整为最后一页或第一页,避免出现超出范围的页码。

3,使用

@Test
public void test03(){
    EmpMapper empMapper = MyBatisUtils.getMapper(EmpMapper.class);
    PageHelper.startPage(1,2);
    List<Emp> list = empMapper.getEmpByRowBounds();
    PageInfo<Emp> pageInfo = new PageInfo<>(list);
    System.out.println(pageInfo);
}

4, PageHelper文档

https://pagehelper.github.io/docs/

13,逆向工程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值