《JavaEE进阶》----16.<Mybatis简介、操作步骤、相关配置>

news2024/9/20 7:53:23

本篇博客讲记录:

1.回顾MySQL的JDBC操作

2..Mybatis简介、Mybatis操作数据库的步骤

3.Mybatis 相关日志的配置(日志的配置、驼峰自动转换的配置)

前言

之前学习应用分层时我们知道Web应用程序一般分为三层,Controller、Service、Dao。

浏览器发起请求、现请求Controller、Controller接收到请求后,调用Service进行业务逻辑处理。Service再调用Dao。Dao再从数据库中获取数据。今天我们就来讲解Mybatis数据库。

学习MySQL数据库时,已经学习了JDBC来操作数据库。而JDBC操作很复杂。这就是我们要学习 MyBatis 的真正原因,它可以帮助我们更方便、更快速的操作数据库。Mybatis就是对JDBC封装后的结果。

一、JDBC操作示例回顾:

JDBC操作步骤

1. 创建数据库连接池DataSource

    private final DataSource dataSource;
    public SimpleJdbcOperation(DataSource dataSource) {
         this.dataSource = dataSource;
    }

2. 通过DataSource获取数据库连接Connection

    public void addBook() {
    Connection connection = null;
    PreparedStatement stmt = null;
    try {
        //获取数据库连接 
        connection = dataSource.getConnection();

3. 编写要执行带?占位符的SQL语句(预编译SQL)(不能封装)

        //创建语句 
        stmt = connection.prepareStatement(
            "insert into soft_bookrack (book_name, book_author, 
book_isbn) values (?,?,?);"
        );

4. 通过Connection及SQL创建操作命令对象Statement

    PreparedStatement stmt = null;

5. 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值(不能封装)

        //参数绑定 
        stmt.setString(1, "Spring in Action");
        stmt.setString(2, "Craig Walls");
        stmt.setString(3, "9787115417305");

6. 使用Statement执行SQL语句

        //执⾏语句 
        stmt.execute();

7. 查询操作:返回结果集ResultSet,更新操作:返回更新的数量

            //执⾏语句 
            rs = stmt.executeQuery();
            if (rs.next()) {
                book = new Book();
                book.setName(rs.getString("book_name"));
                book.setAuthor(rs.getString("book_author"));
                book.setIsbn(rs.getString("book_isbn"));
            }
            System.out.println(book);

8. 处理结果集

       } catch (SQLException e) {
            //处理异常信息 

9. 释放资源

    } finally {
         //清理资源 
         try {
            if (stmt != null) {
                stmt.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (SQLException e) {
            //
        }
    }

由于使用JDBC时,太多内容重复操作,并且步骤繁琐。为了更加方便我们操作数据库。我们对这些非个性化的步骤进行封装。因此Mybatis就诞生了。

①如果Mybatis字段和Java属性一样,Mybatis会自动进行赋值,对于不一样的字段,需要我们告诉Mybatis,字段和属性的映射关系。

最终我们发现只有第三条和第五条是需要我们手动操作的。其他的一切都有规律可循。因此我们将其他步骤进行封装,只留一个接口让你去填SQL语句和赋值的参数。其他Mybatis都帮你搞定。

-- 创建数据库 
create database if not exists library default character set utf8mb4;
-- 使⽤数据库 
use library;
-- 创建表 
create table if not exists soft_bookrack (
 book_name varchar(32) NOT NULL,
 book_author varchar(32) NOT NULL,
 book_isbn varchar(32) NOT NULL primary key
);

 以下是 JDBC 操作的具体实现代码:

package com.example.demo.mapper;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SimpleJdbcOperation {
    private final DataSource dataSource;
    public SimpleJdbcOperation(DataSource dataSource) {
         this.dataSource = dataSource;
    }
 /**
 * 添加⼀本书 
 */
    public void addBook() {
    Connection connection = null;
    PreparedStatement stmt = null;
    try {
        //获取数据库连接 
        connection = dataSource.getConnection();
        //创建语句 
        stmt = connection.prepareStatement(
            "insert into soft_bookrack (book_name, book_author, 
book_isbn) values (?,?,?);"
        );
        //参数绑定 
        stmt.setString(1, "Spring in Action");
        stmt.setString(2, "Craig Walls");
        stmt.setString(3, "9787115417305");
        //执⾏语句 
        stmt.execute();
    } catch (SQLException e) {
         //处理异常信息 
    } finally {
         //清理资源 
         try {
            if (stmt != null) {
                stmt.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (SQLException e) {
            //
        }
    }
}
 /**
 * 更新⼀本书 
 */
    public void updateBook() {
        Connection connection = null;
        PreparedStatement stmt = null;
        try {
            //获取数据库连接 
            connection = dataSource.getConnection();
            //创建语句 
            stmt = connection.prepareStatement(
                    "update soft_bookrack set book_author=? where book_isbn=?;"
            );
            //参数绑定 
            stmt.setString(1, "张卫滨");
            stmt.setString(2, "9787115417305");
            //执⾏语句 
            stmt.execute();
        } catch (SQLException e) {
            //处理异常信息 
        } finally {
            //清理资源 
            try {
                if (stmt != null) {
                    stmt.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                //
            }
        }
    }
 /**
 * 查询⼀本书 
 */
    public void queryBook() {
        Connection connection = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        Book book = null;
        try {
            //获取数据库连接 
            connection = dataSource.getConnection();
            //创建语句 
            stmt = connection.prepareStatement(
                    "select book_name, book_author, book_isbn from 
soft_bookrack where book_isbn =?"
            );
            //参数绑定 
            stmt.setString(1, "9787115417305");
            //执⾏语句 
            rs = stmt.executeQuery();
            if (rs.next()) {
                book = new Book();
                book.setName(rs.getString("book_name"));
                book.setAuthor(rs.getString("book_author"));
                book.setIsbn(rs.getString("book_isbn"));
            }
            System.out.println(book);
        } catch (SQLException e) {
            //处理异常信息 
        } finally {
            //清理资源 
            try {
                if (rs != null) {
                    rs.close();
                }
                if (stmt != null) {
                    stmt.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                //
            }
        }
    }
        public static class Book {
        private String name;
        private String author;
        private String isbn;
        //省略 setter getter ⽅法 
    }
}

二、什么是Mybatis?

MyBatis是一款优秀的持久层框架,用于简化JDBC的开发。

MyBatis本是Apache的⼀个开源项目iBatis,2010年这个项目由apache迁移到了googlecode,并 且改名为MyBatis。2013年11月迁移到Github。 

Mybatis官网:https://blog.mybatis.org

Mybatis中文网:MyBatis中文网 

持久层:指的就是持久化操作的层,通常指数据访问层(dao),是用来操作数据库(DB)的.

简单来说MyBatis是更简单完成程序和数据库交互的框架,也就是更简单的操作和读取数据库工具 接下来,我们就通过⼀个入门程序,让大家感受下一通过Mybatis如何来操作数据库

Mybatis操作数据库的步骤:

1.准备工作:创建Springboot工程、数据库表准备、实体类

2.引入Mybatis相关依赖(Mybatis Framework、Mysql Drive)、配置Mybatis数据库连接信息。

3.编写SQL语句(注解/XML)

4.测试

配置Mybatis。

我们使用配置文件application.yml类型的文件进行配置。配置代码如下

驼峰自动转换就可以让我们在数据库中存在的映射关系进行匹配成功

例如字段

delete_flag    →  deleteFlag

create_time   →createTime

updata_time  →updataTime

此时数据库的字段名称就可以与我们在实体类创建的成员属性相匹配,这样打印出来的有分割的类似这种数据库表字段updata_time 就可以被识别打印了 而不是打印的null。

三、Mybatis在IDEA中的操作方法:

3.1 第一步:准备工作(创建springboot工程、数据库表准备、实体类)

3.1.1创建springboot工程

 首先创建SpringBoot项目。选择Maven来管理项目。选择Mybatis Framework,MySQL Driver等依赖。在项目左侧的数据库栏添加配置数据库。并新建表,创建已经准备好的数据库表。

Mybatis是一个持久层框架,具体的数据存储和数据操作还是在MySQL中操作的,所以需要添加 MySQL驱动

 3.1.2数据库表准备

如我们现在要创建一个用户表,并要创建对应的实体类

 建表规范 

常见规范:

1.字段全部小写,单词之间使用_分割(数据库的字段修改代价非常大,数据量很大!)

2.建表需要具备三个字段,哪怕你不用 (id      create_time       updata_time)

使互联网上有迹可循

delete_flag删除标志(1.逻辑删除(企业)(通过更新),2.物理删除(delete))

create_time       updata_time 这两个字段使用Data类型是因为这个类型保存的时间是非常细节的,可以细致到时分秒。

--创建数据库
DROP DATABASE IF EXISTS mybatis_test;
CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;

-- 使用数据数据
USE mybatis_test;

-- 创建表 [ 用户表 ]
DROP TABLE IF EXISTS userinfo;
CREATE TABLE `userinfo` (
                 `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
                `username` VARCHAR ( 127 ) NOT NULL,
                `password` VARCHAR ( 127 ) NOT NULL,
                 `age` TINYINT ( 4 ) NOT NULL,
                `gender` TINYINT ( 4 ) DEFAULT '0' COMMENT '1 男 2 女 0 默认 ',
                 `phone` VARCHAR ( 15 ) DEFAULT NULL,
                `delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0 正常 , 1 删除 ',
                `create_time` DATETIME DEFAULT now(),
                `update_time` DATETIME DEFAULT now(),
                PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;

 -- 添加用户信息
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'admin', 'admin', 18, 1, '18612340001' );
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'zhangsan', 'zhangsan', 18, 1, '18612340002' );
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'lisi', 'lisi', 18, 1, '18612340003' );
INSERT INTO mybatis_test.userinfo ( username, `password`, age, gender, phone )
VALUES ( 'wangwu', 'wangwu', 18, 1, '18612340004' );

3.1.3对应的实体类

 配置好数据库之后,在Idea中创建一个实体类。对应数据库表中的元素

对应的实体类

实体类的属性名与表中的字段名⼀⼀对应

注意Data类型是java.util包下面的,代表的是时间日期。

3.2 第二步:引入Mybatis的相关依赖

引入Mybatis的相关依赖

引入的依赖就是上面我们选择的Mybatis Framework,MySQL Driver等依赖。在pom.xml文件中,我们会看到我们引入的依赖信息;

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

若我们在选择依赖的时候忘了选择,我们可以在pom.xml文件中右键,点击第一个选项,我们之前下载好的插件。从这里我们可以继续选择依赖。

 3.1.3 配置数据库连接字符串

①使用配置文件

配置文件代码如下

注:当引入了Mybatis框架,就一定要进行配置文件,需要配置数据库链接。负责服务就不会正常启动,就会报错。

 # 数据库连接配置
spring:
        datasource: url: jdbc:mysql://127.0.0.1:3306/mybatis_test? characterEncoding=utf8&useSSL=false
        username: root         
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver

 说明

  • url: 数据库的 JDBC 连接 URL。请确保 IP 地址、端口号、数据库名称以及连接参数正确。
  • username: 数据库用户名称(默认为root)。
  • password: 数据库用户密码(这个是安装MySQL的时候设置的密码)。
  • driver-class-name: 使用的 JDBC 驱动类,这里指定 com.mysql.cj.jdbc.Driver 是 MySQL 8.x 使用的驱动类
url: jdbc:mysql://127.0.0.1:3306/mybatis_test? characterEncoding=utf8&useSSL=false

这个url中 

jdbc: 表示Java Database Connectivity (JDBC)API进行数据库链接。

mysql:指定要连接的数据库类型为mysql

127.0.0.1:3306:这是数据库服务器的IP地址和端口号

mybatis_test:数据库名称(要确保这个数据库已经在MySQL服务器上面创建)

?:?后面这是;连接参数部分,以?开始,多个参数用&分隔。

characterEncoding=utf8&useSSL=false:设置字符编码为 UTF-8、指定不使用 SSL 加密连接。

注:

配置好了之后就可以直接访问数据库了。不过为了更直观的看到数据库中的信息。我们通常不会通过MySQL来操作。

1.我们可以使用数据库管理工具Navicat Premium来管理我们的数据库。

2.我们也可以不用配置文件。直接在IDEA中自带的数据库中直接添加数据源。

②不使用配置文件

直接在IDEA中自带的数据库中直接添加数据源。选择MySQl。

输入名称、用户名、密码、数据库名称点击确定。这样数据库就被导入进来了。

 我们可以右键这个数据库来创建表。复制我们之前准备好的数据。粘贴进去点击确定。表就被创建好了

3.3 第三步:编写SQL语句(注解/XML)

为了避免标题级别过多,文章内容过多。我们将这一步放在下一篇文章来详细说明。

3.4 第四步:测试

测试实际上很简单,只需要在我们添加了注解的SQL语句右侧右键,点击生成。再点击测试即可。

我们点击测试, 我们就会进入UserInfoMapperTest这个类。

/**
 * 是开发人员的自测代码
 */
//上面的三角运行是执行所有的Test方法
@Slf4j
@SpringBootTest //帮助我们加载Spring的运行环境
class UserInfoMapperTest {
    @Autowired//
    private UserInfoMapper userInfoMapper;//注入Mapper
    @BeforeEach
    void setUp() {//表示Test方法执行前执行
        log.info("setUp...");
    } //执行Test方法前执行

    @AfterEach
    void tearDown() {//表示Test方法执行后执行
        log.info("tearDown...");
    } //执行Test方法后执行

//    下面的三角运行符号表示执行当前Test方法
    @Test
    void queryUserList() {
        log.info(userInfoMapper.queryUserList().toString());
    }
}

报错 

这是SQL语句出错、检查SQL语句就好了。

四、Mybatis相关配置文件

4.1配置Mybatis打印日志

mybatis:
  configuration: # 配置打印 MyBatis⽇志
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

 配置后Mybatis日志。多了如下内容,更加方便我们查看数据库。这是MySQL

返回的结果。

对通过注解+SQL语句查的补充:驼峰转换

虽然我们编写了如下关于查的相关SQL语句,在MySQL放回的结果中我们可以看到delete_flage,create_time,update_time的值,但是通过Mybatis查询的结果并没有这些值,这是为什么呢?因为MySQL中的字段名是delete_flage,create_time,update_time带有分隔符,而在实体类中我们写的是deleteFlag,createTime,updateTime是不一样的。因此Mybatis并没有访问到MySQL中的相对应字段值。如何解决这个问题呢?有三种办法。

 三种办法解决deleteFlag,createTime,updateTime为null

1.改别名

2.通过注解来映射

(也可以复用通过@ResultMap注解,@ResultMap(value = “BaseResult”))

4.2 第三种:配置自动驼峰的转换

将下文添加到配置文件application.yml中,代码不做任何处理

mybatis:
  configuration:
    map-underscore-to-camel-case: true # 配置驼峰自动转换


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2129364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Python从头开始创建PowerPoint演示文稿

目录 一、环境搭建与基础知识 1.1 环境搭建 1.2 基础知识 二、创建演示文稿对象 三、添加幻灯片 3.1 选择幻灯片布局 3.2 设置幻灯片内容 3.2.1 设置标题和副标题 3.2.2 添加文本内容 3.2.3 插入图片 3.2.4 插入图表 四、高级应用&#xff1a;批量生成演示文稿 4.…

太惨了!许家印前妻每个月只能花18万

文&#xff5c;琥珀食酒社 作者 | 积溪 许家印前妻被判了 我跟你说啊她真是太惨了 一个月只能取18万啊 你说这日子怎么过啊 买个包包都不够啊&#xff01; 大家都知道许皮带爆雷&#xff08;BL&#xff09;后 丁玉梅虽然和许皮带战略性离婚&#xff0c;逃到了英国 还把…

【计算机组成原理】浮点数的表示及IEEE 754规格化

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

软件工程测试

1. 软件测试概述 通俗地说&#xff0c;软件测试是为了发现错误而执行程序的过程。 软件测试&#xff1a;根据软件开发各阶段的规格说明和程序的内部结构而精心设计一批测试用例&#xff08;即输入数据及其预期的输出结果&#xff09;&#xff0c;并利用这些测试用例去运行程序…

深度学习:基础知识

深度学习是机器学习的一个领域 神经网络构造 一个神经元有n个输入&#xff0c;每一个输入对应一个权值w&#xff0c;神经元内会对输入与权重做乘法后求和。 感知器 由两层神经元组成的神经网络--“感知器”&#xff08;Perceptron&#xff09;,感知器只能线性划分数据。 公式…

Netty笔记03-组件Channel

文章目录 Channel概述Channel 的概念Channel 的主要功能Channel 的生命周期Channel 的状态Channel 的类型channel 的主要方法 ChannelFutureCloseFuture&#x1f4a1; netty异步提升的是什么要点总结 Channel概述 Channel 的概念 在 Netty 中&#xff0c;Channel 是一个非常重…

c++题目_洛谷 / 题目详情 P1012 [NOIP1998 提高组] 拼数

# [NOIP1998 提高组] 拼数 ## 题目描述 设有 $n$ 个正整数 $a_1 \dots a_n$&#xff0c;将它们联接成一排&#xff0c;相邻数字首尾相接&#xff0c;组成一个最大的整数。 ## 输入格式 第一行有一个整数&#xff0c;表示数字个数 $n$。 第二行有 $n$ 个整数&#xff0c;表…

Linux学习笔记(黑马程序员,前四章节)

第一章 快照 虚拟机快照&#xff1a; 通俗来说&#xff0c;在学习阶段我们无法避免的可能损坏Linux操作系统&#xff0c;如果损坏的话&#xff0c;重新安装一个Linux操作系统就会十分麻烦。VMware虚拟机支持为虚拟机制作快照。通过快照将当前虚拟机的状态保存下来&#xff0c;…

Leetcode 移动零

要求将数组中的所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。下面是该题的 C 解决方案&#xff1a; class Solution { public:void moveZeroes(vector<int>& nums) {int nonZeroPos 0; // 记录非零元素应该放置的位置// 遍历数组&#xff0c;…

镭速如何高效使用SQLite数据库高速传输结构化数据文件传输

SQLite数据库因其小巧、跨平台和无需配置的特性&#xff0c;在众多关系型数据库中独树一帜。与传统的服务器/客户端架构数据库&#xff0c;例如MySQL不同&#xff0c;SQLite通常被嵌入到应用程序中作为一个库。它不仅支持创建索引&#xff0c;还严格遵循ACID原则&#xff0c;非…

先攒一波硬件,过几年再给电脑升级,靠谱吗?想啥呢?

前言 最近有小伙伴发来消息&#xff1a;我可以今年先买电脑的部分硬件&#xff0c;明年再买电脑的另一部分硬件&#xff0c;再组装起来不就是一台电脑了吗&#xff1f; 这确实是一个很好的办法。 我还记得大学有个室友&#xff0c;从大一每个月省吃俭用&#xff0c;攒下的钱…

全球AI应用市场洞察:市场快速崛起,“陪伴式AI”、“图像AI”应用爆款频出!

自2023年 GPT4 的问世以来&#xff0c;得益于这股新的技术变量&#xff0c;各类 AI 应用在市场上遍地开花。在这轮热潮之下&#xff0c;不同市场和细分赛道有何机遇和挑战&#xff1f;以下根据 Sensor Tower 最新报告分析&#xff0c;帮助移动开发者、广告主洞察全球AI应用市场…

Linux系统:chgrp命令

1、命令详解&#xff1a; chgrp命令是Linux操作系统中用于修改文件或目录的所属组的命令。chgrp命令还可以修改链接文件的所属组&#xff0c;而不是链接所指向的文件的所属组。默认情况下&#xff0c;只有文件的所有者和超级用户才能修改文件的所属组&#xff0c;但如果用户是一…

JavaWeb【day08】--(MySQL-Mybatis入门)

数据库开发-MySQL 1. 多表查询 1.1 概述 1.1.1 数据准备 SQL脚本&#xff1a; #建议&#xff1a;创建新的数据库 create database db04; use db04; ​ -- 部门表 create table tb_dept (id int unsigned primary key auto_increment comment 主键ID,name v…

Win32编程:创建属于你的第一个窗口

目录 一、Win32程序的运行过程 二、创建Windows窗口 1、分析 2、完整代码 一、Win32程序的运行过程 Win32编程&#xff0c;也称为Windows编程。运行步骤主要包含&#xff1a;加载程序&#xff08;操作系统加载程序的可执行文件&#xff08;exe格式&#xff09;到内存中,创建…

代码随想录刷题day30丨452. 用最少数量的箭引爆气球, 435. 无重叠区间,763.划分字母区间

代码随想录刷题day30丨452. 用最少数量的箭引爆气球&#xff0c; 435. 无重叠区间&#xff0c;763.划分字母区间 1.题目 1.1用最少数量的箭引爆气球 题目链接&#xff1a;452. 用最少数量的箭引爆气球 - 力扣&#xff08;LeetCode&#xff09; 视频讲解&#xff1a;贪心算法…

ColorThief的介绍与使用

概述 colorThief是一个 Javascript 插件&#xff0c;支持在浏览器端或 Node 环境中使用。Thief的中文意思是偷窃、小偷。colorThief的作用就是通过算法去获取图片的色源。 API 介绍与示例 colorThief提供两个方法&#xff0c;getColor和getPalette&#xff0c;这两个方法在 …

类和对象(c++)

欢迎来到本期频道&#xff01; 类和对象 类定义&#xff1a;格式&#xff1a;类域&#xff1a;访问限定符友元内部类this指针静态与非静态成员关系类型转换六大默认成员函数&#xff08;C98&#xff09;1️⃣构造函数2️⃣拷贝构造函数浅拷贝与深拷贝 3️⃣赋值重载拷贝函数4️…

x-cmd pkg | superfile: 终端文件管理器,界面精致美观

目录 简介快速上手功能特点竞品和相关项目进一步阅读 简介 superfile 是 github.com/yorukot 用 Go 开发的终端文件管理器&#xff0c;相比于其他终端文件管理器&#xff0c;它最显著的特点是 UI 精致美观。 Tip Superfile 采用了特殊的 Unicode 符号来标识各种类型的文件&…

【我的 PWN 学习手札】Unsortedbin Leak

前言 从前都是野路子学习&#xff0c;学校时间也比较紧张&#xff0c;没有能够好好总结。一直有做个人笔记的习惯&#xff0c;但是学习路线比较分散盲目&#xff0c;虽然跟着wiki做&#xff0c;但是也面临知识点不全的窘境。近期开始跟着课程系统的学习&#xff0c;对于老的知…