MyBatis

本文详细介绍了MyBatis的使用,包括mybatis的CRUD基本流程、lombok的注解应用、SqlSession的详细解释、动态Mapper的实现原理、占位符#{}的注意事项、别名的使用、切换数据源和日志实现、ResultMap的配置、动态SQL查询语句(如where、if、trim标签)和级联查询。同时讲解了MyBatis的一级缓存和二级缓存机制,以及如何启用和禁用二级缓存,并探讨了第三方缓存ehcache的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MyBatis

动态代理的核心就是对一个方法的增强。

mybatis的crud基本流程:

// 1、创建一个SqlSessionFactory的 建造者 ,用于创建SqlSessionFactory
// SqlSessionFactoryBuilder中有大量的重载的build方法,可以根据不同的入参,进行构建
// 极大的提高了灵活性,此处使用【创建者设计模式】
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 2、使用builder构建一个sqlSessionFactory,此处我们基于一个xml配置文件
// 此过程会进行xml文件的解析,过程相对比较复杂
SqlSessionFactory sqlSessionFactory = builder.build(Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatis-config.xml"));
// 3、通过sqlSessionFactory获取另一个session,此处使用【工厂设计模式】
SqlSession sqlSession = sqlSessionFactory.openSession();

1、创建一个SqlSessionFactory的 建造者 ,用于创建SqlSessionFactory

2、使用builder构建一个sqlSessionFactory,此处我们基于一个xml配置文件

3、通过sqlSessionFactory获取另一个session,此处使用【工厂设计模式】

4、一个sqlsession就是一个会话,可以使用sqlsession对数据库进行操作,原理后边会讲。

1 . lombok的使用:

  • @AllArgsConstructor:生成全参构造器。

  • @NoArgsConstructor:生成无参构造器。

  • @Getter/@Setter: 作用类上,生成所有成员变量的getter/setter方法;作用于成员变量上,生成该成员变量的getter/setter方法。可以设定访问权限及是否懒加载等。

  • @Data:作用于类上,是以下注解的集合:@ToString @EqualsAndHashCode @Getter @Setter @RequiredArgsConstructor

  • @Log:作用于类上,生成日志变量。针对不同的日志实现产品,有不同的注解。

  • @Builder:使用创建者模式又叫建造者模式。简单来说,就是一步步创建一个对象,它对用户屏蔽了里面构建的细节,但却可以精细地控制对象的构造过程。

  • @@Slf4j:

    @Slf4j//加上这个就不需要我们手动写:
    //public static  final Logger logger  = LoggerFactory.getLogger(TestMybatis.class);
    public class TestMybatis {
        //public static  final Logger logger  = LoggerFactory.getLogger(TestMybatis.class);
        @Test
        public void testSqlSession() throws IOException {
            log.debug("debug");
        }
    
    }
    

引入依赖:并在Setings中搜索plugings,搜索lombok将其下载。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>provided</scope>
</dependency>
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private  int id;
    private String username;
    private String password;
}

这样子我们就不用写:getter,setter,构造器,toString()之类的方法了。它已经给我们写好了。我们直接调用即可

2 . sqlSession详解:

mybatis-config.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>
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/ssm?characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </properties>

    <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>
    <mappers>
        <mapper resource="UserMapper.xml"/>
    </mappers>
</configuration>

具体实现类:

@Slf4j//加上这个就不需要我们手动写:
//public static  final Logger logger  = LoggerFactory.getLogger(TestMybatis.class);
public class TestMybatis {
    //public static  final Logger logger  = LoggerFactory.getLogger(TestMybatis.class);
    @Test
    public void testSqlSession() throws IOException {
        User user = new User(1,"cc","12");
        String resource = "mybatis-config.xml";
        //通过该路劲拿到一个流
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        try (SqlSession session = sqlSessionFactory.openSession()) {
            //statement是sql的声明
            //首先找到mybatis-config.xml文件,解析完mybatis-config.xml自身后,
            // 发现mybatis-config.xml内部配置了一个mappers,
            // mappers内部配置了一个UserMapper.xml,找到了UserMapper.xml文件
            //,解析UserMapper.xml文件后找到了一个叫user的命名空间
            //在该空间找到一个sql语句 ,该sql语句的名字是select,该id的值为User类的全限定名,
            // 将id和id的值存到List中
            //从list中拿到user命名空间中的select
            List<Object> objects = session.selectList("user.select");
            log.debug("result is [{}]",objects);
            //result is [[User(id=1, username=itnanls, password=123456),
            // User(id=2, username=itlils, password=abcdef),
            // User(id=3, username=ydlclass, password=987654)]]
        }
    }
}

3 . 动态mapper的实现原理:

我们线创建一个用户相关的接口和实现类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kMwPV6FV-1681790306034)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664613981586.png)]

public interface UserMapper {
    //查询所有的用户
    List<User> selectAll();
}

具体实现类:

public class UserMapperimpl implements UserMapper{
    @Override
    public List<User> selectAll() {
        String resource = "mybatis-config.xml";
        //通过该路劲拿到一个流
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        //这个对象是核心,一个工程的数据库相关的操作都死在围绕sqlSessionFactory
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
        
        try (SqlSession session = sqlSessionFactory.openSession()) {
            List<Object> objects = session.selectList(UserMapper.class.getName()+"."+"selectAll");
            System.out.println(objects);
        }
        return null;
    }

    public static void main(String[] args) {
        UserMapper userMapper = new UserMapperimpl();
        userMapper.selectAll();
         //[User(id=1, username=itnanls, password=123456),
        // User(id=2, username=itlils, password=abcdef),
        // User(id=3, username=ydlclass, password=987654)]
    }
}

由于我们使用反射来得到接口类名,并用此来作为命名空间,sql语句查询所有用户作为内容,所以我们要将配置文件UserMapper.xml的内容修改为对应的内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MMbvdxI1-1681790306035)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664614264816.png)]

  • esultType:指定返回类型,查询是有结果的,结果啥类型,你得告诉我
  • parameterType:指定参数类型,查询是有参数的,参数啥类型,你得告诉我
  • id:指定对应的方法映射关系,就是你得告诉我你这sql对应的是哪个方法
  • #{id}:sql中的变量,要保证大括号的变量必须在User对象里有
  • #{}:占位符,其实就是咱们的【PreparedStatement】处理这个变量,mybatis会将它替换成?

以上便是mybatis的动态mapper代理的实现原理,我们在此原理上使用sqlSession进行运用:使其更加简单

sqlSession.getMapper(UserMapper.class);帮我们生成一个代理对象,该对象实现了这个接口的方法,具体的数据库操作比如建立连接,创建statment等重复性的工作交给框架来处理,唯一需要额外补充的就是sql语句了,xml文件就是在补充这个描述信息,比如具体的sql,返回值的类型等,框架会根据命名空间自动匹配对应的接口,根据id自动匹配接口的方法,不需要我们再做额外的操作。

@Slf4j//加上这个就不需要我们手动写:
//public static  final Logger logger  = LoggerFactory.getLogger(TestMybatis.class);
public class TestMybatis {

    SqlSessionFactory sqlSessionFactory = null;

    @Before//使用该注解,程序已启动就会执行这里面的内容
    public void before() throws IOException {
        String resource = "mybatis-config.xml";
        //通过该路劲拿到一个流
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        //这个对象是核心,一个工程的数据库相关的操作都死在围绕sqlSessionFactory
        sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);

    }

    //public static  final Logger logger  = LoggerFactory.getLogger(TestMybatis.class);
    @Test
    public void testSqlSession() throws IOException {

        try (SqlSession session = sqlSessionFactory.openSession()) {
            //动态代理生成了一个mapper对象
            //获得一个代理对象,使用的jdk的Proxy类,并且代理对象实现了UserMapper的接口
            UserMapper mapper = session.getMapper(UserMapper.class);
            List<User> users = mapper.selectAll();
            log.debug("users is [{}]",users);
            //users is [[User(id=1, username=itnanls, password=123456),
            // User(id=2, username=itlils, password=abcdef),
            // User(id=3, username=ydlclass, password=987654)]]
        }
    }
}

select查询单个用户:

public interface UserMapper {
    //查询所有的用户
    List<User> selectAll();

    //查询某个用户
    User selectOne(int id);
}

我们将配置文件UserMapper.xml配置修改:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8g1nuR7n-1681790306035)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664617179144.png)]

具体实现类:

@Slf4j//加上这个就不需要我们手动写:
//public static  final Logger logger  = LoggerFactory.getLogger(TestMybatis.class);
public class TestMybatis {

    SqlSessionFactory sqlSessionFactory = null;

    @Before//使用该注解,程序已启动就会执行这里面的内容
    public void before() throws IOException {
        String resource = "mybatis-config.xml";
        //通过该路劲拿到一个流
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        //这个对象是核心,一个工程的数据库相关的操作都死在围绕sqlSessionFactory
        sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);

    }
@Test
public void testFindById() throws IOException {

    try (SqlSession session = sqlSessionFactory.openSession()) {
        //动态代理生成了一个mapper对象
        //获得一个代理对象,使用的jdk的Proxy类,并且代理对象实现了UserMapper的接口
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.selectOne(1);
        log.debug("users is [{}]",user);
        //users is [User(id=1, username=itnanls, password=123456)]
    }
}

1 . 占位符#{}的注意事项 :

#{}是占位符,$ {}是字符串拼接。

(1)按照多个参数操作的情况:

已知UserMapper接口中有方法selectUser,

@Test
public User selectUser() throws IOException {

    try (SqlSession session = sqlSessionFactory.openSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.selectUser("cc","123");
        log.debug("users is [{}]",user);
        return user;
    }
}

UserMapper.xml文件配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rC2PYXhd-1681790306036)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664618186488.png)]

运行报错,这是因为在xml配置文件中的占位符#{}中的只是识别的数据的类型,并不能直接通过名字来识别,所以报错。

我们对接口进行修改:

意思就是username的值会传到Param中的username中,(传到xml文件中名为username的),password的值会传到Param中的password中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IRWB8XZ9-1681790306036)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664618657904.png)]

此时xml文件能够识别到参数的内容查寻到相关用户信息

(2)如果按照类对象User来进行查询时:

UserMapper接口中:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0JydBfuo-1681790306036)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664619648001.png)]

配置文件:UserMapper.xml:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-spqdEumo-1681790306037)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664619681622.png)]

具体实现类:

@Test
public void selectOneUser() throws IOException {
    try (SqlSession session = sqlSessionFactory.openSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        //先封装User
        User user = new User(1, "cc", "123");
        User param = mapper.selectOneUser(user);
        log.debug("users is [{}]",param);
    }
}

此时正常运行,因为user对象本身就包含了username和password,所以不用使用@Param

(3)按照Map来进行操作时

已知UserMapper接口中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GHY008qx-1681790306037)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664620122346.png)]

UserMapper.xml文件配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cqqZRh89-1681790306037)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664620146919.png)]

具体实现类:

@Test
public void selectByMap() throws IOException {
    try (SqlSession session = sqlSessionFactory.openSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        //新建一个Map并添加数据
        Map<String,Object> map = new HashMap<>();
        map.put("username","cc");
        User param = mapper.selectByMap(map);
        log.debug("users is [{}]",param);
        //users is [User(id=1, username=itnanls, password=123456)]
    }
}

原理同上,因为该map中已经包含了username,password的内容不需要注解去指定。

(4)模糊查询的格式问题:

UserMapper接口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5upMZQvS-1681790306038)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664623049508.png)]

UserMapper.xml文件配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lBQO21Lz-1681790306038)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664623076267.png)]

具体实现类:

@Test
public void selectByUsername() throws IOException {
    try (SqlSession session = sqlSessionFactory.openSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        //解决sql语句模糊查询:like username like 'i';的格式问题
        List<User> param = mapper.selectByUsername("%"+"i"+"%");
        log.debug("users is [{}]",param);
        //users is [User(id=1, username=itnanls, password=123456)]
    }
}

(5)增,修改和删除操作:

<insert id="insert" >
    insert into user (id,username,password) values (#{id},#{username},#{password})
</insert>

<update id="update" >
    update `user` set username = #{username},password = #{password} where id = #{id}
</update>

<delete id="delete" >
    delete from  `user` where id = #{id}
</delete>

接口:

int insert(User user);

int update(User user);

int delete(User user);

具体实现:

@Test
public void insertUser() {
    try (SqlSession session = sqlSessionFactory.openSession()) {
        try{
            UserMapper mapper = session.getMapper(UserMapper.class);
            int cc = mapper.insert(new User(133, "cc", "123"));
            log.debug("afftectrow is [{}]",cc);
            //因为sqlSession不能自动提交事务,所以我们后台插入数据后,
            // mysql中并没有成功加数据插入,需要手动提交
            session.commit();
        }catch (Exception e){
            log.error("发生了异常",e);
            //发生异常后,事务回滚
            session.rollback();
        }
    }
}

@Test
public void updateUser() {
    //添加上true的参数,则代表自动提交事务
    try (SqlSession session = sqlSessionFactory.openSession(true)) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        int c2 = mapper.update(new User(3,"cgboy","456"));
        log.debug("afftectrow is [{}]",c2);
    }
}

@Test
public void deleteUser() {
    //添加上true的参数,则代表自动提交事务
    try (SqlSession session = sqlSessionFactory.openSession(true)) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        int c2 = mapper.delete(new User(134,"cgboy","456"));
        log.debug("afftectrow is [{}]",c2);
    }
}

3 . 别名的使用:

我们每次写新的sql时在UserMapper.xml中配置文件都要重复的写上数据库中表对应目标的类,这样太麻烦了“:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RCHKyYPA-1681790306038)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664783448052.png)]

所以我们在mybatis-config.xml中给这个数据库中表对应的目标类配置别名:

查看配置的位置顺序:点击下列信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sqe7vsKN-1681790306038)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665296952141.png)]

进入后显示各个标签设置的先后顺序得知别名要配在settings后边,environment前面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4WohtnXS-1681790306039)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665297038894.png)]

配置信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUH9gZoK-1681790306039)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664783511919.png)]

然后我们在UserMapper.xml中只需要写,就更方便了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UtkeK3Io-1681790306039)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664783562121.png)]

但是这样依然很麻烦,因为当有很多类时,我们就要配很多类的别名,所以我们用包名来进行配别名,这样整个包

下的类都会有别名,不需要我们自己配置了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gx0NZ4XL-1681790306040)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664783769520.png)]

在该包下的所有类都会自动有别名。我们只需要在UserMapper.xml中写对应的数据库表名就行:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQqKwoTS-1681790306040)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664785350906.png)]

4 . 切换数据源:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tk9Jbkr8-1681790306040)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665655409815.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XTPyJrE6-1681790306040)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665655474466.png)]

引入依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>${druid.version}</version>
</dependency>

mybatis-config.xml配置:使其测试时通过德鲁伊连接池进行操作

<environment id="test"> <!--id为test代表为测试环境-->
    <transactionManager type="JDBC"/>
    <dataSource type="com.cgboy.datasource.DruidDataSourceFactory">
        <property name="druid.driverClassName" value="${driver}"/>
        <property name="druid.url" value="${url}"/>
        <property name="druid.username" value="${username}"/>
        <property name="druid.password" value="${password}"/>
    </dataSource>
</environment>

获得德鲁伊数据源:

public class DruidDataSourceFactory extends PooledDataSourceFactory {
    private Properties properties;
    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public DataSource getDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.configFromPropety(properties);
        return druidDataSource;
    }
}

测试:

@Test
public void testSqlSession() throws IOException {

    try (SqlSession session = sqlSessionFactory.openSession()) {
        //动态代理生成了一个mapper对象
        //获得一个代理对象,使用的jdk的Proxy类,并且代理对象实现了UserMapper的接口
        UserMapper mapper = session.getMapper(UserMapper.class);
        List<User> users = mapper.selectAll();
        log.debug("users is [{}]",users);
        //users is [[User(id=1, username=itnanls, password=123456),
        // User(id=2, username=itlils, password=abcdef),
        // User(id=3, username=ydlclass, password=987654)]]
    }
}

5 . 切换日志实现:

配置文件logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss} %c [%thread] %-5level %msg%n"/>
    <property name="log_dir" value="d:/logs" />

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.out</target>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>

    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <!--日志格式配置-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--日志输出路径-->
        <file>${log_dir}/sql.log</file>
    </appender>

    <root level="ALL">
        <appender-ref ref="console"/>
    </root>

    <!--输出到文件,和控制台-->
    <logger name="mybatis.sql" level="debug" additivity="false">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </logger>

</configuration>

引入依赖:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>${logback.version}</version>
</dependency>

mybatis-config.xml中设置使用slf4j的日志实现:

<settings>
   <setting name="logImpl" value="SLF4J"/>
</settings>

在我们测试类运行后显示的便是slf4j的日志实现格式。并在指定文件路径新建了一个sql.log的日志文件记录

5 . reslutmap:

引入原因:

我们在数据库中将对应user表的username列修改为user_name,保存后,在idea中测试遍历表中所有数据时,发现id和password的值都能显示,username拿到的值却为null,这是因为我们mybatis是使用的反射的原理拿到了对应的user类,从而从user类中id,username,password找到数据库user表对应的数据。而我们将数据库user表的username修改为user_name,导致了mybatis找不到了user_name的值,所以为null。

这种情况我们可以在UserMapper.xml文件中将sql语句的user_name列名加上别名来解决:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HFL1KnV7-1681790306041)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664864779635.png)]

但是这样太麻烦了。我们是使用resultMap的映射关系:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MdF1osSa-1681790306041)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664865030299.png[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d7PV5oQR-1681790307024)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664980423914.png)]]

更方便的操作:在mybayis.config.xml主配置文件下开启驼峰命名规则:

<!--开启驼峰命名规则-->
<setting name="mapUnderscoreToCamelCase" value="true"/>

数据库user表中user_name的列名,在mybatis中User类的userName会被自动识别到数据库的user_name。

有了驼峰式命名的配置信息后,前面我们写的都可以不要,也能映射到对应的值。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZ50u4Ec-1681790306042)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664865733652.png)]

照样能运行拿到对应的值。

6 . 动态sql查询语句(必须学会):

(1)where和if标签:

MyBatis提供了对SQL语句动态的组装能力,大量的判断都可以在 MyBatis的映射XML文件里面配置,以达到许多我们需要大量代码才能实现的功能,大大减少了我们编写代码的工作量。

如下在UserMapper.xml配置中“:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uF9qxmD0-1681790306042)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664870662168.png)]

在where标签内保证了会在where标签内运行,它实际上是这种格式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GbzH8CWp-1681790306042)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664872837236.png)]

意思是保证where后我们加了一个1=1的绝对true的语句,目的是为了防止语句错误,变成SELECT * FROM student WHERE这样where后没有内容的错误语句。这样会写看着有点奇怪,所以我们直接将内容包在标签内。

在实现类中:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-THTxcr7X-1681790306042)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664870694105.png)]

看输出的sql语句:为null都不显示出来

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XW11fhD9-1681790306043)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664870719807.png)]

如果我们把参数更改:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7wS1DsRa-1681790306043)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664870790287.png)]

此时观察输出sql语句:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T85Vqwl5-1681790306043)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664870817685.png)]

(2)trim标签:

有时候我们要去掉一些特殊的SQL语法,比如常见的and、or,此时可以使用trim元素。trim元素意味着我们需要去掉一些特殊的字符串,prefix代表的是语句的前缀,而prefixOverrides代表的是你需要去掉的那种字符串,suffix表示语句的后缀,suffixOverrides代表去掉的后缀字符串。

把原本的前缀编程一个新的前缀。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rRgPGzUE-1681790306043)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664873189051.png)]

将and改成了where(只是为了示范,其实这样做了sql语句就错了)

已知id,username为null,输出不显示,只显示password查看输出的sql语句证实:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iv4KJ44I-1681790306044)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664873270193.png)]

(3)choose,when(if),otherwise(else)标签:

类似 if … else if 语法:在choose标签内执行,when就是if,otherwise就是else

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bcdH8azB-1681790306044)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664873933613.png)]

查询语句动态sql:

以上就是有关查询语句的动态sql的解释,我们最常用的是:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SDTlvGKf-1681790306045)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664874141027.png)]

7 . 动态sql修改语句:

xml文件配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRL35vgn-1681790306045)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664875674608.png)]

具体实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WrRzAJ4E-1681790306045)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664875614167.png)]

输出结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iS1AeNu6-1681790306045)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664875630623.png)]

8 . foreach标签:

(1)批量删除和遍历:

UserMapper.xml文件配置:

删除

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gejskTCP-1681790306046)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664877395374.png)]

遍历

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t9IdJglv-1681790306046)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664877332181.png)]

UserMapper接口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u2MTcXKI-1681790306046)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664877369974.png)]

具体实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mEe2GwRy-1681790306046)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664877436356.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sdvyInac-1681790306046)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664877447600.png)]

(2)批量插入:

UserMapper.xml文件配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ufCEIzHX-1681790306047)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664878336511.png)]UserMapper接口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6crlrlWz-1681790306047)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664878379809.png)]

具体实现类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cpe3OAuc-1681790306047)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664878397369.png)]

(3)sql片段:

我们在sql语句中应该避免使用 * 符号,将其改为sql片段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ov0ZfmNc-1681790306047)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664886082976.png)]

其他的相关有*符号的都替换掉:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uopMwRHY-1681790306047)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664886132053.png)]

9 . 多表查询:

已知数据库表:dept表emloyee表,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sTUCi8dZ-1681790306047)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947334753.png)]

dept表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dj4i0uol-1681790306048)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947361258.png)]

查询类似:

select * from employee e 
LEFT JOIN dept d on d.id = e.did

主配置mybatis-config.xml文件配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5lseQaNy-1681790306048)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947433665.png)]

dept类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-coZPJn8a-1681790306048)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947459306.png)]

employee类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EwPjfhVR-1681790306048)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947472246.png)]

对应的接口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JyiSvjCc-1681790306048)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947498495.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0oiqujkd-1681790306048)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947513089.png)]

对应的xml文件配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97vAgCk0-1681790306049)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947549954.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w5V8UYje-1681790306049)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947565965.png)]

employ具体实现类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8otl56U4-1681790306049)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664947606177.png)]

10 . 高级映射: association:

(1)级联查询(子查询):

以下查询类似:

select * from employee e where e.did in (
select d.id from dept d
)

EmployeeMapper.xml:.select是代表的查询,意思是从Dept中查询,如果是修改的sql,则是.update

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BU8vA7xq-1681790306049)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664961456287.png)]

DeptMapper.xml:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-msSn0eSC-1681790306049)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664955846618.png)]

(2)结果嵌套(类似left join)常用:

查询类似:

select e.id eid,e.name ename,d.id did,d.name dname
        from employee e left join dept d on d.id = e.did

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BWmiOJGi-1681790306049)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664960397797.png)]

相关接口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rYnTEArM-1681790306050)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664960431530.png)]

具体实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XMVjUiVg-1681790306050)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664960453189.png)]

11 . 高级映射collection:

(1)级联查询:

查询思路sql语句:

select * from employee e where e.did in (
select d.id from dept d
)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oMHDgZxN-1681790306050)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664962156076.png)]

然后从dept中查到对应id:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bq9jsiHd-1681790306050)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664962198994.png)]

(2)结果嵌套:

DeptMapper.xml:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QnYbjH6j-1681790306050)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664972819603.png)]

如果此时拿到员工的信息后,还有公司表(代表每个员工所属公司),那么我们可以再用asssociation或者collection映射一个公司表来查询到每个员工的公司的信息:

company类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XKqPMQmK-1681790306050)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664980259450.png)]

在Employee类中添加:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QbY4mDWQ-1681790306050)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664980298201.png)]

companyMapper接口:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EuUxsIic-1681790306051)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664980326019.png)]

companyMapper.xml文件配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q4MZbldL-1681790306051)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1664980377913.png)]

12 . 懒加载:

按需加载,我们需要什么的时候再去进行什么操作。而且先从单表查询,需要时再从关联表去关联查询,能大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。

当我们在使用单表或多表查询时,有时候查询会查询比如部门id和部门name,但有时也可能只查询部门id不会查询部门name,那么我们就要再sql中进行修改,但是太麻烦了,所以我们使用懒加载方式,再需要时再从关联表中去关联查询,大大提高了数据库性能:当我们查某个列时,懒加载自动给我们关联到该信息,不用我们自己去sql语句更改。

配置信息:

<!-- 开启懒加载配置 -->
<settings>
  <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 -->
  <setting name="lazyLoadingEnabled" value="true"/>
  <!-- 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载 -->
  <setting name="aggressiveLazyLoading" value="false"/>
</settings>
懒加载的底层是怎么实现的?

代理,方法的增强(比如对数据库的数据查询,懒加载后本来应该查询的值为null,但是它却能查到,用的动态代理)

13 . 获得自增的主键:

拿到id可以通过该id找相关联的表。

UserMapper.xml配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-afaawsGN-1681790306051)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665039528312.png)]

具体实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b7aL4020-1681790306051)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665039560735.png)]

已知表中数据userid最大为148,输出结果后,插入的user的id分明是null,可是我们看到它的id为149,这说明使用了自动递增主键

14 . (重点)Mybatis缓存:

(1)为什么要用缓存?

如果缓存中有数据,就不用从数据库获取,大大提高性能

mybatis提供一级缓存(默认使用)和二级缓存

  • 在操作数据库时,需要构造sqlsession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据
  • 不同的sqlsession之间的缓存区域是互相不影响的。

一级缓存一开始就会存在。

一级缓存:当前会话共享数据,与数据库连接时就会有一级缓存

二级缓存:不同绘画共享数据,mybatis的mapper执行使用的二级缓存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AflLcwJH-1681790306051)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665047105577.png)]

查询时没有该数据,所以通过sql从数据库查询,查询到的数据缓存到一级缓存,当重复查询此数据时,会先去一级缓存查询有没有此数据,有此数据直接用,没有再去数据库根据sql查询数据。

(2)一级缓存是sqlSession级别的缓存,

一级缓存失效的情况

1 .只能在相同的会话中,不然失效。

2 .当sqlSession对象相同的时候,查询的条件不同,原因是第一次查询时候,一级缓存中没有第二次查询所需要的数据

3 . 当sqlSession对象相同,两次查询之间进行了插入的操作(此时一级缓存失效是避免脏读)

4 . sqlSession对象相同,手动清除了一级缓存中的数据

(3)二级缓存:

不同的会话下能共享数据。

首先在主配置文件mybatis-config.xml下配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zZMt3BoT-1681790306052)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665050390886.png)]

随后在你要操作的Mapper中进行配置cache,如在UserMapper.xml中配置,你可以在该标签写上你的内容也可以不写:

如下:

<!--开启本Mapper的namespace下的二级缓存-->
<cache eviction="LRU" flushInterval="100000" size="512" readOnly="true"></cache>
<!--
创建了一个 LRU 最少使用清除缓存,每隔 100 秒刷新,最多可以存储 512 个对象,返回的对象是只读的。
-->

但我们的UserMapper.xml这里选择不写:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pzhjjg7x-1681790306052)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665050632636.png)]

这样我们的二级缓存就生效了。

测试二级缓存:

根据上述配置完成的信息,具体实现类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Usw0J4or-1681790306052)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665052585540.png)]

输出结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rosFD6wx-1681790306052)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665052630265.png)]

由此可知,它告诉了我们使用到了二级缓存,第一次查询通过sql语句查询到数据库获取数据,第二次新建会话查询相同数据时,它通过二级缓存查询到有数据,直接使用此数据。

注意:

数据库的数据一旦发生变化,缓存的数据必须清空,必须保证缓存与数据库的数据一致(避免脏读)

二级缓存一般情况很少开启,因为二级缓存可以共享其他会话的数据,那么当其中一个会话的数据与另一个会话的数据有相关联时,若修给了其中一个会话的数据,那么该会话的缓存也会清空,而另一个会话的缓存因为所在会话的数据没有修改缓存不会被清空,导致实际上该会话的数据已经和另一个会话数据不一致了

禁用二级缓存:

在statement中可以设置useCache=false,禁用当前select语句的二级缓存,默认情况为true

<select id="getStudentById" parameterType="java.lang.Integer" resultType="Student" useCache="false">

1

在实际开发中,针对每次查询都需要最新的数据sql,要设置为useCache=“false” ,禁用二级缓存

flushCache标签:刷新缓存(清空缓存)

<select id="getStudentById" parameterType="java.lang.Integer" resultType="Student" flushCache="true">

1

一般下执行完commit操作都需要刷新缓存,flushCache="true 表示刷新缓存,可以避免脏读

(4)cache属性简介:

eviction回收策略(缓存满了的淘汰机制),目前MyBatis提供以下策略。

  1. LRU(Least Recently Used),最近最少使用的,最长时间不用的对象
  2. FIFO(First In First Out),先进先出,按对象进入缓存的顺序来移除他们
  3. SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象,当内存不足,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用的包裹的对象给干掉,也就是只有内存不足,JVM才会回收该对象。
  4. WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。弱引用的特点是不管内存是否足够,只要发生GC,都会被回收。

flushInterval:刷新间隔时间,单位为毫秒,

  1. 这里配置的是100秒刷新,如果你不配置它,那么当SQL被执行的时候才会去刷新缓存。

size:引用数目,

  1. 一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。

    这里配置的是1024个对象

**readOnly:**只读,

  1. 意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有

    办法修改缓存,它的默认值是false,不允许我们修改

格式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O6T4P52v-1681790306052)(C:\Users\CG\AppData\Roaming\Typora\typora-user-images\1665053634655.png)]

15 . 使用第三方缓存ehcache:

pom.xml中引入依赖:

<dependency>
   <groupId>org.mybatis.caches</groupId>
   <artifactId>mybatis-ehcache</artifactId>
   <version>1.2.1</version>
</dependency>

然后我们在对应的Mapper.xml使用这个缓存:

type固定缓存是使用ehcache缓存,创建了一个 LRU 最少使用清除缓存,每隔 100 秒刷新,最多可以存储 512 个对象,返回的对象是只读的。

<cache type="org.mybatis.caches.ehcache.EhcacheCache" eviction="LRU" flushInterval="10000" size="512" readOnly="true"></cache>

">


1

一般下执行完commit操作都需要刷新缓存,flushCache="true 表示刷新缓存,可以避免脏读



## (4)cache属性简介:

> **eviction**回收策略(缓存满了的淘汰机制),目前MyBatis提供以下策略。

1. LRU(Least Recently Used),最近最少使用的,最长时间不用的对象
2. FIFO(First In First Out),先进先出,按对象进入缓存的顺序来移除他们
3. SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象,当内存不足,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用的包裹的对象给干掉,也就是只有内存不足,JVM才会回收该对象。
4. WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。弱引用的特点是不管内存是否足够,只要发生GC,都会被回收。

> **flushInterval**:刷新间隔时间,单位为毫秒,

1. 这里配置的是100秒刷新,如果你不配置它,那么当SQL被执行的时候才会去刷新缓存。

> **size**:引用数目,

1. 一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。

   这里配置的是1024个对象

> **readOnly:**只读,

1. 意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有

   办法修改缓存,它的默认值是false,不允许我们修改

格式:

[外链图片转存中...(img-O6T4P52v-1681790306052)]



# 15 . 使用第三方缓存ehcache:

pom.xml中引入依赖:

```xml
<dependency>
   <groupId>org.mybatis.caches</groupId>
   <artifactId>mybatis-ehcache</artifactId>
   <version>1.2.1</version>
</dependency>

然后我们在对应的Mapper.xml使用这个缓存:

type固定缓存是使用ehcache缓存,创建了一个 LRU 最少使用清除缓存,每隔 100 秒刷新,最多可以存储 512 个对象,返回的对象是只读的。

<cache type="org.mybatis.caches.ehcache.EhcacheCache" eviction="LRU" flushInterval="10000" size="512" readOnly="true"></cache>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值