[SSM基础] Mybatis学习笔记

Mybatis学习笔记

注:因为本人第一次学习mybatis,这个文章只是想记录一下学习的总结笔记,如果理解有偏差欢迎指正,谢谢包含;

一、Mybatis概述

基本概念

百度百科:

MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了google code,并且改名为MyBatis。2013年11月迁移到Github。

iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAOs)。

特点:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-craV2pqt-1642738633051)(C:\Users\86185\Desktop\笔记\Mybatis\mybatis-logo.png)]

三层架构

一般一个bs项目通常分为三层:界面层(User Interface layer)、业务逻辑层(Business Logic Layer)、数据访问层(Data access layer)

MyBatis就是三层架构中的数据访问层所使用的框架;

学习Mybatis前先简单介绍一下这三部分:

三层的职责 :

  1. 界面层(表示层,视图层):主要功能是接受用户的数据,显示请求的处理结果。使用 web 页面和用户交互,手机 app 也就是表示层的,用户在 app 中操作,业务逻辑在服务器端处理。
  2. 业务逻辑层:接收表示传递过来的数据,检查数据,计算业务逻辑,调用数据访问层获取数据。
  3. 数据访问层:与数据库打交道。主要实现对数据的增、删、改、查。将存储在数据库中的数据提交给业务层,同时将业务层处理的数据保存到数据库.

优点:

三层架构区分层次的目的是为了 “高内聚,低耦合”。开发人员分工更明确,将精力更专注于应用系统核心业务逻辑的分析、设计和开发,加快项目的进度,提高了开发效率,有利于项目的更新和维护工作。

三层对应的包:

界面层: controller包 (servlet)

业务逻辑层: service 包(XXXService类)

数据访问层: dao包(XXXDao类)

三层中类的交互:
用户使用界面层–> 业务逻辑层—>数据访问层(持久层)–>数据库(mysql)

三层对应的处理框架:
界面层—servlet—springmvc(框架)
业务逻辑层—service类–spring(框架)
数据访问层—dao类–mybatis(框架)

简单了解了这三层架构,那么学习MyBatis过程中就更能清楚的知道到自己学的框架到底干什么,用在哪里了;

自我理解

Mybatis就是一个可以自定义Sql的持久层框架,提供了操作数据库的能力;它内置了 JDBC 操作,简化了数据库的访问流程,让我们只需要关心如何写好Sql语句即可;(可以简单理解为就是一个增强的JDBC)

JDBC回顾

简单回顾一下JDBC的主要五部操作:

// 1,注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2,获取链接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/数据库名?serverTimezone=UTC", "用户", "密码");
// 3,获取数据操作对象
String sql = "sql语句";
preparedStatement = connection.prepareStatement(sql);
// 4,执行sql语句
resultSet = preparedStatement.executeQuery();
// 5,处理结果集
while (resultSet.next()) {
	// 对应操作
}

这五步只要你需要写sql语句操作数据库都需要写,所以很麻烦,于是我们又自定义了JDBC工具类:

/*
    JDBC工具类,简化JDBC编程
*/
public class DBUtil {
    /**
     * 工具类中的构造方法是私有的
     * 因为工具类中的方法都是静态的,直接通过类名去调即可。
     */
    private DBUtil(){}
    
    /**
     * 静态代码块,类加载的时候执行
     * 把 注册驱动 程序的代码放在静态代码块中,避免多次获取连接对象时重复调用
     */
    static {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
    // 获取连接
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/数据库名?serverTimezone=UTC", "用户", "密码");
    }
    
    // 关闭方法
    public static void close(Connection connection, Statement statement, ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

这样大大减少了重复代码的出现,但是每次使用时依然不够精简,因为处理结果等操作依旧是重复的;

并且最不好的一点就是Sql语句和Java代码写在了一起,如果想要快速找到某个Sql语句的操作十分麻烦,而且不易管理;

所以为了解决这个问题,我们可以使用MyBatis来写出更简洁的代码来;

MyBatis解决的问题

减轻使用 JDBC 的复杂性,不用编写重复的创建 Connetion , Statement ;不用编写关闭资源代码; 可以直接使用 java 对象表示结果数据,让开发者专注 SQL 的处理,其他分心的工作由 MyBatis 代劳。

所以上面的JDBC的操作MyBatis都可以解决,而需要我们做的就是写好Sql语句配置好就可以了;

二、第一个MyBatis程序

前提条件

现在我已经在数据库中创建好了一个student表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nN1br5HY-1642738633052)(C:\Users\86185\Desktop\笔记\Mybatis\数据库表student.png)]
接下来的所有操作都是基于这张表进行;

准备工作

在Maven中添加对应版本的Mybatis依赖来导入jar包

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

最好把mysql的依赖也加上

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

在 < build >标签中加上以下内容:

<resources>
	<resource>
    	<!--所在的目录-->
		<directory>src/main/java</directory>
    	<!--包括目录下的.properties,.xml 文件都会扫描到-->
		<includes>
			<include>**/*.properties</include>
			<include>**/*.xml</include>
		</includes>
		<filtering>false</filtering>
	</resource>
</resources>

为什么需要这个呢?这是为了保证编译后对应的xml等配置文件也可以生成到对应的编译后的文件(target)位置,这样Mybatis才可以从中找到我们的配置;如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AThJsTQw-1642738633052)(C:\Users\86185\Desktop\笔记\Mybatis\resource标签的目的.png)]

项目结构

配置好了maven对应的pom.xml文件后,接下来就需要构建项目了,这里我先展示一下整体的结构,因为每一部分的内容都是固定的,所以一定要清楚每个位置应该放什么东西

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b5Wt2ihP-1642738633053)(C:\Users\86185\Desktop\笔记\Mybatis\文件结构.png)]

可以看到我创建了一个Maven模块,创建了一个com.yang包,这个包下有三个包,下面分别介绍一下它们是干什么的

  • dao包:这个包是放接口和对应的Mapper.xml文件的,在这里mapper文件名必须和对应接口名相同;
  • domain包:这个包存放对应数据库表的一些类
  • utils包:顾名思义这是个工具包;

下面的resource包用来存放config.xml等一些配置文件;

test包就是一个测试用的包;

记住这些结构,它们其实都是固定的,后面我会一一说;

domain包

这个包中的类对应数据库的每一个表,后期可以通过Mybatis来执行sql获取对应表类型的数据;

因为我只有一个student表,所以需要一个Student类,这个类的属性需要和student表的每一个字段类型相匹配,并且建议最好属性名和student表的属性名相同,不然后期会多几步操作(后面会讲)

public class Student {
    private int id;
    private String name;
    private String email;
    private int age;

    public Student() {
    }

    public Student(int id, String name, String email, int age) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.age = age;
    }

    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Student)) return false;
        Student student = (Student) o;
        return id == student.id && age == student.age && Objects.equals(name, student.name) && Objects.equals(email, student.email);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, email, age);
    }
}

这里需要注意:一定要把get和set方法加上,因为mybatis的实现设计到动态代理的内容,这里就不过多阐述了,但是这些细节一定要记住;

这里我重写了toString等方法,就是为了测试方便;

dao包

这个包放的是一些接口,这些接口的作用就是来写sql操作的;同样一个接口对应一个表

StudentDao接口:

package com.yang.dao;

import com.yang.domain.Student;

public interface StudentDao {
    // 查询方法
    List<Student> selectStudent(); // 查询结构返回一个Student的list集合
}

通俗点说就是:一个接口对应一个表的sql操作,接口中的一个方法对应执行一条sql语句;

这个包下同样还有mapper.xml文件,这个文件是用来定位接口中的对应方法,来写sql语句的,并且一个类型的接口对应一个mapper.xml文件

注意mapper.xml文件要和对应的接口名相同

StudentDao.xml

<?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">
<!--
结论:上面是固定格式,直接复制粘贴就可以;
sql映射文件:写sql语句,mybatis会执行;
mybatis-3-mapper.dtd是约束文件名称;
约束文件作用:限制、检查在当前文件中出现的标签、属性是否符合mybatis规范;
-->

<!--
mapper是当前文件的根标签;
namespace:命名标签,唯一值,可以是自定义的字符串,
但是这里要求使用dao接口的全限名称
-->
<mapper namespace="com.yang.dao.StudentDao">
    <!--
        mapper内部可以通过以下标签表示数据库的操作:
        <select>:查询
        <update>:更新
        <insert>:插入(新增)
        <delete>:删除
    -->
    <!--
        查询语句
        id:执行sql语法的唯一标识,mybatis会通过这个id找到要执行的sql语句,但是要写成对应dao接口的 方法名(动态代理的要求)
        resultType:表示结果类型,是sql执行后得到的ResultSet遍历得到的Java对象类型;
        值为该类型的权限名称
    -->
    <select id="selectStudent" resultType="com.yang.domain.Student">
        select * from student order by id
    </select>
</mapper>

这个文件的结构解释都写在注释中了,除了select标签其余部分就是一个固定格式,直接用就行;

现在再理一下为什么这个包要放接口和mapper.xml文件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dHHnXpV5-1642738633053)(C:\Users\86185\Desktop\笔记\Mybatis\Dao文件夹中各个部分对应关系.png)]

因为这里就有一个student表,如果想要对该表进行数据操作,就需要一个StudentDao接口

对表进行一个操作(增删改查)就要在接口中写一个抽象方法(操作名)

想要写StudentDao接口中对应操作的sql语句就需要一个和接口同名的mapper.xml文件:StudentDao.xml

最后在StudentDao.xml中写StudentDao接口中的抽象方法的sql语句(查询 、更新、新增、删除)

因为这一块不好描述清楚,所以我只能按照我的理解来描述,可能会有人疑惑为什么必须要写个StudentDao接口?写成抽象类不行吗?为什么id什么的必须相对应?这就涉及到了mybatis的底层实现了,这是动态代理所要求的,必须写接口,必须这样写,实在疑惑的话可以看看代理模式的相关内容,你就会明白为什么了;

补充:dao接口中的方法不能重载,一旦出现重名的操作后Mybatis就无法判断执行哪一个了;

resource包

先跳过utils包,先说说resource包;

这个包下也有一个很重要的xml文件:config.xml

这个文件是 MyBatis 主配置文件,在dao包中的所有mapper.xml文件都整合到该文件中,且该文件中配置对应的数据库信息;

实际开发可能会有多个数据库,所以最好把每个数据库信息单独写在一个properties文件中,由config.xml文件读取;

jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/数据库名?serverTimezone=UTC
jdbc.username=用户
jdbc.password=密码
jdbc.driver.online=com.mysql.cj.jdbc.Driver
jdbc.url.online=jdbc:mysql://localhost:3306/数据库名?serverTimezone=UTC
jdbc.username.online=用户
jdbc.password.online=密码

mybatis.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>

    <!--设置jdbc连接信息的配置文件jdbc.properties路径(该声明必须放在上面)-->
    <properties resource="jdbc.properties" />
    <!--settings:控制mybatis全局行为-->
    <settings>
        <!--设置mybatis输出日志-->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>

    <!--环境配置: 数据库的连接信息
        default:必须和某个environment的id值一样。
        告诉mybatis使用哪个数据库的连接信息。也就是访问哪个数据库
    -->
    <environments default="mydev">
        <!-- environment : 一个数据库信息的配置, 环境
             id:一个唯一值,自定义,表示环境的名称。
        -->
        <environment id="mydev">
            <!--
               transactionManager :mybatis的事务类型
                   type: JDBC(表示使用jdbc中的Connection对象的commit,rollback做事务处理)
            -->
            <transactionManager type="JDBC"/>
            <!--
               dataSource:表示数据源,连接数据库的
                  type:表示数据源的类型, POOLED表示使用连接池
            -->
            <dataSource type="POOLED">
                <!--
                   driver, user, username, password 是固定的,不能自定义。
                -->
                <!--数据库的驱动类名-->
                <property name="driver" value="${jdbc.driver}"/>
                <!--连接数据库的url字符串-->
                <property name="url" value="${jdbc.url}"/>
                <!--访问数据库的用户名-->
                <property name="username" value="${jdbc.username}"/>
                <!--密码-->
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>

        <!--下面这个environment标签内的内容是为了和上面对比写的,在这个例子中不需要的-->
        <!--表示线上的数据库,是项目真实使用的库-->
        <environment id="online">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver.online}"/>
                <property name="url" value="${jdbc.url.online}"/>
                <property name="username" value="${jdbc.username.online}"/>
                <property name="password" value="${jdbc.password.online}"/>
            </dataSource>
        </environment>
    </environments>

    <!-- sql mapper(sql映射文件)的位置-->
    <mappers>
        <!--一个mapper标签指定一个文件的位置。
           从类路径开始的路径信息。  target/clasess(类路径)
        -->
        <mapper resource="com/yang/dao/StudentDao.xml"/>
    </mappers>
</configuration>
<!--
   mybatis的主配置文件: 主要定义了数据库的配置信息, sql映射文件的位置

   1. 约束文件
   <!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

    mybatis-3-config.dtd:约束文件的名称

  2. configuration 根标签。
-->

mapper标签就是对应的dao包中的StudentDao.xml文件,dao包中有几个xml文件就写几个mapper标签;

设置输出日志

其中有一段代码:

<settings>
	<!--设置mybatis输出日志-->
	<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

这样在测试时可以输出日志信息,好处就是如果出现了错误可以从日志中很快的找出来;所以建议加上

设置properties文件

这里还要说一点,在xml文件中引入properties文件的操作中,首先要加一个:

<!--设置jdbc连接信息的配置文件jdbc.properties路径(该声明必须放在上面)-->
<properties resource="文件名.properties" />
<!--properties文件和xml文件要放在一起-->

然后在下面调用properties文件内容的时候value就是这样写:

value="${key值}"

记住就行;

小测试

做完这些就可以写一个测试代码测试了,我们写好了sql语句,只需要执行sql语句对应的StudentDao中的方法就可以了;

测试代码:

@Test
public void selectStudent() throws IOException {
    // 访问mybatis读取student数据
    // 1.定义mybatis主配置文件的名称, 从类路径的根开始(target/clasess)
    String config = "mybatis.xml";
    // 2.读取这个config表示的文件
    InputStream in = Resources.getResourceAsStream(config);
    // 3.创建了SqlSessionFactoryBuilder对象
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    // 4.创建SqlSessionFactory对象,build()方法
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(in);
    // 5.获取SqlSession对象,从SqlSessionFactory中获取SqlSession
    SqlSession session = sqlSessionFactory.openSession();
    // 6.使用mybatis的动态代理机制, 使用SqlSession.getMapper(dao接口)
    // getMapper能获取dao接口对于的实现类对象
    StudentDao studentDao = session.getMapper(StudentDao.class);
    // 7.执行sql语句(其实执行的是sql语句对应的方法)
    List<Student> students = studentDao.selectStudent();
    // 8.输出结果
    students.forEach(stu -> System.out.println(stu)); // Lambda表达式(直接遍历也行)
    // 9.关闭SqlSession对象
    session.close();
}

输出结果:

Student{id=1001, name='张三', email='zhangsan@123.com', age=18}
Student{id=1002, name='李四', email='lisi@123.com', age=28}
Student{id=1003, name='王五', email='wangwu@123.com', age=38}
Student{id=1006, name='小六', email='xiaoliu@123.com', age=66}
Student{id=1007, name='小七', email='xiaoqi@123.com', age=77}
Student{id=1008, name='小八', email='xiaoba@123.com', age=88}
Student{id=1009, name='九小', email='jiuxiao@123.com', age=99}

看到执行步骤这么多,是不是感觉还不如JDBC的那几句代码呢?

当实际开发中查询量大时使用这种方法还是方便的,毕竟还是省去好多代码并且对于sql语句的管理也更方便了;

简单介绍一下用到的几个方法:

Resources 类

Resources 类,顾名思义就是资源,用于读取资源文件。其有很多方法通过加载并解析资源文件,返回不同类型的 IO 流对象;

SqlSessionFactoryBuilder 类

SqlSessionFactory 的创建 ,需要使用 SqlSessionFactoryBuilder 对象的build()方法 ;由于 SqlSessionFactoryBuilder 对象在创建完工厂对象后,就完成了其历史使命,即可被销毁;所以,一般会将该 SqlSessionFactoryBuilder 对象创建为一个方法内的局部对象,方法结束,对象销毁;

SqlSessionFactory 接口

SqlSessionFactory 接口对象是一个重量级对象(系统开销大的对象),是线程安全的,所以一个应用只需要一个该对象即可(不要多次重复创建);创建 SqlSession 需要使用 SqlSessionFactory 接口的的 openSession()方法;

openSession(true):创建一个有自动提交功能的 SqlSession

openSession(false):创建一个非自动提交功能的 SqlSession,需手动提交

openSession():同 openSession(false)

SqlSession 接口

SqlSession 接口对象用于执行持久化操作;一个 SqlSession 对应着一次数据库会话,一次会话以 SqlSession 对象的创建开始,以 SqlSession 对象的关闭结束; SqlSession 接口对象是线程不安全的,所以每次数据库会话结束前,需要马上调用其 close()方法,将 其关闭;再次需要会话,再次创建;SqlSession 在方法内部创建,使用完毕后关闭;

SqlSession 的 getMapper(Class class)方法,可获取指定接口的实现类对象。该方法的参数为指定 Dao 接口类的 Class 值;

虽然说Mybatis帮我们省去了大部分步骤,但是每次都需要写这几句代码也是有点麻烦,不如直接把它们封装成一个工具类;

utils包

终于就剩最后一个包了,我们在这个包中来声明各种工具类,为了简化Mybatis执行代码步骤,我们封装一个MybatisUtil工具类;

MybatisUtil.java

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;

// 工具类,返回一个SqlSession对象
public abstract class MybatisUtil {
    private static SqlSessionFactory sqlSessionFactory = null;

    static {
        String config = "mybatis.xml";
        try {
            InputStream in = Resources.getResourceAsStream(config);
            // 创建SqlSessionFactory对象,且只创建一次,因为它开销大,将用于整个应用,
            // 放在静态代码块中只在类初始化中创建一次
            sqlSessionFactory =  new SqlSessionFactoryBuilder().build(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // 获取SqlSession对象
    public static SqlSession getSqlSession() {
        SqlSession sqlSession  = null;
        if(sqlSessionFactory != null){
            sqlSession = sqlSessionFactory.openSession();// 非自动提交事务
        }
        return sqlSession;
    }
}

这样通过这个工具类就可以获取到SqlSession对象了;工具类写成抽象类也是为了防止使用工具类创建对象

再来个小测试:

@Test
public void selectStudent() {
    // 直接获取到SqlSession对象
	SqlSession session = MybatisUtil.getSqlSession();
	StudentDao studentDao = session.getMapper(StudentDao.class);
	List<Student> students = studentDao.selectStudent();
	students.forEach(stu -> System.out.println(stu));
	session.close();
}

这样步骤就减少了很多了,多次执行sql语句时就不需要那么多步骤了;

补充

设置自动提交事务

因为这个例子是select查找语句,直接查找就可以了;但是如果是update、delete、insert的话就涉及事务的问题,在默认情况下事务提交是关闭的,所以在执行sql语句后想要在数据库中增添数据就要手动提交,就是多了一行代码,当执行完sql语句,在下面直接调用SqlSession对象的commit()即可;

注:这里省略前面的一些代码,主要想展示一下如何提交数据;

插入insert语句的测试:

@Test
public void insertStudent() {
    SqlSession session = MybatisUtil.getSqlSession();
    StudentDao studentDao = session.getMapper(StudentDao.class);
    int num = studentDao.insertStudent(new Student(1010, "小石" , "xiaoshi@123.com", 10));
    session.commit(); // 手动提交事务,就是想说的这一点
    session.close();
    System.out.println("新增了" + num + "条数据");
}

输出结果(包括日志内容):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ILI7jqwz-1642738633054)(C:\Users\86185\Desktop\笔记\Mybatis\commit设置自动提交.png)]


如果想要开始就设置自动提交事务呢?

这就需要改动MybatisUtil.java工具类,也就是改一处即可:

sqlSession = sqlSessionFactory.openSession(true); // 自动提交事务

SqlSessionFactory 的 openSession() 方法分为有参数和无参数的,无参数默认就是关闭事务提交的,而有参数的将参数设置为true就开启事务自动提交了;

在测试代码中就不再需要写session.commit() 手动提交事务了;

输出结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YdGwWW3d-1642738633054)(C:\Users\86185\Desktop\笔记\Mybatis\设置自动事务提交输出结果.png)]

package 一次配置所有mapper.xml

如果存在多个表时,dao包下就会有多个mapper.xml文件,那么主配置文件config.xml配置起来就很麻烦,这时我们可以使用package指定dao包下的所有mapper文件,一次就完成了所有mapper文件的配置;

语法: <package name=“对应包的路径”>

示例:

mybatis.xml

<!-- sql mapper(sql映射文件)的位置-->
<mappers>  
	<!--<mapper resource="com/yang/dao/StudentDao.xml"/>-->
    
	<!--用package代替mapper,表示dao包下的所有mapper.xml文件-->
	<package name="com.yang.dao"/>
</mappers>

注意:该方法要求 Dao 接口名称和 mapper 映射文件名称相同,且在同一个目录中;

三、MyBatis 传递参数

如果我们想要实现输入数据执行查询到结果,又或者输入一些数据实现在数据库的新增…这种情况下sql语句就不能写死,如何把传入java代码中的参数传给sql语句呢?下面就简单描述一下方法;

一个参数

当传入的是一个参数时,直接传给sql语句即可,注意sql语句中传入的变量为:#{参数名}

StudentDao.java

public interface StudentDao {
    // 查询方法,从数据库中通过id查找对应学生
   Student selectStudent03(int id); // 一个参数查询,返回值是Student类型
}

mapper文件:

StudentDao.xml

<?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.yang.dao.StudentDao">
    <!--一个参数,#{id}就是传入的参数-->
    <select id="selectStudent03" resultType="com.yang.domain.Student">
        select * from student where id = #{id}
    </select>
</mapper>

config文件:

mybatis.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>

    <!--设置jdbc连接信息的配置文件jdbc.properties路径(该声明必须放在上面)-->
    <properties resource="jdbc.properties" />
    <!--settings:控制mybatis全局行为-->
    <settings>
        <!--设置mybatis输出日志-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <environments default="test">
        <environment id="test">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!--数据库的驱动类名-->
                <property name="driver" value="${jdbc.driver}"/>
                <!--连接数据库的url字符串-->
                <property name="url" value="${jdbc.url}"/>
                <!--访问数据库的用户名-->
                <property name="username" value="${jdbc.username}"/>
                <!--密码-->
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>

    </environments>

    <!-- sql mapper(sql映射文件)的位置-->
    <mappers>
        <mapper resource="com/yang/dao/StudentDao.xml"/>
    </mappers>
</configuration>

jdbc.properties略;

测试代码:

@Test 
public void selectStudent07() {
    SqlSession session = MybatisUtil.getSqlSession();
    StudentDao studentDao = session.getMapper(StudentDao.class);
    Student student = studentDao.selectStudent03(1003); // 从数据库中查找id为1003的学生
    session.close();
    System.out.println(student);
}

输出结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P9E2xlU3-1642738633055)(C:\Users\86185\Desktop\笔记\Mybatis\传入一个参数输出结果.png)]

通过输出结果以及打印的日志信息可以看到整个sql语句的执行过程;

传递多参数

传递多参数就麻烦一些了,但是并不难,因为这篇文章我已经写过了,直接开个传送门过去吧

传送门

#和$

在参数传递时经常看到 ‘ # ’ 符号,其实还有一个 ‘ $ ’ 符号,下面来对比一下这两种符号代表的意思;

#:占位符,告诉 mybatis 使用实际的参数值代替;并使用 PrepareStatement 对象执行 sql 语句, #{…} 代替 sql 语句的占位符“?”。这样做更安全(可以防止sql注入),更迅速,通常也是首选做法

$:字符串替换,告诉 mybatis 使用 $ 包含的“字符串”替换所在位置;使用 Statement 把 sql 语句${…}的 内容连接起来(就是字符串拼接,直接把sql语句和传入的内容拼起来)。主要用在替换表名,列名,不同列排序等操作;但是Statement存在Sql注入问题,所以一般不建议使用;(并不是没用,分页查询还是用得到的)


#和$的区别:

  1. #使用 ’ ? ’ 在sql语句中做占位符, 使用PreparedStatement执行sql,效率高
  2. #能够避免sql注入,更安全。
  3. $不使用占位符,是字符串连接方式,使用Statement对象执行sql,效率低
  4. $有sql注入的风险,缺乏安全性。
  5. $:可以替换表名或者列名

四、属性名和字段名不同解决方法

resultMap

前面我说过尽可能保证domain包下的Student类的属性名和student表中的字段名相同,但是在实际开发中总会存在例外,如果出现了属性名和字段名不同的情况,同样也有解决办法,就是使用resultMap元素

resultMap 可以自定义 sql 的结果java 对象属性的映射关系,更灵活的把列值赋值给指定属性;常用在列名和 java 对象属性名不一样的情况

使用方式:

1.先定义 resultMap,指定列名和属性的对应关系;

2.在中把 resultType 替换为 resultMap;

代码演示

将domain中的Student类的属性稍作修改:

public class Student {
    // 这次的属性名和表的字段名不同
    private int myId;
    private String myName;
    private String myEmail;
    private int myAge;
    // 下面还是相同的一系列get、set操作,就不写了
}

mapper.xml文件:

StudentDoa.xml

<?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.yang.dao.StudentDao">
	<!--
	当数据库表的列名和java程序的属性名 不一样 时记住要用resultMap,
	id值就是整个resultMap的自定义的值,用来和下面的查询语句匹配的
	type就是resultMap匹配的类型
	-->
	<resultMap id="studentMap" type="com.yang.domain.Student">
		<!--column: 是数据库中表的列名 property:在这里就是Student类中的属性名-->
        <!-- 主键字段使用 id标签 -->
		<id column="id" property="myId" />
        <!--非主键字段使用 result标签-->
		<result column="name" property="myName" />
		<result column="email" property="myEmail" />
		<result column="age" property="myAge" />
	</resultMap>
    <!--select标签中的 resultMap="studentMap"就是对应id的resultMap-->
	<select id="selectStudent02" resultMap="studentMap">
		select * from student where id > #{myId}
	</select>
</mapper>

其他剩余操作就不演示了,和前面都一样,这里主要想强调的还是 <resultMap> 标签,通过它来把不同的属性名和字段名联系起来;

resultType

说到resultMap了也顺带聊聊resultType,其实上面第一个示例也介绍过了,这里再提一下;

resultType: 执行 sql 得到 ResultSet 转换的类型,使用类型的完全限定名或别名; 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身;

要注意resultType 和 resultMap不能同时使用;

五、Like模糊查询

因为sql中的like模糊查询需要加 ‘ %’ 号,所以就在这里说一下两种实现:

  1. 在mapper文件的sql语句中写 ‘ %’ 号
  2. 在java代码中传入一个带 ‘ %’ 号的字符串给sql语句

下面简单演示一下:

在sql语句中写 ‘ %’ 号

StudentDao.java

List<Student> selectStudentByLike(@Param("likeName") String name); // Like测试

StudentDao.xml

<?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.yang.dao.StudentDao">
    <!--在sql语句中写%-->
	<select id="selectStudentByLike" resultType="com.yang.domain.Student">
        select * from student where name like %#{likeName}%
    </select>
</mapper>

测试代码:

@Test
public void selectStudent03() {
    SqlSession session = MybatisUtil.getSqlSession();
    StudentDao studentDao = session.getMapper(StudentDao.class);
    // like模糊查询,传入查询内容
    List<Student> students = studentDao.selectStudentByLike("小");
    session.close();
    students.forEach(stu -> System.out.println(stu));
}

这种方法其实并不建议,因为模糊查询可以分为三种查询方式:%内容、%内容%、内容%

所以这样写你就无法自己想怎么差就怎么查了,想换种查询方法还得修改sql语句;所以不建议使用该方法

java代码传入“%查询内容%”

这种方法的灵活度就高了,因为java代码传入的就是查询方式和内容,下面演示一下:

StudentDao.java

List<Student> selectStudentByLike(@Param("likeName") String name); // Like测试

StudentDao.xml

<?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.yang.dao.StudentDao">
    <!--sql语句直接传一个字符串-->
	<select id="selectStudentByLike" resultType="com.yang.domain.Student">
        select * from student where name like #{likeName}
    </select>
</mapper>

测试代码:

@Test
public void selectStudent03() {
    SqlSession session = MybatisUtil.getSqlSession();
    StudentDao studentDao = session.getMapper(StudentDao.class);
    // like模糊查询,直接传入拼接的字符串,这样的灵活度更高
    List<Student> students = studentDao.selectStudentByLike("%小%"); 
    session.close();
    students.forEach(stu -> System.out.println(stu));
}

使用这种方法就可以了,上面第一种方法了解一下就行;

六、动态 SQL

官方定义

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。

下面介绍四个常用的标签:

  • if
  • where
  • set
  • foreach

<if>标签

语法: <if test=“条件”> sql 语句 <if>

规则: 如果test中的条件为true时,才会把 if 标签中的sql语句拼接到前面的sql语句上;

使用示例:

StudendDao.java

List<Student> selectStudentIf(Student student); // If标签

StudentDao.xml

<!--if标签-->
<select id="selectStudentIf" resultType="com.yang.domain.Student">
	select * from student where id > 0
	<if test="name != null and name != '' ">
    	and id > #{id}
	</if>
	<if test="age >= 0">
    	and age > #{age}
	</if>
</select>

简单解释一下,如果"name != null and name != ''成立,那么sql语句就是:select * from student where id > 0 and id > #{id}

如果age >= 0也成立,sql语句为:select * from student where id > 0 and id > #{id} and age > #{age}

如果只有age >= 0成立,那么sql语句为:select * from student where id > 0 and age > #{age}

就是满足条件就拼接,不满足就不管;

测试代码:

@Test
public void selectStudent04() {
    SqlSession session = MybatisUtil.getSqlSession();
    StudentDao studentDao = session.getMapper(StudentDao.class);
    List<Student> students = studentDao.selectStudentIf(new Student(1006, "小六" , "xiaoliu@123.com", 66));
    session.close();
    students.forEach(stu -> System.out.println(stu));
}

输出结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rWr4XAOo-1642738633055)(C:\Users\86185\Desktop\笔记\Mybatis\if标签输出.png)]
可以看到if标签中所有条件都满足,所以都拼接上了;

<where>标签

if 标签存在一个问题:你可以发现在上面的 if 标签例子中,where语句后面开始就加了一个 id>0,为什么呢?假设没有这段sql语句,那么开始时我的sql语句就是:select * from student where;第一个条件满足还好说,拼接后就是:select * from student where id > #{id},那么如果第一个条件不满足第二个条件满足呢?拼接后就成了:select * from student where and age > #{age} ,你就会发现这个sql语句是错误的,where后面怎么能直接跟一个and;如果两个条件都不满足那么sql语句不就成了:select * from student where这样了吗?

这样就会造成sql语法的错误,所以我在前面加上了一个不论何时都满足的条件:id>0

但是这并不是最优方法,如果查询数据量大时这样很影响效率,这个时候where标签就可以解决这个问题;


where标签语法: <where> 其他动态 sql语句(比如if语句) </where>

规则: where 标签只会在子标签返回任何内容的情况下才插入 “WHERE” 子句;而且,若子句的开头为 “AND” 或 “OR”,where 标签也会将它们去除;

使用示例:

StudendDao.java

List<Student> selectStudentWhere(Student student); // Where标签

StudentDao.xml

<!--where标签-->
<select id="selectStudentWhere" resultType="com.yang.domain.Student">
	select * from student
	<where>
		<if test="name != null and name != '' ">
			id > #{id}
		</if>
		<if test="age >= 0">
			and age > #{age}
		</if>
	</where>
</select>

和if标签都一个意思,但是这里sql语句中的where就不用我们自己写了,如果where标签中有满足条件的就拼接上,都不满足where语句就不会存在;

测试代码:

@Test
public void selectStudent05() {
    SqlSession session = MybatisUtil.getSqlSession();
    StudentDao studentDao = session.getMapper(StudentDao.class);
    List<Student> students = studentDao.selectStudentWhere(new Student(1006, "小六" , "xiaoliu@123.com", 66));
    session.close();
    students.forEach(stu -> System.out.println(stu));
}

输出结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZzY1sco-1642738633056)(C:\Users\86185\Desktop\笔记\Mybatis\where标签输出.png)]
对于where标签和if标签的选择应该根据实际情况,知道用法灵活变通;

<set>标签

set标签和where标签作用差不多,但是set标签就会用在update语句中,set 标签可以用于动态包含需要更新的列,忽略其它不更新的列;

语法:<set>动态sql语句</set>

规则: set标签也是只有子标签返回任何内容时才拼接,就是和where标签使用的位置不同罢了;(但是并不意味着update不能使用where标签了)

使用示例:

StudendDao.java

int updateStudent02(Student student); // set标签

StudentDao.xml

<!--set标签专门用于更新语句的,和where标签的作用相似,只是使用位置不同-->
<update id="updateStudent02">
    update student
    <set>
        <if test="id != null">id = #{id},</if>
        <if test="name != null">name = #{name},</if>
        <if test="email != null">email = #{email},</if>
        <if test="age != null">age = #{age}</if>
    </set>
    where id = #{id}
</update>

测试代码:

@Test
public void updateStudent02() {
    SqlSession session = MybatisUtil.getSqlSession();
    StudentDao studentDao = session.getMapper(StudentDao.class);
    int num = studentDao.updateStudent02(new Student(1009, "小九" , "xiaojiu@123.com", 9));
    session.commit(); // 提交事务(默认是关闭的,所以需要手动提交)
    session.close();
    System.out.println("更新了" + num + "条数据");
}

输出结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tixe9Cwh-1642738633056)(C:\Users\86185\Desktop\笔记\Mybatis\set标签输出.png)]

<foreach>标签

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候),这时候就可以使用foreach标签;

语法:

<foreach collection=“集合类型” item=“集合中的成员” open=“开始字符” close=“结束字符” separator=“集合成员间的分隔符”>

#{item值}

</foreach>

规则: 可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach;当使用 Map 对象(或者 Map.Entry 对象的集合)时,需要在foreach中额外加一个index属性,index 是键,item 是值;

使用示例:

StudendDao.java

List<Student> selectStudentFor(List<Student> student); // foreach标签

StudentDao.xml

<!--foreach标签-->
<!--
当存在一个集合时查找该集合元素可以使用
collection:所要查找的集合类型
item:对应方法中形参的名字(即接口中方法形参名:List<Student> student)
open:循环开始前家的东西
close:循环结束后加的东西
separator:集合中每一个元素的分隔符
-->
<!--注意这里sql语句后面使用了in()-->
<select id="selectStudentFor" resultType="com.yang.domain.Student">
	select * from student where id in
	<foreach collection="list" item="student" open="(" close=")" separator=",">
    	#{student.id}
	</foreach>
</select>

其实这个sql语句可以简单抽象成这样:

select * from student where id in (
	#{student.id}
)
<!--因为student是一个list集合,所以通过for循环每次从中遍历id值,每生成一个id后面再加一个分隔符‘,’-->

所以最后sql语句可以拼接成:select * from student where id in (student.id01, student.id02, student.id03.....)

测试代码:

@Test
public void selectStudent06() {
    SqlSession session = MybatisUtil.getSqlSession();
    StudentDao studentDao = session.getMapper(StudentDao.class);
    List<Student> student = new ArrayList<>();
    student.add(new Student(1006, "小六" , "xiaoliu@123.com", 66));
    student.add(new Student(1007, "小七" , "xiaoqi@123.com", 77));
    student.add(new Student(1009, "小九" , "xiaojiu@123.com", 9));
    List<Student> students = studentDao.selectStudentFor(student);
    session.close();
    students.forEach(stu -> System.out.println(stu));
}

输出结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZSGSlFtk-1642738633056)(C:\Users\86185\Desktop\笔记\Mybatis\foreach标签输出.png)]
array、set集合和list都是一样的,map就多了一个index属性这一点区别,这里就不示范了;

七、小拓展

使用PageHelper实现分页功能

项目开发中分页功能几乎是每次都会出现的,一般我们会通过limit进行操作:

select * from 表名 limit (pageNum - 1)*pageSize, pageSize

pageNum是当前的页数,pageSize是每页显示的记录条数,这个公式可以自己找找规律推导一下;

但是这只是关键步骤,实际情况下还是很麻烦,所以就有牛人写了一个mybatis分页插件,PageHelper;

这个插件在GitHub上可以搜到:传送门

引入jar包

可以直接使用Maven依赖来引入jar包:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>最新版本</version>
</dependency>
添加plugin 配置

在config.xml文件中还需要添加额外的配置,需要加到<environments>标签之前;

mybatis.xml

<!--添加PageHelper的配置-->
<plugins>
	<plugin interceptor="com.github.pagehelper.PageInterceptor" />
</plugins>
配置文件

mapper文件:

StudentDao.xml

<?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.yang.dao.StudentDao">
    <select id="selectStudent" resultType="com.yang.domain.Student">
        select * from student
    </select>
</mapper>

config文件

mybatis.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>

    <!--配置properties文件-->
    <properties resource="jdbc.properties"/>
    
    <!--settings:控制mybatis全局行为-->
    <settings>
        <!--设置mybatis输出日志-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <!--添加PageHelper的配置-->
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor" />
    </plugins>
    
    <environments default="test">
        <environment id="test">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!--数据库的驱动类名-->
                <property name="driver" value="${jdbc.driver}"/>
                <!--连接数据库的url字符串-->
                <property name="url" value="${jdbc.url}"/>
                <!--访问数据库的用户名-->
                <property name="username" value="${jdbc.username}"/>
                <!--密码-->
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>

    </environments>
    <!-- sql mapper(sql映射文件)的位置-->
    <mappers>
<!--        <mapper resource="com/yang/dao/StudentDao.xml"/>-->

        <!--用package代替mapper,表示dao包下的所有mapper.xml文件-->
        <package name="com.yang.dao"/>
    </mappers>
</configuration>

数据库配置文件:

jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm_learning?serverTimezone=UTC
jdbc.username=root
jdbc.password=020216
查询测试

完成基本的配置后,实现分页查询只需要在需要进行分页的 MyBatis 查询方法前调用 PageHelper.startPage() 静态方法即可,紧跟在这个方法后的第一个 MyBatis 查询方法会被进行分页;

测试代码:

@Test
public void selectTest01() {
    SqlSession session = MybatisUtil.getSqlSession();
    StudentDao studentDao = session.getMapper(StudentDao.class);
    // pageNum:第几页(从1开始)
    // pageSize:页几行数据
    // 表示第一页,一页三行数据
    PageHelper.startPage(1, 3); // 在执行下面的查询方法selectStudent()之前调用
    List<Student> list = studentDao.selectStudent();
    list.forEach(stu-> System.out.println(stu));
}

输出结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XToh1PFr-1642738633057)(C:\Users\86185\Desktop\笔记\Mybatis\PageHelper.png)]
这就输出了第一页的三行内容,如果想看第二页的内容,稍改代码:

PageHelper.startPage(2, 3); // 第二页的三行数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q9vRJIR3-1642738633057)(C:\Users\86185\Desktop\笔记\Mybatis\PageHelper第二页输出.png)]
非常方便简单;并且PageHelper支持多种数据库,绝对够用的;

八、总结

第一次接触框架,配置挺多的,也不知道理解有没有偏差,或者语言描述有问题,如果内容哪里有问题也欢迎指正!

这个笔记只是记录了一些比较重要的部分,Mybatis并不是仅仅这些内容,后期如果遇到新的内容会再补充上去;

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

YXXYX

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

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

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

打赏作者

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

抵扣说明:

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

余额充值