Mybatis动态Sql的作用

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: Mybatis动态Sql的作用

一、mybatis动态SQL

update 表名 set name=?,age=? where id=?

如果我们的前台没有传参,比如没有传入我们的name值,name就会把字段值改为null,这就违背了我们编码的初衷。

许多人会使用类似于where 1 = 1 来作为前缀,在代码中会用if判断是否为null,再用and进行一个sql拼接。

我们可以用常用的几个标签:

1、if

再if标签里面,test里面的条件满足了,才会把后面的sql语句进行拼接

<!-- 示例 -->
<select id="updateByPrimaryKeySelective" parameterType="com.xzs.model.Book">
        SELECT * FROM Book WHERE price>= 9.9
        <if test="name != null and name != ''">
            AND name like '%${name}%'
        </if>
</select>

2、foreach

我们再调用删除的方法的时候,通常会与SQL语句中的IN查询条件结合使用。

delete from student where id in()

parameterType为List(链表)或者Array(数组),后面在引用时,参数名必须为list或者array。如在foreach标签中,collection属性则为需要迭代的集合,由于入参是个List,所以参数名必须为list。

<select id="selectById" resultType="com.xzs.model.Book" parameterType="java.util.List" >
      select
      <include refid="Base_Column_List"/>
      from t_mvc_book
      where bid in
      <foreach collection="bids" item="bid" open="(" separator="," close=")">
          #{bid}
      </foreach>
  </select>

测试一下结果

 

3、choose、when、otherwise

你可以使用choosewhenotherwise元素来实现类似于switch语句的逻辑

<select id="getUser" parameterType="int" resultType="User">
  SELECT * FROM users
  WHERE 1=1
  <choose>
    <when test="id != null">
      AND id = #{id}
    </when>
    <when test="name != null">
      AND name = #{name}
    </when>
    <otherwise>
      AND status = 'ACTIVE'
    </otherwise>
  </choose>
</select>

如果传入的id不为空,则会在SQL语句中包含AND id = #{id}这个条件;如果传入的name不为空,则会在SQL语句中包含AND name = #{name}这个条件;否则,会在SQL语句中包含AND status = 'active'这个条件。

二、mybatis模糊查询

我们写入一个xml文件,log4j2.xml 用于打印出来你的sql语句

<?xml version="1.0" encoding="UTF-8"?>
<!-- status : 指定log4j本身的打印日志的级别.ALL< Trace < DEBUG < INFO < WARN < ERROR
  < FATAL < OFF。 monitorInterval : 用于指定log4j自动重新配置的监测间隔时间,单位是s,最小是5s. -->
<Configuration status="WARN" monitorInterval="30">
    <Properties>
        <!-- 配置日志文件输出目录 ${sys:user.home} -->
        <Property name="LOG_HOME">/root/workspace/lucenedemo/logs</Property>
        <property name="ERROR_LOG_FILE_NAME">/root/workspace/lucenedemo/logs/error</property>
        <property name="WARN_LOG_FILE_NAME">/root/workspace/lucenedemo/logs/warn</property>
        <property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t-%L] %-5level %logger{36} - %msg%n</property>
    </Properties>
    <Appenders>
        <!--这个输出控制台的配置 -->
        <Console name="Console" target="SYSTEM_OUT">
            <!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
            <ThresholdFilter level="trace" onMatch="ACCEPT"
                             onMismatch="DENY" />
            <!-- 输出日志的格式 -->
            <!-- %d{yyyy-MM-dd HH:mm:ss, SSS} : 日志生产时间 %p : 日志输出格式 %c : logger的名称
                %m : 日志内容,即 logger.info("message") %n : 换行符 %C : Java类名 %L : 日志输出所在行数 %M
                : 日志输出所在方法名 hostName : 本地机器名 hostAddress : 本地ip地址 -->
            <PatternLayout pattern="${PATTERN}" />
        </Console>
        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用 -->
        <!--append为TRUE表示消息增加到指定文件中,false表示消息覆盖指定的文件内容,默认值是true -->
        <File name="log" fileName="logs/test.log" append="false">
            <PatternLayout
                    pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </File>
        <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size, 则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档 -->
        <RollingFile name="RollingFileInfo" fileName="${LOG_HOME}/info.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
            <ThresholdFilter level="info" onMatch="ACCEPT"
                             onMismatch="DENY" />
            <PatternLayout
                    pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
            <Policies>
                <!-- 基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour。 modulate=true用来调整时间:比如现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am. -->
                <!-- 关键点在于 filePattern后的日期格式,以及TimeBasedTriggeringPolicy的interval, 日期格式精确到哪一位,interval也精确到哪一个单位 -->
                <!-- log4j2的按天分日志文件 : info-%d{yyyy-MM-dd}-%i.log -->
                <TimeBasedTriggeringPolicy interval="1"
                                           modulate="true" />
                <!-- SizeBasedTriggeringPolicy:Policies子节点, 基于指定文件大小的滚动策略,size属性用来定义每个日志文件的大小. -->
                <!-- <SizeBasedTriggeringPolicy size="2 kB" /> -->
            </Policies>
        </RollingFile>
        <RollingFile name="RollingFileWarn" fileName="${WARN_LOG_FILE_NAME}/warn.log"
                     filePattern="${WARN_LOG_FILE_NAME}/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log">
            <ThresholdFilter level="warn" onMatch="ACCEPT"
                             onMismatch="DENY" />
            <PatternLayout
                    pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="2 kB" />
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 -->
            <DefaultRolloverStrategy max="20" />
        </RollingFile>
        <RollingFile name="RollingFileError" fileName="${ERROR_LOG_FILE_NAME}/error.log"
                     filePattern="${ERROR_LOG_FILE_NAME}/$${date:yyyy-MM}/error-%d{yyyy-MM-dd-HH-mm}-%i.log">
            <ThresholdFilter level="error" onMatch="ACCEPT"
                             onMismatch="DENY" />
            <PatternLayout
                    pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
            <Policies>
                <!-- log4j2的按分钟 分日志文件 : warn-%d{yyyy-MM-dd-HH-mm}-%i.log -->
                <TimeBasedTriggeringPolicy interval="1"
                                           modulate="true" />
                <!-- <SizeBasedTriggeringPolicy size="10 MB" /> -->
            </Policies>
        </RollingFile>
    </Appenders>
    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效 -->
    <Loggers>
        <!--过滤掉spring和mybatis的一些无用的DEBUG信息 -->
        <logger name="org.springframework" level="INFO"></logger>
        <logger name="org.mybatis" level="INFO"></logger>
        <!-- 第三方日志系统 -->
        <logger name="org.springframework" level="ERROR" />
        <logger name="org.hibernate" level="ERROR" />
        <logger name="org.apache.struts2" level="ERROR" />
        <logger name="com.opensymphony.xwork2" level="ERROR" />
        <logger name="org.jboss" level="ERROR" />
        <!-- 配置日志的根节点 -->
        <root level="all">
            <appender-ref ref="Console" />
            <appender-ref ref="RollingFileInfo" />
            <appender-ref ref="RollingFileWarn" />
            <appender-ref ref="RollingFileError" />
        </root>
    </Loggers>
</Configuration>

 

1、mybatis模糊查询的三种方式

1.第一种

<select id="like01" resultType="com.xzs.model.Book" parameterType="java.util.List">
        select
        <include refid="Base_Column_List"/>
        from t_mvc_book
        where bname like #{bname}
    </select>

测试的结果

 @Test
    public void like01() {
        bookbiz.like01("%圣%").forEach(System.out::println);
    }

 

第二种:需要单引号,需要单引号,传参是占位符的形式

 <select id="like02" resultType="com.xzs.model.Book" parameterType="java.util.List">
        select
        <include refid="Base_Column_List"/>
        from t_mvc_book
        where bname like '${bname}'
    </select>

测试的结果

  @Test
    public void like02() {
        bookbiz.like02("%圣%").forEach(System.out::println);
    }

 

第三种

<select id="like03" resultType="com.xzs.model.Book" parameterType="java.util.List">
        select
        <include refid="Base_Column_List"/>
        from t_mvc_book
        where bname like concat('%',#{bname},'%')
    </select>

测试的结果

    @Test
    public void like03() {
        bookbiz.like03("%圣%").forEach(System.out::println);
    }

 

2、mybatis中#与$的区别

  1. $是占位符赋值,#是预处理SQL。
  2. 外在形式,$传参不带引号'',#传参自带引号''。
  3. $传参存在sql注入,#不存在。
  4. $可以用来做动态列,完成动态sql的开发。

 三、MyBatis结果映射(单表多表查询)

<resultMap id="BaseResultMap" type="com.xzs.model.Book">
        <constructor>
            <idArg column="bid" jdbcType="INTEGER" javaType="java.lang.Integer"/>
            <arg column="bname" jdbcType="VARCHAR" javaType="java.lang.String"/>
            <arg column="price" jdbcType="REAL" javaType="java.lang.Float"/>
        </constructor>
    </resultMap>

这个代码中,column属性写的是数据库里面的字段,里面有一个name的属性,是实体类里面的字段,当数据库里面的字段喝实体类里面的字段是一样的可以不用写name属性;

如果数据库里面出现了下划线,比如data_time,就需要在

<arg column="data_time",name="DataTime" jdbcType="VARCHAR" javaType="java.lang.String"/>

使用mybatis的各种场景

使用mybatis的各种场景,返回的结果是多样的,resultType/resultMap?

  1. 返回单表的对应的实体类,仅有一个查询结果,可以用resultType/resultMap。
  2. 使用了resultType
 <select id="selectresultType" resultType="com.xzs.model.Book" parameterType="java.lang.Integer">
        select
        <include refid="Base_Column_List"/>
        from t_mvc_book
        where bid = #{bid,jdbcType=INTEGER}
    </select>
  1. 2.编写接口实现接口
Book selectresultType(Integer bid);
    @Override
    public Book selectresultType(Integer bid) {
        return bookMapper.selectresultType(bid);
    }
  1. 3.测试方法
    @Test
    public void selectresultType() {
        System.out.println("测试resultType查询");
        Book book = bookbiz.selectByPrimaryKey(12);
        System.out.println(book);
    }
  1. 4.测试结果




    2. 返回单表的对应的实体类,有多个查询结果,可以用resultType/resultMap
    1.resultType/resultMap。resultType一个或多个都用实体对象
<select id="selectresultType01" resultType="com.xzs.model.Book">
        select
        <include refid="Base_Column_List"/>
        from t_mvc_book
    </select>
    <select id="selectresultMap01" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from t_mvc_book
    </select>
  1. 2.编写接口方法和实现接口
    List<Book> selectresultType01();
    List<Book> selectresultMap01();
@Override
    public List<Book> selectresultType01() {
        return bookMapper.selectresultType01();
    }
    @Override
    public List<Book> selectresultMap01() {
        return bookMapper.selectresultMap01();
    }
  1. 3.测试方法
    @Test
    public void selectresultType01() {
        bookbiz.selectresultType01().forEach(System.out::println);
    }
    @Test
    public void selectresultMap01() {
        bookbiz.selectresultMap01().forEach(System.out::println);
    }
  1. 4.测试结果是一样的

    3.返回多表的对应的实体类,仅有一个查询结果,通常用resultType,也可以用resultMap
    4.返回多表的对应的实体类,有多个查询结果,通常用resultType,也可以用resultMap
    1.编写配置,我们可以不用配置的表,可以调用其他的表|
<select id="select01" resultType="java.util.Map" parameterType="java.util.Map">
        select s.*, sc.cid, sc.score
        from t_mysql_student s,
             t_mysql_score sc
        where s.sid = sc.sid
          and sc.sid = #{sid}
          and sc.cid = #{cid}
    </select>
    <select id="select02" resultType="java.util.Map" parameterType="java.util.Map">
        select s.*, sc.cid, sc.score
        from t_mysql_student s,
             t_mysql_score sc
        where s.sid = sc.sid
    </select>
  1. 2.编写接口和实现类
    Map select01(Map map);
    List<Map> select02(Map map);
    @Override
    public Map select01(Map map) {
        return bookMapper.select01(map);
    }
    @Override
    public List<Map> select02(Map map) {
        return bookMapper.select02(map);
    }
  1. 3.测试方法
    @Test
    public void select01() {
        Map map = new HashMap();
        map.put("sid", "01");
        map.put("cid", "01");
        System.out.println(bookbiz.select01(map));
    }
    @Test
    public void select02() {
        Map map = new HashMap();
        map.put("sid", "01");
        map.put("cid", "01");
        bookbiz.select02(map).forEach(System.out::println);
    }
  1. 4.测试结果


    5.返回单个列表,仅有一个查询结果,就用resultType
    6.返回单个列表,有多个查询结果,就用resultType。
    1.配置xml
<!--单个-->
    <select id="selectByString01" resultType="java.lang.String" parameterType="java.lang.Integer">
        select bname
        from t_mvc_book
        where bid = #{bid,jdbcType=INTEGER}
    </select>
    <!--多个-->
    <select id="selectByString02" resultType="java.lang.String" parameterType="java.lang.String">
        select bname
        from t_mvc_book
        where bname like concat('%', #{bname}, '%')
    </select>

2.编写接口和实现接口

    String selectByString01(Integer bid);
    List<String> selectByString02(String bname);
  @Override
    public String selectByString01(Integer bid) {
        return bookMapper.selectByString01(bid);
    }
    @Override
    public List<String> selectByString02(String bname) {
        return bookMapper.selectByString02(bname);
    }

3.测试方法

    @Test
    public void selectByString01() {
        System.out.println(bookbiz.selectByString01(60));
    }
    @Test
    public void selectByString02() {
        Map map = new HashMap();
        map.put("sid", "01");
        map.put("cid", "01");
        bookbiz.selectByString02("圣墟").forEach(System.out::println);
    }
  1. 4.测试结果


resultType:对应返回类型。

resultMap:对应返回映射关系,指的是实体类与数据表字段关系。

单表查询,返回单列,返回多表查询结果,是用resultType

查询的结果,需要有关属性的体现,那么就用resultMap

一, 分页

一、PageHelper介绍

       PageHelper 是 Mybatis 的一个插件,这里就不扯了,就是为了更加便捷的进行分页查询

官方网址:MyBatis 分页插件 PageHelper

PageHelper插件的优点:

物理分页:支持常见的 12 种数据库Oracle,MySql,MariaDB,SQLite,DB2,PostgreSQL,SqlServer 等...

支持多种分页方式:支持常见的RowBounds(PageRowBounds),PageHelper.startPage 方法调用,Mapper 接口参数调用。

QueryInterceptor 规范 :使用 QueryInterceptor 规范,开发插件更轻松。

二、PageHelper使用

1. 导入pom依赖

        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.2.0</version>
        </dependency>

2. Mybatis.cfg.xml 配置拦截器

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
    </plugins>

必须配置在上面,不然会报错。

正确实例:

模拟测试

mayBatis.xml配置

 <select id="like4" resultType="com.xzs.model.Book"  parameterType="java.util.List">
    select
    <include refid="Base_Column_List" />
    from t_mvc_book
    where bname like Concat ('%',#{bname},'%')
  </select>

接口

List<Book> like4(String bname, PageBean pageBean);

实现接口

 public List<Book> like4(String bname, PageBean pageBean) {
        if (pageBean!=null&&pageBean.isPagination()){
            PageHelper.startPage(pageBean.getPage(),pageBean.getRows());
        }
        List<Book> books = bookmapper.like4(bname);
        if (pageBean!=null&&pageBean.isPagination()){
            PageInfo<Book> bookPageInfo = new PageInfo<>(books);
            System.out.println("当前页" + bookPageInfo.getPageNum());
            System.out.println("展示记录数:" + bookPageInfo.getPageSize());
            System.out.println("符合查询条件的总记录数" + bookPageInfo.getTotal());
            pageBean.setTotal((int) bookPageInfo.getTotal());
        }
        return books ;
    }

测试类:

 public void like4() {
        PageBean pageBean = new PageBean();
        pageBean.setPage(2);
        pageBean.setRows(10);
        BookBiz.like4("%圣%",pageBean).forEach(System.out::println);
    }

结果:

三、特殊字符处理

 众所周知  在mybatis中我们经常用到特殊字符动态拼接语句,如经常使用到 大于(>,>=)、小于(<,<=)、不等于(<> ,!=)符号。Mybatis使用的 *.xml文件格式,需要在尖括号进行相关的转义或者使用 CDATA 区段。

1. 使用转义字符

殊字符 转义字符
< &lt;
> &gt;
& &amp;
" &quot;
&apos;
<= &lt;=
>= &gt;=

列如:

<select id="listPager" resultType="java.util.Map" parameterType="java.util.Map">
        select * from t_mvc_book where price < 9 and price > 20
    </select>

2. 使用CDATA 区段

是xml语法,在内部的所有内容都会被解析器忽略,不进行转义。所以在xml中这是一种通用方案。

 

特殊字符 <![CDATA[ ]]>
< <![CDATA[<]]>
> <![CDATA[>]]>
& <![CDATA[&]]>
" <![CDATA["]]>
<![CDATA[']]>
<= <![CDATA[<=]]>
>= <![CDATA[>=]]>
!= <![CDATA[!=]]>

例如:

<select id="listPager" resultType="java.util.Map" parameterType="java.util.Map">
        select * from t_mvc_book where  <![CDATA[ price > #{price} and #{price} != 0 ]]>
    </select>


相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
5月前
|
SQL Java 测试技术
3、Mybatis-Plus 自定义sql语句
这篇文章介绍了如何在Mybatis-Plus框架中使用自定义SQL语句进行数据库操作。内容包括文档结构、编写mapper文件、mapper.xml文件的解释说明、在mapper接口中定义方法、在mapper.xml文件中实现接口方法的SQL语句,以及如何在单元测试中测试自定义的SQL语句,并展示了测试结果。
3、Mybatis-Plus 自定义sql语句
|
16天前
|
SQL XML Java
mybatis实现动态sql
MyBatis的动态SQL功能为开发人员提供了强大的工具来应对复杂的查询需求。通过使用 `<if>`、`<choose>`、`<foreach>`等标签,可以根据不同的条件动态生成SQL语句,从而提高代码的灵活性和可维护性。本文详细介绍了动态SQL的基本用法和实际应用示例,希望对您在实际项目中使用MyBatis有所帮助。
46 11
|
2月前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
2月前
|
SQL Java 数据库连接
canal-starter 监听解析 storeValue 不一样,同样的sql 一个在mybatis执行 一个在数据库操作,导致解析不出正确对象
canal-starter 监听解析 storeValue 不一样,同样的sql 一个在mybatis执行 一个在数据库操作,导致解析不出正确对象
|
3月前
|
SQL Java 数据库连接
mybatis使用四:dao接口参数与mapper 接口中SQL的对应和对应方式的总结,MyBatis的parameterType传入参数类型
这篇文章是关于MyBatis中DAO接口参数与Mapper接口中SQL的对应关系,以及如何使用parameterType传入参数类型的详细总结。
61 10
|
4月前
|
SQL XML Java
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
文章介绍了MyBatis中动态SQL的用法,包括if、choose、where、set和trim标签,以及foreach标签的详细使用。通过实际代码示例,展示了如何根据条件动态构建查询、更新和批量插入操作的SQL语句。
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
|
5月前
|
SQL Java 数据库连接
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
文章讲述了在使用Mybatis时遇到的资源文件找不到的问题,并提供了通过修改Maven配置来解决资源文件编译到target目录下的方法。
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
|
4月前
|
SQL XML Java
mybatis :sqlmapconfig.xml配置 ++++Mapper XML 文件(sql/insert/delete/update/select)(增删改查)用法
当然,这些仅是MyBatis功能的初步介绍。MyBatis还提供了高级特性,如动态SQL、类型处理器、插件等,可以进一步提供对数据库交互的强大支持和灵活性。希望上述内容对您理解MyBatis的基本操作有所帮助。在实际使用中,您可能还需要根据具体的业务要求调整和优化SQL语句和配置。
75 1
|
5月前
|
SQL Java 数据库连接
Mybatis系列之 动态SQL
文章详细介绍了Mybatis中的动态SQL用法,包括`<if>`、`<choose>`、`<when>`、`<otherwise>`、`<trim>`和`<foreach>`等元素的应用,并通过实际代码示例展示了如何根据不同条件动态生成SQL语句。
|
5月前
|
SQL 关系型数据库 MySQL
解决:Mybatis-plus向数据库插入数据的时候 报You have an error in your SQL syntax
该博客文章讨论了在使用Mybatis-Plus向数据库插入数据时遇到的一个常见问题:SQL语法错误。作者发现错误是由于数据库字段中使用了MySQL的关键字,导致SQL语句执行失败。解决方法是将这些关键字替换为其他字段名称,以避免语法错误。文章通过截图展示了具体的操作步骤。