【Mybatis】Mybatis基础(中)

目录

1、作用域(Scope)和生命周期

2、解决属性名和字段名不一致的问题

2.1、SQL别名

2.2、结果映射(resultMap)

3、日志

3.1、日志工厂

3.2、log4j

4、分页

4.1、Limit分页

4.2、RowBounds分页(了解即可)

4.3、分页插件(了解即可)

5、使用注解开发

5.1、操作实例

5.2、通过注解进行CRUD

5.3、@Param()注解

6、Mybatis基层运行

7、Lombok使用

7.1、简介

7.2、使用步骤

8、多对一处理

8.1、环境搭建

8.2、按查询嵌套处理

8.3、按结果嵌套处理

9、一对多处理

9.1、环境搭建

9.2、按查询嵌套处理

9.3、按结果嵌套处理

9.4、小结


1、作用域(Scope)和生命周期

不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题

SqlSessionFactoryBuilder:

  • 这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。

  • 局部变量

SqlSessionFactory:

  • SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。

  • 相当于数据库连接池

  • 因此 SqlSessionFactory 的最佳作用域是应用作用域。最简单的就是使用单例模式或者静态单例模式。

SqlSession:

  • 连接到连接池的一个请求

  • SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求方法作用域。

  • 用完之后需要关闭,否则会资源占用

每一个mapper代表一个具体的业务(就是去执行SQL)。

2、解决属性名和字段名不一致的问题

数据库中的字段

User实体类

public class User {
    private int id;
    private String name;
    private String password;//字段不同
    ......}

结果

2.1、SQL别名

MyBatis 会在幕后自动创建一个 ResultMap,再根据属性名来映射列到 JavaBean 的属性上。如果列名和属性名不能匹配上,可以在 SELECT 语句中设置列别名(这是一个基本的 SQL 特性)来完成匹配.

<select id="getUserList" resultType="user">
    select id,name,pwd as password from mybatis.user
</select>

2.2、结果映射(resultMap)

在学习了上面的知识后,你会发现上面的例子没有一个需要显式配置 ResultMap,这就是 ResultMap 的优秀之处——你完全可以不用显式地配置它们。 虽然上面的例子不用显式配置 ResultMap。 但为了讲解,我们来看看如果在刚刚的示例中,显式使用外部的 resultMap 会怎样,这也是解决列名不匹配的另外一种方式。

在UserMapper.xml上加上以下代码:

<!--哪个不一样,就转换哪个-->
<resultMap id="userResultMap" type="user">
    <result property="password" column="pwd"/>
</resultMap>

然后在引用它的语句中设置 resultMap 属性就行了(注意我们去掉了 resultType 属性)。

<!--这是一个查询语句-->
<select id="getUserList" resultMap="userResultMap">
    select * from mybatis.user
</select>

3、日志

3.1、日志工厂

在对数据库进行操作时,如果出现了异常,就需要排错,最好的方法就是看日志

Mybatis 通过使用内置的日志工厂提供日志功

能。内置日志工厂将会把日志工作委托给下面的实现之一:

  • SLF4J

  • Apache Commons Logging

  • Log4j 2

  • Log4j【掌握】

  • JDK logging

  • Commons_Logging

  • Stdout_Logging【掌握】

  • No_Logging

在mybatis具体使用哪一个日志实现,在设置中设定

Stdout_Logging:标准日志输出

注意:写settings的时候一定要注意在配置文件的位置,上面有讲到过。

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

下面我将select语句里面的*去除,看看日志文件怎么显示

<!--这是一个查询语句-->
<select id="getUserList" resultMap="userResultMap">
    select  from mybatis.user
</select>

3.2、log4j

什么是log4j:

  • Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程

  • 我们也可以控制每一条日志的输出格式

  • 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程

  • 能通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

操作步骤:

1.导入log4j依赖包

<dependencies>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>

2.在resources下创建log4j.properties配置文件

### 配置根 ###
log4j.rootLogger = debug,console,file
​
### 设置输出sql的级别,其中logger后面的内容全部为jar包中所包含的包名 ###
log4j.logger.org.apache=dubug
log4j.logger.java.sql.Connection=dubug
log4j.logger.java.sql.Statement=dubug
log4j.logger.java.sql.PreparedStatement=dubug
log4j.logger.java.sql.ResultSet=dubug
​
### 配置输出到控制台 ###
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = [%c]-%m%n
​
### 配置输出到文件 ###
log4j.appender.file = org.apache.log4j.FileAppender
log4j.appender.file.File = ./logs/log.log
log4j.appender.file.Append = true
log4j.appender.file.Threshold = DEBUG
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

3.在mybatis-config.xml内配置log4j为日志实现

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

运行后会自动添加一个log日志文件:

 

注意:要注意你的maven是否导入,记得刷新一下maven,要不然会报错哦

4.使用log4j(本次实验在测试类中写的)

  • 如何有一个地方报错了,可以通过日志的一些方法找到错误的地方,然后通过查看日志文件的信息去看错误到底是什么

1.在要使用Log4j的类中导入包

import org.apache.log4j.Logger;

2.获取日志对象,参数为当前类的class

static Logger logger = Logger.getLogger(UserDaoTest.class);

3.想去哪找错就去哪使用logger

......
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            //以前的旧方法
            //System.out.println(sqlSession);
            //使用日志文件来看错误地方
            logger.info("info:测试,获取mapper成功!!");
            ........

4.日志文件显示

5.日志级别

logger.info("info:测试,获取mapper成功!!");
logger.debug("debug:测试,获取mapper成功!!");
logger.error("error:测试,获取mapper成功!!");

6.项目结构

logi4j详细配置文件可以查看:log4j.properties配置文件详解 - 小白知浅 - 博客园

4、分页

为什么要使用分页?

  • 当数据量过大时,处理数据就会很麻烦,而分页就是减少数据的处理量

4.1、Limit分页

通过SQL语句的limit进行分页

select * from user limit 0,3;
#select * from user limit startIndex,pageSize;--->从startIndex,取pageSize个数据

使用mybatis实现分页:

1.接口

//分页
List<User> getUserByLimit(Map<String,Object> map);

2.Mapper.xml

<!--这是一个分页查询语句-->
<select id="getUserByLimit" parameterType="map" resultMap="userResultMap">
    select * from mybatis.user limit #{startIndext},#{pageSize}
</select>

注意:

  • 在这里,你的parameterType值是map,只有基础类可以省略哦,缩写不要随便写,可看官方文档

  • resultMap,因为之前写了resultMap映射,所以写这个,没有写的话就是resultType,不知道的可以看前面讲的8.2结果映射(resultMap)

3.测试

@Test
public void getUserByLimit(){
    SqlSession sqlSession = MybatisUtils.getsqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
​
    HashMap<String, Object> map = new HashMap<>();
    map.put("startIndext",0);
    map.put("pageSize",3);
​
    List<User> userList = mapper.getUserByLimit(map);
    for (User user : userList) {
        System.out.println(user);
    }
    sqlSession.close();
}

4.结果展示


4.2、RowBounds分页(了解即可)

不使用SQL实现分页,而是java代码进行分页操作,但不推荐。

1.接口

//RowBounds分页
List<User> getUserByRowBounds();

2.mapper.xml

<!--通过RowBounds进行分页查询语句-->
<select id="getUserByRowBounds" resultMap="userResultMap">
    select * from mybatis.user
</select>

3.测试

//通过RowBounds进行分页查询
@Test
public void getUserByRowBounds(){
    SqlSession sqlSession = MybatisUtils.getsqlSession();
    //RowBounds实现
    RowBounds rowBounds = new RowBounds(1, 2);
    //通过Java代码层实现分页
    List<User> userList = sqlSession.selectList("com.wen.Dao.UserMapper.getUserByRowBounds", null, rowBounds);
    for (User user : userList) {
        System.out.println(user);
    }
    sqlSession.close();
}

4.结果

4.3、分页插件(了解即可)

分页插件自己写的小项目可以不用写,写SQL还可以练练手,但以后写大项目的时候可能会用到这个,详情请看:MyBatis 分页插件 PageHelper分页插件 | MyBatis-Plus

5、使用注解开发

对于像 BlogMapper 这样的映射器类来说,还有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如,上面的 XML 示例可以被替换成如下的配置:

package org.mybatis.example;
public interface BlogMapper {
    @Select("SELECT * FROM blog WHERE id = #{id}")
    Blog selectBlog(int id);
}

使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。

选择何种方式来配置映射,以及认为是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松的在基于注解和 XML 的语句映射方式间自由移植和切换。

5.1、操作实例

实例:

1、在接口上使用注解

//查询全部用户
@Select("select * from user")
List<User> getUserList();

2、在mybatis-config.xml配置文件中绑定接口

<!--绑定接口-->
<mappers>
    <mapper class="com.wen.Dao.UserMapper"/>
</mappers>

3、测试

@Test
public void test(){
    SqlSession sqlSession = MybatisUtils.getsqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> userList = mapper.getUserList();
    for (User user : userList) {
        System.out.println(user);
    }
    sqlSession.close();
}

4、结果

本质:反射机制实现

底层:动态代理(在Spring中讲到过)

5.2、通过注解进行CRUD

由于操作雷同,所以接口上写上所有操作,但运行只运行一个增加操作即可

接口:

//查询全部用户
@Select("select * from user")
List<User> getUserList();
​
//方法存在多个参数时,所有的参数前面都必须加上@Param("id")注解
@Select("select * from user where id = #{id}")
User getUserByID(int id);
​
//配置文件取参数的时候取的是@Param()里的参数!!!
@Select("select * from user where id = #{id} and pwd = #{pwd}")
User getUser(@Param("id") int id,@Param("pwd") String pwd);
​
//添加,对象就不用加@Param()
@Insert("insert into user(id,name,pwd) values(#{id},#{name},#{pwd})")
int addUser(User user);
​
//删除
@Delete("delete from user where id = #{id}")
int deleteUserByID(int id);
​
//修改
@Update("update user set name = #{username},pwd = #{userpwd} where id = #{userid}")
int updateUser(User user);

测试:

public class UserDaoTest {
    @Test
    public void test(){
        SqlSession sqlSession = MybatisUtils.getsqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        int user = mapper.addUser(new User(4, "老四", "456"));
        List<User> userList = mapper.getUserList();
        for (User user1 : userList) {
            System.out.println(user1);
        }
        sqlSession.close();
    }
}

注意:我们在使用修改语句的时候需要提交事务: sqlSession.commit();

但可以在MybatisUtils工具类里的getsqlSession()方法中,提交事务为true,使其自动提交事务。

//有了sqlSessionFactory就可以获取sqlSession的实例
//sqlSession完全包含了面向数据库执行SQL命令所需的方法
public static SqlSession getsqlSession(){
    return sqlSessionFactory.openSession(true);
}

结果:

5.3、@Param()注解

  • 多个基本类型的参数或者String类型需要加上

  • 引用类型不需要添加

  • 只有一个基本类型,可以忽略,建议加上

  • 在SQL中传入的值是@Param()内的值

6、Mybatis基层运行

通过debug查看操作流程

7、Lombok使用

7.1、简介

Lombok项目是一个Java库,它会自动插入编辑器和构建工具中,Lombok提供了一组有用的注释,可以通过简单的注解的形式来帮助我们简化和消除一些必须有但显得很臃肿的Java代码,比如常见的Getter and Setter、构造函数、toString()等等。用来消除Java类中的大量样板代码,简洁且易于维护的Java类。

常用注解:

@Setter :注解在类或字段,注解在类时为所有字段生成setter方法,注解在字段上时只为该字段生成setter方法。
@Getter :使用方法同上,区别在于生成的是getter方法。
@ToString :注解在类,添加toString方法。
@EqualsAndHashCode: 注解在类,生成hashCode和equals方法。
@NoArgsConstructor: 注解在类,生成无参的构造方法。
@RequiredArgsConstructor: 注解在类,为类中需要特殊处理的字段生成构造方法,比如final和被@NonNull注解的字段。
@AllArgsConstructor: 注解在类,生成包含类中所有字段的构造方法。
@Data: 注解在类,生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。
@Slf4j: 注解在类,生成log变量,严格意义来说是常量。

7.2、使用步骤

1.在IDEA中安装Lombok插件

File --> Settings --> Plugins -->搜索lombok -->下载即可

2.在项目中导入lombok的jar包

在maven项目的pom.xml内写入lombok的依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.10</version>
</dependency>

注意:记得看一下有没有加载进去

3.使用注解

  • @Data: 注解在类,生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。

  • @AllArgsConstructor: 注解在类,生成包含类中所有字段的构造方法。

  • @NoArgsConstructor: 注解在类,生成无参的构造方法。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String pwd;
​
}

查看方法:

8、多对一处理

本次实验以书和书籍类作比方

  • 对书而言,多本同类型的书籍关联一个类型,如计算机类【多对一】

  • 对书籍类型而言,一个计算机类型集合了很多本计算机书籍【一对多】

首先,创建数据库,本次要做的便是联表查询

创建书本类型和书的表:

类型表:
CREATE TABLE `type` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
​
图书表:
CREATE TABLE `book` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `tid` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `tid` (`tid`),
  CONSTRAINT `tid` FOREIGN KEY (`tid`) REFERENCES `type` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

8.1、环境搭建

1.新建实体类Book,BookType

由于之前讲到了lombok,所以这里我们使用它,主要是方便。

@Data
public class Book {
    private int id;
    private String name;
    //书本要关联一个类型,所以需要一个对象:每个书本都对应一个对象
    private BookType tid;
}
@Data
public class BookType {
    private int id;
    private String name;
}

2.建立Mapper接口

先测试一下环境,先通过id来查询一本书,这里用的是注解来实现查询

public interface BookMapper {
    @Select("select * from book where id = #{id}")
    Book getBook(@Param("id") int id);
}
public interface BookTypeMapper {
}

3.建立Mapper.xml

为了美观Mapper.xml建议建在resource下,注意:包名尽量相同

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
​
<mapper namespace="com.wen.Dao.BookMapper">
​
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
​
<mapper namespace="com.wen.Dao.BookTypeMapper">
​
</mapper>

4.在mybatis-config.xml中注册Mapper.xml

<!--注册Mapper-->
<mappers>
    <mapper resource="com.wen.Dao/BookMapper.xml"/>
    <mapper resource="com.wen.Dao/BookTypeMapper.xml"/>
</mappers>

5.测试

@Test
public void test(){
    SqlSession sqlSession = MybatisUtils.getsqlSession();
    BookMapper mapper = sqlSession.getMapper(BookMapper.class);
    Book book = mapper.getBook(1);
    System.out.println(book);
    sqlSession.close();
}

6.结果

7.项目结构

注意:记得检查你的*Mapper.xml有没有成功录入你的classes文件中,要不然找不到的。

8.2、按查询嵌套处理

目标:查询所有书本信息以及与其相对应的书本类型

select b.id,b.name,t.name from book b,type t where b.id = t.id;

思路:接口方法- -> 对应的mapper.xml方法 --->test

查询方法解析:

首先写了两个查询语句,第一个select * from book 是查询书本信息的,现在我们需要通过书本的tid来查该书本所属类别,所以需要再写一个select * from where id = #{tid}的查询语句从type里查询出来,然后通过resultMap的方式将数据整合在一起。

1.接口

public List<Book> getBookList();

2.*Mapper.xml里添加查询方法

注意:在resultMap中,复杂的属性不用result

  • 对象 ---> association

  • 集合 ---> collection

  • JavaType ---> 指定属性的类型

  • ofType ---> 集合中泛型信息

后续会涉及到的。

<!--namespace:绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.wen.dao.BookMapper">
    <!--    查询所有书籍信息
        根据查询的信息的tid寻找对应的type
-->
    <select id="getBookList" resultMap="book&amp;type">
        select * from book
    </select>
​
    <!--通过resultMap来对两个查询做联合处理-->
    <resultMap id="book&amp;type" type="book">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <!--复杂的属性,需要单独处理,不用result, booktype是book中引用的一个对象所以要用association处理
     javaType(指定属性类型):确定该类型是BookType类 select:通过下面的查询,给其tid查BookType的数据-->
        <association property="booktype" column="tid" javaType="BookType" select="getType"/>
    </resultMap>
​
    <select id="getType" resultType="booktype">
        select * from type where id = #{tid}
    </select>
</mapper>

3.结果

8.3、按结果嵌套处理

方法解析:

直接写一个查询语句,然后在看需要什么再提数据(个人觉得方法二要好理解)

select b.id bid,b.name bname,t.name tname
from book b,type t
where b.id = t.id;

1、Mapper接口

public List<Book> getBookList2();

2、*Mapper.xml里添加查询方法

<!--按结果嵌套处理-->
<select id="getBookList2" resultMap="book&amp;type2">
    select b.id bid,b.name bname,t.name tname
    from book b,type t
    where b.id = t.id;
</select>
​
<resultMap id="book&amp;type2" type="Book">
    <result property="id" column="bid"/>
    <result property="name" column="bname"/>
    <association property="booktype" javaType="BookType">
        <result property="name" column="tnaem"/>
    </association>
</resultMap>

9、一对多处理

9.1、环境搭建

与上文多对一处理的环境相差不多,不在做详细介绍。只说差别

1.实体类不同:

具体表现在book里没有booktype类,而booktype类里添加一个书本的集合。

@Data
public class Book {
    private int id;
    private String name;
}
@Data
public class BookType {
    private int id;
    private String name;
    //一个类别包含了许多书
    private List<Book> books;//特别注意一下,这里是一个集合,把所有书籍信息放到这个集合内
}

2.接口方法不同

在BookTypeMapper内添加获取书籍的方法

//获取一个类包含的所有书
List<BookType> getBooks();

3.*Mapper.xml方法不同

在BookTypeMapper.xml中添加查询方法

<!--namespace:绑定一个对应的Dao/Mapper接口-->
<mapper namespace="com.wen.dao.BookTypeMapper">
    <select id="getBooks" resultType="booktype">
        select * from type
    </select>
</mapper>

4.测试是否能查询出来

@Test
public void getBookList(){
    SqlSession sqlSession = MybatisUtils.getsqlSession();
    BookTypeMapper mapper = sqlSession.getMapper(BookTypeMapper.class);
    List<BookType> books = mapper.getBooks();
    for (BookType book : books) {
        System.out.println(book);
    }
    sqlSession.close();
}

5.结果

可以看到后面的书籍为空,所以就要开始联表查询了

9.2、按查询嵌套处理

目的:获取指定书籍类下的所有书籍信息

select t.id,t.name,b.name from book b,type t where t.id = b.tid and t.id = 1 ;

思路:接口方法- -> 对应的mapper.xml方法 --->test

1.创建Mapperd 的获取方法

//获取指定书籍类下的所有书籍信息
BookType getBookType(int id);

2.在相应的mapper.xml里添加对应的查询语句

<!--按查询嵌套处理-->
<select id="getBookType2" resultMap="Type&amp;Book2">
    select * from type where id = #{id}
</select>
​
<resultMap id="Type&amp;Book2" type="booktype">
    <!--因为id,name和表内的相同,所以可以省略-->
    <collection property="books" ofType="Book" javaType="ArrayList" select="getBook" column="id"/>
</resultMap>
​
<select id="getBook" resultType="book">
    select * from book where tid = #{id}
</select>

3.测试

@Test
public void getBookType2(){
    SqlSession sqlSession = MybatisUtils.getsqlSession();
    BookTypeMapper mapper = sqlSession.getMapper(BookTypeMapper.class);
    BookType bookType = mapper.getBookType2(1);
    System.out.println(bookType);
    sqlSession.close();
}

4.结果

9.3、按结果嵌套处理

与前面多对一差不多,就是直接写一个查询语句,然后看根据需要什么,添加什么

1.Mapper接口

//获取指定书籍类下的所有书籍信息
BookType getBookType(int id);

2.*Mapper.xml里添加查询方法

<!--按结果嵌套处理-->
<select id="getBookType" resultMap="Type&amp;Book">
    select t.id tid,t.name tname,b.name bname
    from book b,type t
    where t.id = b.tid and t.id = 1
</select>
​
<resultMap id="Type&amp;Book" type="booktype">
    <result property="id" column="tid"/>
    <result property="name" column="tname"/>
    <!--复杂的属性,需要单独处理
            对象:association
            集合:collection
            javaType:指定属性类型
            ofType:指定泛型里的类型
     -->
    <collection property="books" ofType="Book">
        <result property="id" column="bid"/>
        <result property="name" column="bname"/>
    </collection>
</resultMap>

3.测试

@Test
public void getBookType(){
    SqlSession sqlSession = MybatisUtils.getsqlSession();
    BookTypeMapper mapper = sqlSession.getMapper(BookTypeMapper.class);
    BookType bookType = mapper.getBookType(1);
    System.out.println(bookType);
    sqlSession.close();
}

4.结果

9.4、小结

  • 关联(对象)---->association---->多对一

  • 集合---->collection---->一对多

  • JavaType ---> 指定属性的类型

  • ofType ---> 集合中泛型信息,用来指定映射到List或集合中的pojo(实体类)类型,是泛型中的约束----->List<Book>---->Book

猜你喜欢

转载自blog.csdn.net/m0_46313726/article/details/121386351