为什么mybatis更新操作是添加操作_MyBatis常规操作

1. 什么是 MyBatis?

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

2. 快速入门

创建Maven工程

目录结构

aedc4f6171a478b8ff65d8d4c946d311.png

pom.xml

<?xml  version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0modelVersion>    <groupId>top.capiudorgroupId>    <artifactId>mybatis-demoartifactId>    <version>1.0-SNAPSHOTversion>    <packaging>jarpackaging>    <dependencies>                <dependency>            <groupId>org.mybatisgroupId>            <artifactId>mybatisartifactId>            <version>3.4.5version>        dependency>                <dependency>            <groupId>mysqlgroupId>            <artifactId>mysql-connector-javaartifactId>            <version>5.1.6version>        dependency>                <dependency>            <groupId>log4jgroupId>            <artifactId>log4jartifactId>            <version>1.2.12version>        dependency>                <dependency>            <groupId>junitgroupId>            <artifactId>junitartifactId>            <version>4.10version>        dependency>    dependencies>project>

src/main/java/top/capiudor/dao/IUserDao.java

package top.capiudor.dao;import top.capiudor.domain.User;import java.util.List;/** * 用户的持久层接口 */public interface IUserDao {    /**     * 查询所有用户     * @return     */    List<User> findAll();}

src/main/java/top/capiudor/domain/User.java

package top.capiudor.domain;import java.io.Serializable;import java.util.Date;public class User implements Serializable {    private Integer id;    private String username;    private Date birthday;    private Character sex;    private String 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 Character getSex() {        return sex;    }    public void setSex(Character sex) {        this.sex = sex;    }    public String getAddress() {        return address;    }    public void setAddress(String address) {        this.address = address;    }    @Override    public String toString() {        return "User{" +                "id=" + id +                ", username='" + username + '\'' +                ", birthday=" + birthday +                ", sex=" + sex +                ", address='" + address + '\'' +                '}';    }}

src/main/resources/SqlMapConfig.xml

<?xml  version="1.0" encoding="UTF-8" ?>/span>        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"        "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration>        <environments default="mysql">                <environment id="mysql">                        <transactionManager type="JDBC"/>                        <dataSource type="POOLED">                                <property name="driver" value="com.mysql.jdbc.Driver"/>                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo"/>                <property name="username" value="root"/>                <property name="password" value="root"/>            dataSource>        environment>    environments>        <mappers>        <mapper resource="top/capiudor/dao/IUserDao.xml"/>    mappers>configuration>

src/main/resources/log4j.properties

# Set root category priority to INFO and its only appender to CONSOLE.#log4j.rootCategory=INFO, CONSOLE            debug   info   warn error fatallog4j.rootCategory=debug, CONSOLE, LOGFILE# Set the enterprise logger category to FATAL and its only appender to CONSOLE.log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE# CONSOLE is set to be a ConsoleAppender using a PatternLayout.log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppenderlog4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayoutlog4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n# LOGFILE is set to be a File appender using a PatternLayout.log4j.appender.LOGFILE=org.apache.log4j.FileAppenderlog4j.appender.LOGFILE.File=d:\\axis.loglog4j.appender.LOGFILE.Append=truelog4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayoutlog4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

src/main/resources/top/capiudor/dao/IUserDao.xml

<?xml  version="1.0" encoding="UTF-8" ?>/span>        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"        "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="top.capiudor.dao.IUserDao">        <select id="findAll" resultType="top.capiudor.domain.User">        select * from user    select>mapper>

到这里你可以使用MyBatis做一次最简单的查询。

3. Mybatis单表CRUD(基于XML)

单表的CRUD是很简单的,主要是注意几个点就可以了:

1. Mybatis-config.xml中的mappers -> mapper映射配置

2. XxxMapper.xmlmappernamespace 属性,需要和XxxMapper.java接口 全限定类名一致

3. XxxMapper.xml、、、id 属性需要与接口的方法名一致

4. 接口的方法有多个参数,XML中对应的参数需要指定

5. XML映射文件中配置ResultMap需要注意类型、一对一 和 一对多

3.1 准备工作

准备一个表:

-- 来自Mybatis-plus 的样表DROP TABLE IF EXISTS user;CREATE TABLE user(    id BIGINT(20) NOT NULL COMMENT '主键ID',    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',    age INT(11) NULL DEFAULT NULL COMMENT '年龄',    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',    PRIMARY KEY (id));

准备数据:

DELETE FROM user;INSERT INTO user (id, name, age, email) VALUES(1, 'Jone', 18, 'test1@baomidou.com'),(2, 'Jack', 20, 'test2@baomidou.com'),(3, 'Tom', 28, 'test3@baomidou.com'),(4, 'Sandy', 21, 'test4@baomidou.com'),(5, 'Billie', 24, 'test5@baomidou.com');

突然想起,Mybatis-Plus 没有提供自增主键。。。自己操作一下吧

ALTER TABLE mybatis_crud.user MODIFY COLUMN id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID' FIRST;

准备IDEA环境,参照  快速入门

工程结构:

08423c3763cfb9d8ccf9a136f69ca151.png

项目Maven依赖:

de7a00cb210bc0b085db6b261515c8a3.png

必要的对象:

src/main/java/top/capiudor/pojo/User.java

package top.capiudor.pojo;import java.io.Serializable;/** * 用户类 */public class User implements Serializable {    private Integer id;    private String name;    private Integer age;    private String email;    public Integer getId() {        return id;    }    public void setId(Integer id) {        this.id = id;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public Integer getAge() {        return age;    }    public void setAge(Integer age) {        this.age = age;    }    public String getEmail() {        return email;    }    public void setEmail(String email) {        this.email = email;    }    @Override    public String toString() {        return "User{" +                "id=" + id +                ", name='" + name + '\'' +                ", age=" + age +                ", email='" + email + '\'' +                '}';    }}

src/main/java/top/capiudor/dao/UserMapper.java

package top.capiudor.dao;/** * 用户接口 */public interface UserMapper {}

src/main/resources/top/capiudor/dao/UserMapper.xml

<?xml  version="1.0" encoding="UTF-8" ?>/span>        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"        "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="top.capiudor.dao.UserMapper">    mapper>

3.2 保存用户

UserMapper.java

    /**    * 保存用户    * @param user    * @return    * @throws Exception    */    Integer saveUser(User user) throws Exception;

UserMapp.xml

        <insert id="saveUser" parameterType="user" >                <selectKey order="AFTER" keyColumn="id" keyProperty="id" resultType="int">            select last_insert_id()        selectKey>        insert into user(name,age,email) values (#{name},#{age},#{email})    insert>

3.3 修改用户

UserMapper.java

    /**     * 修改用户     * @param user     * @return     * @throws Exception     */    Integer updateUser(User user) throws Exception;

UserMapper.xml

        <update id="updateUser" parameterType="user">        update user set name=#{name},age=#{age},email=#{email} where id=#{id}    update>

3.4 删除用户

UserMapper.java

    /**     * 根据用户id删除用户     * @param id     * @return     * @throws Exception     */    Integer deleteUserById(Integer id) throws Exception;

UserMapper.xml

        <delete id="deleteUserById" parameterType="java.lang.Integer">        delete from user where id=#{id}    delete>

3.5 查询用户

3.5.1 查询所有

UserMapper.java

    /**     * 查询所有用户     * @return     * @throws Exception     */    List<User> findAll() throws Exception;

UserMapper.xml

        <cache eviction="FIFO" />        <resultMap id="baseMap" type="user">        <id property="id" column="id"/>        <result property="name" column="name" />        <result property="age" column="age" />        <result property="email" column="email" />    resultMap>        <sql id="baseSql">select * from user sql>        <select id="findAll" resultMap="baseMap">                <include refid="baseSql">include>    select>
3.5.2 模糊查询

UserMapper.java

	/**
* 根据用户名模糊查询用户信息
* @param name
* @return
* @throws Exception
*/
List findByName(String name) throws Exception;

UserMapper.xml

	
where name like #{name}
3.5.3 根据主键查询

UserMapper.java

	/**
* 根据用户ID查询用户
* @param id
* @return
* @throws Exception
*/
User findById(Integer id) throws Exception;

UserMapper.xml

	
where id = #{id}
3.5.4 查询函数

UserMapper.java

    /**     * 查询总记录数     * @return     */    int findTotal() throws Exception;

UserMapper.xml

        <select id="findTotal" resultType="int" >        select count(id) from user    select>
3.5.5 动态查询

UserMapper.java

    /**     * 根据QueryVo查询用户信息     * @return     * @throws Exception     */    List<User> findByCondition(QueryVo vo) throws Exception;

src/main/java/top/capiudor/pojo/QueryVo.java

package top.capiudor.pojo;import java.io.Serializable;import java.util.List;/** * 条件查询类 */public class QueryVo implements Serializable {    private List<Integer> ids;    private User user;    public List<Integer> getIds() {        return ids;    }    public void setIds(List<Integer> ids) {        this.ids = ids;    }    public User getUser() {        return user;    }    public void setUser(User user) {        this.user = user;    }    @Override    public String toString() {        return "QueryVo{" +                "ids=" + ids +                ", user=" + user +                '}';    }}

UserMapper.xml

<select id="findByCondition" resultMap="baseMap" parameterType="queryVo">    <include refid="baseSql">include>    <where>                <if test="user != null and user.name != null">            name like #{user.name}        if>        <if test="ids != null and ids.size() > 0">                        <foreach collection="ids" item="targetId" open=" or id in (" separator="," close=")">                #{targetId}            foreach>        if>    where>select>

3.6 Junit测试一波

src/test/java/top/capiudor/test/UserTest.java

package top.capiudor.test;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.After;import org.junit.Before;import org.junit.Test;import top.capiudor.dao.UserMapper;import top.capiudor.pojo.QueryVo;import top.capiudor.pojo.User;import java.io.InputStream;import java.util.ArrayList;import java.util.List;/** * 用户测试类 */public class UserTest {    /**     * 抽取公用部分的内容     */    private InputStream in;    private SqlSession sqlSession;    private UserMapper userMapper;        /**     * 初始化     * @throws Exception     */    @Before    public void init() throws Exception{        in = Resources.getResourceAsStream("Mybatis-config.xml");        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);        sqlSession = factory.openSession(true);        userMapper = sqlSession.getMapper(UserMapper.class);    }    /**     * 释放资源     * @throws Exception     */    @After    public void destroy() throws Exception{        sqlSession.commit();        in.close();    }    /**     * 查询所有测试类     * @throws Exception     */    @Test    public void testFindAll() throws Exception{        userMapper.findAll().forEach(user -> System.out.println(user));        userMapper.findAll().forEach(user -> System.out.println(user));    }    /**     * 根据名称模糊查询     * @throws Exception     */    @Test    public void testFindByName() throws Exception{        userMapper.findByName("%a%").forEach(user -> System.out.println(user));        userMapper.findByName("%a%").forEach(user -> System.out.println(user));    }    /**     * 根据ID查询     * @throws Exception     */    @Test    public void testFindById() throws Exception{        System.out.println(userMapper.findById(1));        System.out.println(userMapper.findById(1));    }    /**     * 保存用户,并返回自动生成的主键     * @throws Exception     */    @Test    public void testSaveUser() throws Exception{        User user = new User();        user.setName("ji");        user.setAge(31);        user.setEmail("ji@163.com");        System.out.println(user);        userMapper.saveUser(user);        System.out.println(user);    }    /**     * 修改用户     * @throws Exception     */    @Test    public void testUpdateUser() throws Exception{        User user = userMapper.findById(7);        System.out.println(user);        user.setName("saoji");        userMapper.updateUser(user);        System.out.println(user);    }    /**     * 根据主键删除用户     * @throws Exception     */    @Test    public void testDeleteById() throws Exception{        userMapper.deleteUserById(7);    }    /**     * 根据QueryVo 进行动态查询     * @throws Exception     */    @Test    public void testFindByCondition() throws Exception{        List<Integer> ids = new ArrayList<>();        ids.add(1);        ids.add(2);        ids.add(3);        ids.add(4);        ids.add(5);        QueryVo vo = new QueryVo();        vo.setIds(ids);        userMapper.findByCondition(vo).forEach(u -> System.out.println(u));    }    /**     * 通过mysql内置函数返回总记录数     * @throws Exception     */    @Test    public void testFindTotal() throws Exception{        System.out.println(userMapper.findTotal());    }}

多次重复打印,是因为测试缓存是否生效,观察日志中会有一段 Cache Hit Ratio [top.capiudor.dao.UserMapper]: 0.0 ,这其实就是开启了MyBatis的缓存

4. Mybatis缓存

Hibernate提供了缓存机制,别人都有的,咱们也得有,所以Mybatis也提供了缓存的机制。

缓存的作用就是减少与数据库的磁盘IO,从而提高查询效率,减少查询时间,提高用户体验,但是缺陷就是数据是临时的,一断电就废了。

Mybatis提供了一级缓存、二级缓存。

提到缓存就不得不讲一下CPU的高速缓存,咱们计算机的存储介质大概分为(速度从大到小):CPU > 寄存器 > L1 > L2 > L3 > 内存(内存条) > 硬盘。CPU的速度是贼快的,到现在已经能达到4.6GHz的频率,寄存器是在CPU内部的一块内存空间,速度较之CPU略低,主要协助CPU完成任务,L1,L2,L3中的L代表的是CPU高速缓存器,是介于CPU和内存之间的纽带,你可以想象成一个高速公路,上面有三根道路,L1是最左边的那个道(120马),L2是中间的那根道(80~100马),L3是最右边的那根道(60~80马),程序运行从硬盘加载程序,然后丢给内存,内存交给CPU,由于CPU太快了,等内存把这些操作通知CPU的时候,CPU都睡了好几个时钟周期了,所以有了L1,L2,L3来作为一个流水线来给CPU找事情干,充分利用资源。

4.1 一级缓存

一级缓存: 官方称为本地缓存(local cache),无需配置。它指的是Mybatis中的SqlSession对象的缓存。

流程: 我们执行查询之前,Mybatis的SqlSession根据我们查询的路径去查缓存有没有,缓存一般启动的时候就自行创建了,SqlSession在内存中创建了一个Map,执行查询的时候,组装Key:top.capiudor.dao.UserMapper.findById(1),然后查询结果,将查询结果封装成对象Object,Put进map中,完成缓存,当我们再次查询top.capiudor.dao.UserMapper.findById(1)的时候,SqlSession会优先去查询map中是否包含这个Key,如果有就直接从缓存中提取对象。当SqlSession对象消失的时候,那么一级缓存失效。

清除缓存: SqlSession对象调用  close()clearCache()

触发清空一级缓存: 当调用SqlSession的修改、删除、添加、Commit()语句时,会清空一级缓存。

但是说实话、这个Mybatis的一级缓存意义真的不大、在整合了Spring之后,它的缓存就失效了详情可以关注以下这篇博客

4.2 二级缓存

二级缓存:需要配置。二级缓存是指Mybatis中的SqlSessionFactory对象的缓存。被同一个SqlSessionFactory对象创建出来的SqlSession对象共享SqlSessionFactory对象的缓存。

二级缓存中的内部数据不是对象,而是类似JSON一样的东西,当查询到的时候将数据组装成新的对象返回给调用者。

Mybatis-config.xml中配置

XML方式:

UserMapper.xml中配置

Annotation方式:

UserMapper.java中配置

@CacheNamespace(blocking=true)
public interface UserMapper { }

5. 多表查询(XML)

5.1  一对一

在Mybatis中,我们体现一下 一对一

准备一张表:

-- 假设有几个账户、一个用户对应多个账户、一个账户只属于一个用户
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`(
id int(11) not null auto_increment primary key,
uid int(11) not null,
money double not null
);

准备数据:

DELETE FROM account;INSERT INTO account ( uid, money )VALUES    ( 1, 10000 ),(        1,        20000         ),(        2,        22222         ),(        3,        33333         ),(        3,    76383 );

创建对应的 POJO:

src/main/java/top/capiudor/pojo/Account.java

package top.capiudor.pojo;import java.io.Serializable;/** * 账户类  多的一端 */public class Account implements Serializable {    private Integer id;    private Integer uid;    private Double money;    /**     * 此处将对象引入  对应uid     * */    private User user;    public Integer getId() {        return id;    }    public void setId(Integer id) {        this.id = id;    }    public Integer getUid() {        return uid;    }    public void setUid(Integer uid) {        this.uid = uid;    }    public Double getMoney() {        return money;    }    public void setMoney(Double money) {        this.money = money;    }    public User getUser() {        return user;    }    public void setUser(User user) {        this.user = user;    }    @Override    public String toString() {        return "Account{" +                "id=" + id +                ", uid=" + uid +                ", money=" + money +                ", user=" + user +                '}';    }}

src/main/resources/top/capiudor/dao/AccountMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
br /> PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
select * from account

添加了AccountMapper.xml记得将这个映射配置进Mybatis-config.xmlmappers属性中。

测试一下:

src/test/java/top/capiudor/test/AccountTest.java

package top.capiudor.test;
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.After;
import org.junit.Before;
import org.junit.Test;
import top.capiudor.dao.AccountMapper;
import top.capiudor.dao.UserMapper;
import top.capiudor.pojo.QueryVo;
import top.capiudor.pojo.User;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 账户测试类
*/
public class AccountTest {
/**
* 抽取公用部分的内容
*/
private InputStream in;
private SqlSession sqlSession;
private AccountMapper accountMapper;
/**
* 初始化
* @throws Exception
*/
@Before
public void init() throws Exception{
in = Resources.getResourceAsStream("Mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
sqlSession = factory.openSession(true);
accountMapper = sqlSession.getMapper(AccountMapper.class);
}
/**
* 释放资源
* @throws Exception
*/
@After
public void destroy() throws Exception{
sqlSession.commit();
in.close();
}
@Test
public void testFindAll() throws Exception{
accountMapper.findAll().forEach(a -> System.out.println(a));
}
}

执行结果:

2020-06-23 16:00:07,948 405    [           main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection2020-06-23 16:00:08,172 629    [           main] DEBUG source.pooled.PooledDataSource  - Created connection 1453128758.2020-06-23 16:00:08,174 631    [           main] DEBUG udor.dao.AccountMapper.findAll  - ==>  Preparing: select * from account 2020-06-23 16:00:08,199 656    [           main] DEBUG udor.dao.AccountMapper.findAll  - ==> Parameters:2020-06-23 16:00:08,236 693    [           main] DEBUG    top.capiudor.dao.UserMapper  - Cache Hit Ratio [top.capiudor.dao.UserMapper]: 0.02020-06-23 16:00:08,236 693    [           main] DEBUG piudor.dao.UserMapper.findById  - ====>  Preparing: select * from user where id = ? 2020-06-23 16:00:08,237 694    [           main] DEBUG piudor.dao.UserMapper.findById  - ====> Parameters: 1(Integer)2020-06-23 16:00:08,239 696    [           main] DEBUG piudor.dao.UserMapper.findById  - <====      Total: 12020-06-23 16:00:08,241 698    [           main] DEBUG    top.capiudor.dao.UserMapper  - Cache Hit Ratio [top.capiudor.dao.UserMapper]: 0.02020-06-23 16:00:08,241 698    [           main] DEBUG piudor.dao.UserMapper.findById  - ====>  Preparing: select * from user where id = ? 2020-06-23 16:00:08,241 698    [           main] DEBUG piudor.dao.UserMapper.findById  - ====> Parameters: 2(Integer)2020-06-23 16:00:08,242 699    [           main] DEBUG piudor.dao.UserMapper.findById  - <====      Total: 12020-06-23 16:00:08,243 700    [           main] DEBUG    top.capiudor.dao.UserMapper  - Cache Hit Ratio [top.capiudor.dao.UserMapper]: 0.02020-06-23 16:00:08,243 700    [           main] DEBUG piudor.dao.UserMapper.findById  - ====>  Preparing: select * from user where id = ? 2020-06-23 16:00:08,244 701    [           main] DEBUG piudor.dao.UserMapper.findById  - ====> Parameters: 3(Integer)2020-06-23 16:00:08,245 702    [           main] DEBUG piudor.dao.UserMapper.findById  - <====      Total: 12020-06-23 16:00:08,245 702    [           main] DEBUG udor.dao.AccountMapper.findAll  - <==      Total: 5Account{id=1, uid=1, money=10000.0, user=User{id=1, name='Jone', age=18, email='test1@baomidou.com'}}Account{id=2, uid=1, money=20000.0, user=User{id=1, name='Jone', age=18, email='test1@baomidou.com'}}Account{id=3, uid=2, money=22222.0, user=User{id=2, name='Jack', age=20, email='test2@baomidou.com'}}Account{id=4, uid=3, money=33333.0, user=User{id=3, name='Tom', age=28, email='test3@baomidou.com'}}Account{id=5, uid=3, money=76383.0, user=User{id=3, name='Tom', age=28, email='test3@baomidou.com'}}

一对一至此完成。

5.2 多对一(一对多)

在Mybatis中,一对多被认为是一对一

基于 ### 3.8 一对一的表继续

构建多对一的对象 :

User.java

/* 在类中加入如下操作 */private List<Account> accounts;public List<Account> getAccounts() {    return accounts;}public void setAccounts(List<Account> accounts) {    this.accounts = accounts;}

通常,我们在多的一方加入一个集合属性,我们这里使用List来作为属性。

UserMapper.xml中的结果映射

<collection property="accounts" column="id" select="top.capiudor.dao.AccountMapper.findByUserId"/>

调用testFindById来测试看看

src/test/java/top/capiudor/test/UserTest.java

    /**     * 根据ID查询     * @throws Exception     */    @Test    public void testFindById() throws Exception{        User user = userMapper.findById(1);    }

执行结果:

2020-06-23 18:53:22,552 494    [           main] DEBUG piudor.dao.UserMapper.findById  - ==>  Preparing: select * from user where id = ? 2020-06-23 18:53:22,574 516    [           main] DEBUG piudor.dao.UserMapper.findById  - ==> Parameters: 1(Integer)2020-06-23 18:53:22,615 557    [           main] DEBUG piudor.dao.UserMapper.findById  - <==      Total: 12020-06-23 18:53:22,617 559    [           main] DEBUG dao.AccountMapper.findByUserId  - ==>  Preparing: select * from account where uid = ? 2020-06-23 18:53:22,617 559    [           main] DEBUG dao.AccountMapper.findByUserId  - ==> Parameters: 1(Integer)2020-06-23 18:53:22,623 565    [           main] DEBUG dao.AccountMapper.findByUserId  - <==      Total: 2[Account{id=1, uid=1, money=10000.0, user=User{id=1, name='Jone', age=18, email='test1@baomidou.com'}}, Account{id=2, uid=1, money=20000.0, user=User{id=1, name='Jone', age=18, email='test1@baomidou.com'}}]

至此,多对一映射就成功了。

5.3 多对多

多对多,类似于用户和角色,一个用户可能有多个角色,一个角色可能被多个用户拥有。

咱们就这个案例,用Mybatis基于XML的方式来完成多对多的操作。

准备表和数据

    DROP TABLE IF EXISTS role;    CREATE TABLE role  (        `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',        `role_name` varchar(255) NOT NULL COMMENT '角色名称',        `role_script` varchar(255) NOT NULL COMMENT '角色描述',        PRIMARY KEY (`id`)    );    DELETE FROM role;    INSERT INTO role(role_name,role_script) VALUES    ('角色1','角色1的描述'),    ('角色2','角色2的描述'),    ('角色3','角色3的描述'),    ('角色4','角色4的描述');    DROP TABLE IF EXISTS role_user;    CREATE TABLE role_user  (        `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',        `user_id` bigint(11) NOT NULL COMMENT '用户外键',        `role_id` int(11) NOT NULL COMMENT '角色外键',        PRIMARY KEY (`id`),        CONSTRAINT `fk_ru_u_id` FOREIGN KEY (`user_id`) REFERENCES `mybatis_crud`.`user` (`id`),        CONSTRAINT `fk_ru_r_id` FOREIGN KEY (`role_id`) REFERENCES `mybatis_crud`.`role` (`id`)    );    DELETE FROM role_user;    INSERT INTO role_user(user_id,role_id) VALUES    (1,1),(2,1),(3,2),(3,3),(4,3);

准备POJO:

src/main/java/top/capiudor/pojo/Role.java

package top.capiudor.pojo;
import java.io.Serializable;
import java.util.List;
public class Role implements Serializable {
private Integer id;
private String roleName;
private String roleScript;
/*一个角色包含多个用户*/
private List users;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getRoleScript() {
return roleScript;
}
public void setRoleScript(String roleScript) {
this.roleScript = roleScript;
}
public List getUsers() {
return users;
}
public void setUsers(List users) {
this.users = users;
}
@Override
public String toString() {
return "Role{" +
"id=" + id +
", roleName='" + roleName + '\'' +
", roleScript='" + roleScript + '\'' +
'}';
}
}

src/main/java/top/capiudor/pojo/User.java

/**
*加入以下内容:一个用户可以拥有多个角色
*/
private List roles;
public List getRoles() {
return roles;
}
public void setRoles(List roles) {
this.roles = roles;
}

准备Mapper:

src/main/resources/top/capiudor/dao/UserMapper.xml

src/main/java/top/capiudor/dao/RoleMapper.java

package top.capiudor.dao;
import top.capiudor.pojo.Role;
import java.util.List;
/**
* 角色接口
*/
public interface RoleMapper {
/**
* 查询所有的角色信息,包含用户信息
* @return
* @throws Exception
*/
List findAll() throws Exception;
/**
* 根据用户的Id查找对应的角色信息
* @param userId
* @return
* @throws Exception
*/
List findByUserId(Integer userId) throws Exception;
}

src/main/resources/top/capiudor/dao/RoleMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
br /> PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
select * from role
select * from role where id in (select role_id from role_user where user_id = #{userId})

准备Mybatis-config.xml添加内容:

src/main/resources/Mybatis-config.xml

准备Test:

src/test/java/top/capiudor/test/RoleTest.java

package top.capiudor.test;
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.After;
import org.junit.Before;
import org.junit.Test;
import top.capiudor.dao.RoleMapper;
import java.io.InputStream;
/**
* 角色测试
*/
public class RoleTest {
/**
* 抽取公用部分的内容
*/
private InputStream in;
private SqlSession sqlSession;
private RoleMapper roleMapper;
/**
* 初始化
* @throws Exception
*/
@Before
public void init() throws Exception{
in = Resources.getResourceAsStream("Mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
sqlSession = factory.openSession(true);
roleMapper = sqlSession.getMapper(RoleMapper.class);
}
/**
* 释放资源
* @throws Exception
*/
@After
public void destroy() throws Exception{
sqlSession.close();
in.close();
}
/**
* 测试查询所有
* @throws Exception
*/
@Test
public void testFindAll() throws Exception{
roleMapper.findAll().forEach(r-> System.out.println(r));
}
}

这个top.capiudor.test.RoleTest#testFindAll测试结果为:

2020-06-23 22:38:35,073 537    [           main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection
2020-06-23 22:38:35,291 755 [ main] DEBUG source.pooled.PooledDataSource - Created connection 2050019814.
2020-06-23 22:38:35,293 757 [ main] DEBUG apiudor.dao.RoleMapper.findAll - ==> Preparing: select * from role
2020-06-23 22:38:35,328 792 [ main] DEBUG apiudor.dao.RoleMapper.findAll - ==> Parameters:
2020-06-23 22:38:35,371 835 [ main] DEBUG apiudor.dao.RoleMapper.findAll - <== Total: 4
2020-06-23 22:38:35,420 884 [ main] DEBUG top.capiudor.dao.UserMapper - Cache Hit Ratio [top.capiudor.dao.UserMapper]: 0.0
2020-06-23 22:38:35,420 884 [ main] DEBUG or.dao.UserMapper.findByRoleId - ==> Preparing: select * from user where id in (select user_id from role_user where role_id=?)
2020-06-23 22:38:35,421 885 [ main] DEBUG or.dao.UserMapper.findByRoleId - ==> Parameters: 1(Integer)
2020-06-23 22:38:35,436 900 [ main] DEBUG or.dao.UserMapper.findByRoleId - <== Total: 2
Role{id=1, roleName='角色1', roleScript='角色1的描述'}
2020-06-23 22:38:35,437 901 [ main] DEBUG top.capiudor.dao.UserMapper - Cache Hit Ratio [top.capiudor.dao.UserMapper]: 0.0
2020-06-23 22:38:35,437 901 [ main] DEBUG or.dao.UserMapper.findByRoleId - ==> Preparing: select * from user where id in (select user_id from role_user where role_id=?)
2020-06-23 22:38:35,437 901 [ main] DEBUG or.dao.UserMapper.findByRoleId - ==> Parameters: 2(Integer)
2020-06-23 22:38:35,439 903 [ main] DEBUG or.dao.UserMapper.findByRoleId - <== Total: 1
Role{id=2, roleName='角色2', roleScript='角色2的描述'}
2020-06-23 22:38:35,440 904 [ main] DEBUG top.capiudor.dao.UserMapper - Cache Hit Ratio [top.capiudor.dao.UserMapper]: 0.0
2020-06-23 22:38:35,440 904 [ main] DEBUG or.dao.UserMapper.findByRoleId - ==> Preparing: select * from user where id in (select user_id from role_user where role_id=?)
2020-06-23 22:38:35,440 904 [ main] DEBUG or.dao.UserMapper.findByRoleId - ==> Parameters: 3(Integer)
2020-06-23 22:38:35,450 914 [ main] DEBUG or.dao.UserMapper.findByRoleId - <== Total: 2
Role{id=3, roleName='角色3', roleScript='角色3的描述'}
2020-06-23 22:38:35,451 915 [ main] DEBUG top.capiudor.dao.UserMapper - Cache Hit Ratio [top.capiudor.dao.UserMapper]: 0.0
2020-06-23 22:38:35,451 915 [ main] DEBUG or.dao.UserMapper.findByRoleId - ==> Preparing: select * from user where id in (select user_id from role_user where role_id=?)
2020-06-23 22:38:35,452 916 [ main] DEBUG or.dao.UserMapper.findByRoleId - ==> Parameters: 4(Integer)
2020-06-23 22:38:35,454 918 [ main] DEBUG or.dao.UserMapper.findByRoleId - <== Total: 0
Role{id=4, roleName='角色4', roleScript='角色4的描述'}
2020-06-23 22:38:35,472 936 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7a30d1e6]
2020-06-23 22:38:35,472 936 [ main] DEBUG source.pooled.PooledDataSource - Returned connection 2050019814 to pool.

这说明咱们的角色Role已经绑定好了 User的信息

top.capiudor.test.UserTest

/** * top.capiudor.test.UserTest#testFindById   * 根据ID查询 * @throws Exception */@Testpublic void testFindById() throws Exception{    User user = userMapper.findById(1);}

执行结果:

可以看到咱们在User中也绑定好了Role的信息。至此,多对多就完成了。

6. Mybatis注解开发(基于注解)

6.1 概述

Mybatis近几年提供了注解开发,为啥啊?看看Spring都提供了AnnotationConfigApplicationContext就知道了,XML配置对程序的友好程度太低了,Mybatis中的注解开发涉及的内容比较宽泛,咱们在这里学习一波常用的注解。

6.2 环境准备

重新创建一个工程、把上一个工程拿过来,把映射文件都干掉:

看一下目录结构:

来看看改动后的文件:

src/main/resources/Mybatis-config.xml

<mappers>        <package name="top.capiudor.dao"/>mappers>

top.capiudor.provider.UserProvider.java是新增的动态SQL的提供者

src/main/java/top/capiudor/dao/AccountMapper.java

package top.capiudor.dao;import org.apache.ibatis.annotations.*;import org.apache.ibatis.mapping.FetchType;import top.capiudor.pojo.Account;import java.util.List;/** * 接口 */@CacheNamespace(blocking = true)public interface AccountMapper {    /**     * 查询所有的账户信息,并返回用户的信息     * @return     * @throws Exception     */    @Select("select * from account")    @Results(id = "accountBaseMap", value = {            @Result(id = true,property = "id",column = "id"),            @Result(property = "uid",column = "uid"),            @Result(property = "money",column = "money"),            @Result(property = "user",column = "uid", one = @One(select = "top.capiudor.dao.UserMapper.findById", fetchType = FetchType.LAZY)),    })    List<Account> findAll() throws Exception;    /**     * 根据用户ID查询账户     * @param userId     * @return     * @throws Exception     */    @Select("select * from account where uid = #{userId}")    @ResultMap("accountBaseMap")    List<Account> findByUserId(Integer userId) throws Exception;}

src/main/java/top/capiudor/dao/RoleMapper.java

package top.capiudor.dao;import org.apache.ibatis.annotations.*;import org.apache.ibatis.mapping.FetchType;import top.capiudor.pojo.Role;import java.util.List;/** * 角色接口 */@CacheNamespace(blocking = true)public interface RoleMapper {    /**     * 查询所有的角色信息,包含用户信息     * @return     * @throws Exception     */    @Select("select * from role")    @Results(id = "roleBaseMap", value = {            @Result(id = true,property = "id",column = "id"),            @Result(property = "roleName",column = "role_name"),            @Result(property = "roleScript",column = "role_script"),            @Result(property = "users",column = "id",many = @Many(select = "top.capiudor.dao.UserMapper.findByRoleId",fetchType = FetchType.LAZY)),    })    List<Role> findAll() throws Exception;    /**     * 根据用户的Id查找对应的角色信息     * @param userId     * @return     * @throws Exception     */    @Select("select * from role where id in (select role_id from role_user where user_id = #{userId})")    @ResultMap("roleBaseMap")    List<Role> findByUserId(Integer userId) throws Exception;}

src/main/java/top/capiudor/dao/UserMapper.java

package top.capiudor.dao;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.mapping.FetchType;
import top.capiudor.pojo.QueryVo;
import top.capiudor.pojo.User;
import top.capiudor.provider.UserProvider;
import java.util.List;
/**
* 用户接口
*/
@CacheNamespace(blocking = true)
public interface UserMapper {
/**
* 查询所有用户
* @return
* @throws Exception
*/
@Select("select * from user")
@Results(id="userBaseMap",value = {
@Result(id = true,column = "id",property = "id"),
@Result(column = "id",property = "id"),
@Result(column = "name",property = "name"),
@Result(column = "age",property = "age"),
@Result(column = "email",property = "email"),
@Result(column = "id",property = "accounts", many = @Many( select = "top.capiudor.dao.AccountMapper.findByUserId", fetchType = FetchType.LAZY)),
@Result(column = "id",property = "roles",many = @Many(select = "top.capiudor.dao.RoleMapper.findByUserId",fetchType = FetchType.LAZY))
})
List findAll() throws Exception;
/**
* 根据用户名模糊查询用户信息
* @param name
* @return
* @throws Exception
*/
@Select("select * from user where name like #{name}")
@ResultMap("userBaseMap")
List findByName(String name) throws Exception;
/**
* 根据用户ID查询用户
* @param id
* @return
* @throws Exception
*/
@Select("select * from user where id = #{id}")
@ResultMap(value = {"userBaseMap"})
User findById(Integer id) throws Exception;
/**
* 根据角色ID查询对应的用户信息
* @param roleId
* @return
* @throws Exception
*/
@Select("select * from user where id in (select user_id from role_user where role_id = #{roleId})")
@ResultMap("userBaseMap")
List findByRoleId(Integer roleId) throws Exception;
/**
* 保存用户
* @Options(useGeneratedKeys=true,keyColumn="id") 返回自增长的主键,
* String keyProperty() default "id";
* 由于keyProperty() 默认为id 与我pojo类一致,故此例未指定 keyProperty 属性
*
* @param user
* @return
* @throws Exception
*/
@Insert("insert into user(name,age,email) values(#{name},#{age},#{email})")
@SelectKey(statement = "select LAST_INSERT_ID()",keyProperty = "id",keyColumn = "id",before = false,resultType = java.lang.Integer.class)
Integer saveUser(User user) throws Exception;
/**
* 修改用户
* @param user
* @return
* @throws Exception
*/
@Update("update user set name=#{name}, age=#{age}, email=#{email} where id=#{id}")
Integer updateUser(User user) throws Exception;
/**
* 根据用户id删除用户
* @param id
* @return
* @throws Exception
*/
@Delete("delete from user where id=#{id}")
Integer deleteUserById(Integer id) throws Exception;
/**
* 根据QueryVo查询用户信息
*
* 在3.5.1版本之后可以
* 将Provider实现接口
* ProviderMethodResolver
* 在使用的时候直接传入实现了ProviderMethodResolver接口的实现类即可
* 默认实现中,会将映射器方法的调用解析到实现的同名方法上
*
* @return
* @throws Exception
*/
@SelectProvider(type = UserProvider.class,method = "buildFindByCondition")
List findByCondition(QueryVo vo) throws Exception;
/**
* 查询总记录数
* @return
*/
@Select("select count(id) from user")
int findTotal() throws Exception;
}

src/main/java/top/capiudor/provider/UserProvider.java

package top.capiudor.provider;
import org.apache.ibatis.jdbc.SQL;
import top.capiudor.pojo.QueryVo;
/**
* 动态SQL的 Provider类
*/
public class UserProvider {
public static String buildFindByCondition(QueryVo vo){
return new SQL(){{
SELECT("*");
FROM("user");
if (vo != null){
if ( vo.getIds() != null && vo.getIds().size() > 0){
String items = "";
for (int i = 0; i < vo.getIds().size(); i++) {
items += vo.getIds().get(i)+",";
}
items = items.substring(0,items.length() -1);
WHERE("id in ("+items+")");
}
if (vo.getUser() != null){
if (vo.getUser().getName() != null){
OR();
WHERE("name like #{user.name}");
}
}
}
}}.toString();
}
}

其他的文件都和上个工程一致

6.3 注解开发CRUD

@Select@Insert@Update@Delete

它们都包含同一个属性:value,指定用来组成单个 SQL 语句的字符串数组。每个注解分别代表将会被执行的 SQL 语句。它们用字符串数组(或单个字符串)作为参数。如果传递的是字符串数组,字符串数组会被连接成单个完整的字符串,每个字符串之间加入一个空格。这有效地避免了用 Java 代码构建 SQL 语句时产生的“丢失空格”问题。当然,你也可以提前手动连接好字符串。

实例:

 /**     * 根据用户ID查询用户     * @param id     * @return     * @throws Exception     */    @Select("select * from user where id = #{id}")    @ResultMap(value = {"userBaseMap"})    User findById(Integer id) throws Exception;    /**     * 根据角色ID查询对应的用户信息     * @param roleId     * @return     * @throws Exception     */    @Select("select * from user where id in (select user_id from role_user where role_id = #{roleId})")    @ResultMap("userBaseMap")    List<User> findByRoleId(Integer roleId) throws Exception;    /**     * 保存用户     * @Options(useGeneratedKeys=true,keyColumn="id") 返回自增长的主键,     *  String keyProperty() default "id";     *  由于keyProperty() 默认为id 与我pojo类一致,故此例未指定 keyProperty 属性     *     * @param user     * @return     * @throws Exception     */    @Insert("insert into user(name,age,email) values(#{name},#{age},#{email})")    @SelectKey(statement = "select LAST_INSERT_ID()",keyProperty = "id",keyColumn = "id",before = false,resultType = java.lang.Integer.class)    Integer saveUser(User user) throws Exception;    /**     * 修改用户     * @param user     * @return     * @throws Exception     */    @Update("update user set name=#{name}, age=#{age}, email=#{email} where id=#{id}")    Integer updateUser(User user) throws Exception;    /**     * 根据用户id删除用户     * @param id     * @return     * @throws Exception     */    @Delete("delete from user where id=#{id}")    Integer deleteUserById(Integer id) throws Exception;

简单的CRUD就可以使用以上的方式进行查询。

@SelectKey的用法很简单、相当于XML中的,属性:statement,以字符串数组形式指定将会被执行的 SQL 语句;keyProperty 指定作为参数传入的对象对应属性的名称,该属性将会更新成新的值;before 可以指定为 truefalse 以指明 SQL 语句应被在插入语句的之前还是之后执行;resultType 则指定 keyProperty 的 Java 类型。

6.4 Results注解的妙用

@Results@Result@ResultMap

这三者之间的关系咱们捞一捞。

先来分别介绍一下这三个注解:

@Results

@Results是一组结果映射,指定了对某个特定结果列,映射到某个属性或字段的方式。

咱们来看看它的属性:valueid。value 属性是一个 @Result 注解的数组。而 id 属性则是结果映射的名称。从版本 3.5.4 开始,该注解变为可重复注解。

@Result

它在列和属性或字段之间的单个结果映射。属性:idcolumnjavaTypejdbcTypetypeHandleronemany

id 属性和 XML 元素 相似,它是一个布尔值,表示该属性是否用于唯一标识和比较对象。one 属性是一个关联,和 类似,而 many 属性则是集合关联,和 类似。

@ResultMap

这个注解为 @Select 或者 @SelectProvider 注解指定 XML 映射中 元素的 id。这使得注解的 select 可以复用已在 XML 中定义的 ResultMap。如果标注的 select 注解中存在 @Results 或者 @ConstructorArgs 注解,这两个注解将被此注解覆盖。

从这些解释来看的话,咱们已经差不多知道都是干嘛的了,@Results注解中包含了多个@Result,而Result就是用来映射的,ResultMap就是引用的@Results注解的。

6.5 对象属性映射(一对一、一对多)

@One

上述代码中有一段@Result(property = "user",column = "uid", one = @One(select = "top.capiudor.dao.UserMapper.findById", fetchType = FetchType.LAZY)),其中的属性one 的变量类型是注解@One,是Mybatis注解开发中体现一对一的内容,等同于

咱们详细介绍一下关于这个@One注解,它是复杂类型的单个属性映射。含有两个属性,select,指定可加载合适类型实例的映射语句(也就是映射器方法)全限定名;fetchType,指定在该映射中覆盖全局配置参数lazyLoadingEnabled,可以选用DEFAULT|LAZY|EAGER,分别对应默认|延迟加载|立即加载

@Many

注解Many@One是差不多的功能,唯一的区别就是前者是集合属性映射,而后者则是单个属性映射。

6.6 蛋疼的动态SQL

说老实话,我都不想介绍,这玩意儿太折磨人了,本着学习的想法,还是看看基本使用,咱们这里就简单聊聊@SelectProvider注解,其他的都差不多的。

@SelectProvider@InsertProvider@UpdateProvider@DeleteProvider

属性:typemethodtype 属性用于指定类名。method 用于指定该类的方法名(从版本 3.5.1 开始,可以省略 method 属性,MyBatis 将会使用 ProviderMethodResolver 接口解析方法的具体实现。如果解析失败,MyBatis 将会使用名为 provideSql 的降级实现)。带有xxxProvider的注解允许构建动态 SQL。这些备选的 SQL 注解允许你指定返回 SQL 语句的类和方法,以供运行时执行。当执行映射语句时,MyBatis 会实例化注解指定的类,并调用注解指定的方法。

使用方式:

需要进行动态查询的接口上方指定好Provider的类名,以及Provider的方法与当前@SelectProvider注解上的方法映射上(其他的也是如此)。

实例:

package top.capiudor.provider;import org.apache.ibatis.jdbc.SQL;import top.capiudor.pojo.QueryVo;/** * 动态SQL的 Provider类 */public class UserProvider {    /**     * 此方法为动态查询的提供者     * 它提供了SQL,Mybatis将Java万物皆对象的原则用得相当熟络,将SQL函数进行高度抽象  org.apache.ibatis.jdbc.AbstractSQL     *      提供了数据库常用的基本关键字、剩下的则由我们进行控制组装,说白了,Mybatis是个工厂,提供了足够多的工具,     *      至于怎么组装,那就得看我们的了     * @param vo     * @return  返回复杂的SQL语句     */    public static String buildFindByCondition(QueryVo vo){                        return new SQL(){{            // 查询的内容            SELECT("*");            // 目标表            FROM("user");            if (vo != null){                // 从if标签过度到了if语句                if ( vo.getIds() != null && vo.getIds().size() > 0){                    String items = "";                    for (int i = 0; i < vo.getIds().size(); i++) {                        items += vo.getIds().get(i)+",";                    }                    // 使用Java的方式进行组装                    items = items.substring(0,items.length() -1);                    // 封装拼接好的条件                    WHERE("id in ("+items+")");                }                if (vo.getUser() != null){                    if (vo.getUser().getName() != null){                        // OR()、AND()函数都相当于数据库中的or | and                        OR();                        WHERE("name like #{user.name}");                    }                }            }        }}.toString();    }}

6.7 注解开启二级缓存

在需要开启二级缓存的Mapper接口上使用注解@CacheNamespace(blocking = true),即可开启。

例如:

/** * 角色接口 */@CacheNamespace(blocking = true)public interface RoleMapper {...}

Mybatis还有很多需要我们解锁的姿势、除了满足我们的日常需求外、还有很多的内容没有被利用上,等待开发者们来发现这些个彩蛋。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值