从@Param注解开始,深入了解 MyBatis 参数映射的原理

系列文章目录

MyBatis缓存原理
Mybatis plugin 的使用及原理
MyBatis+Springboot 启动到SQL执行全流程
数据库操作不再困难,MyBatis动态Sql标签解析
Mybatis的CachingExecutor与二级缓存
使用MybatisPlus还是MyBaits ,开发者应该如何选择?
巧用MybatisPlus的SQL注入器提升批量插入性能


在这里插入图片描述

前几天组内一名同事在项目中发现了一个报错:Parameter 'id' not found. Available parameters are [userId, param1],说白了也就是也就是在xml中使用'id'参数,但实际上Mapper层方法却用的@Param("userId"),这就是明显对MyBtais的参数映射不太了解了。相信这一部分内容同样有不少同学是疏忽的,那么本期我们来看一下@Param注解的使用与原理

📕作者简介:战斧,从事金融IT行业,有着多年一线开发、架构经验;爱好广泛,乐于分享,致力于创作更多高质量内容
📗本文收录于 MyBatis专栏 专栏,有需要者,可直接订阅专栏实时获取更新
📘高质量专栏 云原生、RabbitMQ、Spring全家桶 等仍在更新,欢迎指导
📙Zookeeper Redis kafka docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待


一、MyBatis 里 SQL执行的过程

1. 核心对象 BoundSql

我们之前其实多次讨论 MyBatis 里执行SQL的原理: MyBatis+Springboot 启动到SQL执行全流程,这里我们再简单复习。其简要步骤就是
获取会话-> 会话执行-> 执行器执行 -> statement -> jdbc

我们主要来看这一段:

在这里插入图片描述
在真正执行SQL的时候,我们拿到了MapperStatement后,还需要做三步:
① 把可执行的语句准备好,注意此时SQL没有参数;
② 把外界提供的参数进行整理与保存
③ 把整理好的入参,按一定规则代入上述SQL的指定位置

其实前两步,它们会构成这样一个对象:

// StaticSqlSource.java
  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    // sql 就是缺参数的可执行SQL
    // parameterMappings 是从xml解析出来的所需要的参数列表
    // parameterObject 实际执行到该方法的参数内容
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

  public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.parameterObject = parameterObject;
    // additionalParameters 部分只有动态SQL才会用到
    this.additionalParameters = new HashMap<>();
    this.metaParameters = configuration.newMetaObject(additionalParameters);
  }

这个对象中含有MyBatis配置、对应SQL,SQL中需要的参数,现在实际提供的参数,那么我们现在只需要关注 SQL中需要的参数现在实际提供的参数 即可

2. 解析出 parameterMappings

parameterMappings 实际保存的就是 从xml解析出的关于SQL所需要的参数列表,这个所需参数的对象由下面几个属性构成:

List<ParameterMapping> parameterMappings;

public class ParameterMapping {
  private String property;
  private ParameterMode mode;
  private Class<?> javaType = Object.class;
  private JdbcType jdbcType;
  private Integer numericScale;
  private TypeHandler<?> typeHandler;
  private String resultMapId;
  private String jdbcTypeName;
  private String expression;
 }

在一般场景下,最核心的就是 property ParameterModejdbcType 三个字段了,比如说我们在xml里如果写了这样一段SQL

<select id="findUserById" resultType="com.zhanfu.springboot.demo.entity.User" parameterType="int">
     SELECT * FROM user where id =#{idy}
</select>

此时我们只需要一个参数,因此解析出来只会有一个 ParameterMapping
① 它的 property 就是我们设定的变量名 ‘idy’,
② ParameterMode 是参数模式,我们这里是入参,也就是 ‘IN’ ;除此以外还有 ‘OUT’ 和 ‘INOUT’
③ jdbcType 因为我们指定了参数的 parameterType, javaType 就是 Integer

3. 整理方法入参 parameterObject

刚刚我们说了,parameterObject 就是我们实际提供的参数,也就是我们实际调用Mapper方法时传入的参数组,直白的说,MyBatis会把我们传入的参数数组转化为一个参数Map对象,key是参数名,value是参数值。

  // MapperMethod.java 
  // Mapper层代理类执行方法时调用的就是此类
  Object param = method.convertArgsToSqlCommandParam(args);
  result = sqlSession.selectOne(command.getName(), param);

 // 将传入方法的arg数组,转为一个参数对象,实际上就是一个HashMap
  public Object convertArgsToSqlCommandParam(Object[] args) {
     return paramNameResolver.getNamedParams(args);
  }

对于这个Map,参数值自然是我们传的参数值,而参数名是怎么获取的,又有什么规则呢? 我们来看下所谓的 name 是怎么设计的。

 // ParamNameResolver.java
   public ParamNameResolver(Configuration config, Method method) {
   this.useActualParamName = config.isUseActualParamName();
   final Class<?>[] paramTypes = method.getParameterTypes();
   final Annotation[][] paramAnnotations = method.getParameterAnnotations();
   final SortedMap<Integer, String> map = new TreeMap<>();
   int paramCount = paramAnnotations.length;
   // get names from @Param annotations
   for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
     if (isSpecialParameter(paramTypes[paramIndex])) {
       // skip special parameters
       continue;
     }
     String name = null;
     for (Annotation annotation : paramAnnotations[paramIndex]) {
       if (annotation instanceof Param) {
         hasParamAnnotation = true;
         name = ((Param) annotation).value();
         break;
       }
     }
     if (name == null) {
       // @Param was not specified.
       if (useActualParamName) {
         name = getActualParamName(method, paramIndex);
       }
       if (name == null) {
         // use the parameter index as the name ("0", "1", ...)
         // gcode issue #71
         name = String.valueOf(map.size());
       }
     }
     map.put(paramIndex, name);
   }
   names = Collections.unmodifiableSortedMap(map);
 }
 

从上述代码中,我们看到了所谓参数name的制取流程,对于每一个参数,其实都分为三个优先级:
①优先取@Param里的value值作为参数名;
②其次如果myBtais的配置里配置了 useActualParamName = true(默认是开启的), 则取方法中写的形参名;
③如果上述都没有值,最后取该参数在所有入参中的序号,从0开始,计值0,1,2…作为参数的名称

 public Object getNamedParams(Object[] args) {
   final int paramCount = names.size();
   if (args == null || paramCount == 0) {
     return null;
   } else if (!hasParamAnnotation && paramCount == 1) {
    // 仅有一个参数,且该参数没有使用 @Param注解
     Object value = args[names.firstKey()];
     return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
   } else {
   // 对于一般场景,则会为参数提供两个名称,一个是我们上面获取的名称,一个是‘param+序号’的名称
     final Map<String, Object> param = new ParamMap<>();
     int i = 0;
     for (Map.Entry<Integer, String> entry : names.entrySet()) {
       param.put(entry.getValue(), args[entry.getKey()]);
       // add generic param names (param1, param2, ...)
       final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
       // ensure not to overwrite parameter named with @Param
       if (!names.containsValue(genericParamName)) {
         param.put(genericParamName, args[entry.getKey()]);
       }
       i++;
     }
     return param;
   }
 }

// 如果参数是集合,则构建map,形成<参数名, 参数值>的map, 并且额外为参数补充如 ‘list’、‘collection’ 这样的的参数名;
// 如果不是集合,直接返回对象
 public static Object wrapToMapIfCollection(Object object, String actualParamName) {
   if (object instanceof Collection) {
     ParamMap<Object> map = new ParamMap<>();
     map.put("collection", object);
     if (object instanceof List) {
       map.put("list", object);
     }
     Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
     return map;
   } else if (object != null && object.getClass().isArray()) {
     ParamMap<Object> map = new ParamMap<>();
     map.put("array", object);
     Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
     return map;
   }
   return object;
 }

通过对源码的分析,我们不难看出对参数的处理其实分成了两种逻辑:

(1) 整理结果1: 仅有一个参数,且该参数没有使用 @Param注解

  • 此时,如果该参数是集合类型的,则还是构建一个<参数名,参数值>这样的map,把参数及其名称放入其中。而且还给他补充几个参数名, 如 ‘list’、‘collection’,当然参数值都是一样的。这样,该参数其实就有多个别名了。
  • 如果该参数不是集合,就直接使用参数值本体,而不再使用map的形式

(2) 整理结果2:(即有多个参数,或者有参数使用了@Param注解)

  • 构建一个<参数名,参数值>这样的map,使用我们上面获取到的参数名,同时为每一个参数添加一个别名,即param1, param2 等。

总结的说,就是如果你这个方法仅有一个参数,且参数不是集合,而我们也没有使用@Param,那它就会直接使用这个参数值。除此以外我们的参数值全部是以map的形式存储的,而参数名正是我们的@Param的值、方法形参名,或者单纯的次序号‘0’、‘1’、‘2’;除了这些参数名外,Mybatis还会为我们增加这些参数的别名,如param1, param2等。

4. 参数的代入

我们现在知道了需要的参数,也整理好了方法的入参,那接下来自然就是把参数代入SQL的指定位置了,它的核心方法如下:

  // DefaultParameterHandler.java
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

不难看出,他就是遍历SQL里每一个需要参数的位置,然后再去我们整理的入参中去找合适的,把入参的值填入该位置:这一步其实含有不少细节,我们把它做个梳理,找参数值可以按照方法里的逻辑归纳为3个优先级:

  • 代入场景1优先去 AdditionalParameter 找有没有符合的参数,AdditionalParameter 是动态SQL的产物,它
  • 代入场景2直接使用 parameterObject 作为参数值 ,注意直接使用 parameterObject 仅会发生在方法仅有一个非集合参数的情况下,上面我们已经讲过,此时parameterObject 就是入参传的对象;① 若入参为 null 则 parameterObject 为 null ; ② 若 parameterObject 不为 null 且是简单类型,能直接被Mybatis 处理则直接将其处理,得到参数值
  • 代入场景3从 parameterObject 中找符合的参数,注意此种情况下,parameterObject 其实是一个复杂对象 或者是 map,需要根据所需参数名,在 对象 中或者 map 找到对应的参数值。

5. 动态SQL的特殊处理

我们上面说的其实是静态SQL的处理逻辑,而我们也曾介绍过动态SQL,可见《数据库操作不再困难,MyBatis动态Sql标签解析》。

静态SQL比较简单,给参数预留的位置与个数是一目了然,不会变的,只要从xml里写的SQL里进行解析就好了。
而动态SQL则不然,动态SQL直到执行前一刻,获取到了入参,结合了入参的情况才能确定SQL需要几个参数,以及参数的位置。

因此在获取了BoundSql 后,还需要对 BoundSql 做一步操作

  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 结合xml的动态SQL标签,以及当前入参的情况,构造出动态SQL,
    // 并整理SQL内需要的参数个数及位置,将信息保存在 上下文的binding 中
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    // 获取了 boundSql 
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    // 处理 boundSql ,将动态上下文的 binding 部分赋值给 boundSql 的 AdditionalParameters 中
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

比方说对于这样一个接口 及其 xml

boolean batchAddUser(List<User> userList)
<insert id="batchAddUser" parameterType="com.zhanfu.springboot.demo.entity.User">
      insert into user(id,username,password) values
      <foreach collection="list" item="user" separator=",">
            (#{user.id}, #{user.username}, #{user.password})
      </foreach>
</insert>

就利用了 动态标签,此时如果我们插入两条数据(list 长度等于2),上下文的bing 部分就会变成如下。我们主要关系上面的 __frch_user_0 以及 __frch_user_1 。这两条就是结合原SQL与入参情况产生的新SQL中,所需要储存着的参数名 - 参数值映射关系。

在这里插入图片描述

二、@Param注解在各场景下的效果

1. 单个参数 - 简单类型

(1)加@Param注解之前

此时 Mapper 层方法为:

User findUserById(int id)

此时xml语句为:

<select id="findUserById" resultType="com.zhanfu.springboot.demo.entity.User" parameterType="int">
     SELECT * FROM user where id =#{id}
</select>

此时我们的入参是单个的简单对象,所以会使用整理结果1,导致 parameterObject 就直接是我们的 id 对象(基础类型会变为包装类型Integer), 而 parameterMappings 需要的参数为一个,即‘id’,所以最终走入代入场景2,直接把我们传入的id,放进SQL中的 #{id} 位置,值得一提的是,此时其实根本没有用到parameterMapping里的属性名,也就是说,即使xml中你的SQL写的是

<select id="findUserById" resultType="com.zhanfu.springboot.demo.entity.User" parameterType="int">
     SELECT * FROM user where id =#{niuAAAAAA}
</select>

也没事,仍然能把外界传的id,放进 #{niuAAAAAA} 这个位置

(2)加@Param注解之后

此时 Mapper 层方法为:

User findUserById(@Param("id")int id);

此时xml语句为:

<select id="findUserById" resultType="com.zhanfu.springboot.demo.entity.User" parameterType="int">
     SELECT * FROM user where id =#{id}
</select>

此时因为加入了@Param,所以会使用整理结果2,导致 parameterObject 成为了一个map,内含两个键值对,两个键值对的 value 值都是我们传入的 id 对象,而 key 值一个是我们设定的参数名 ‘idr’,一个是Mybatis 为我们加的别名 ‘param1’

在这里插入图片描述

所以此时会进入代入场景3,然后从 map 中以 ‘id’ 去取值。就取到了我们的传入的 id 对象。因此在这种情况下,我们的 #{id} 就不能写成 #{niuAAAAAA} 了,必须和 @Param 注解里写的参数名保持一致

2. 单个参数 - 复杂对象

(1)加@Param注解之前

此时 Mapper 层方法为:

boolean addUser(User user)

此时xml语句为:

 <insert id="addUser" parameterType="com.zhanfu.springboot.demo.entity.User" >
     insert  into user(id,username,password) values (#{id},#{username},#{password})
</insert>

此时我们的入参是单个的对象,且没有使用@Param, 所以会使用整理结果1,导致 parameterObject 就直接是我们的 user 对象, 而 parameterMappings 需要的参数为三个,即‘id’ 、‘username’ 、 ‘password’,

所以最终则会循环走入代入场景3,然后从我们的入参对象中分别取出 ‘id’ 、‘username’ 、 ‘password’,更具体的,其获取值的方式自然是使用对象的get属性值的方法。注意,如果你使用的是@lombok来自动补足get set方法的话,需要注意里面的一些坑,详情见《【问题处理】—— lombok 的 @Data 大小写区分不敏感》

(2)加@Param注解之后

此时 Mapper 层方法为:

boolean addUser(@Param("user") User user);

此时xml语句为:

 <insert id="addUser" parameterType="com.zhanfu.springboot.demo.entity.User" >
     insert  into user(id,username,password) values (#{id},#{username},#{password})
</insert>

此时我们的入参是单个的复杂对象,但因为加入了@Param,所以会使用整理结果2,导致 parameterObject 成为了一个map,内含两个键值对,两个键值对的 value 值都是我们传入的 user 对象,而 key 值一个是我们设定的参数名 ‘user’,一个是Mybatis 为我们加的别名 ‘param1’

在这里插入图片描述

此时同样的 parameterMappings 需要的参数为三个,所以还是会循环走入代入场景3,然后从 map 中以 ‘id’ 去取值。自然,我们都知道,map 里此时并没有一个key 值叫 ‘id’,所以这样的使用方式最终会报错 Parameter 'id' not found. Available parameters are [user, param1].如果这种场景下一定要使用@Param注解,则需要将xml语句写成

<insert id="addUser" parameterType="com.zhanfu.springboot.demo.entity.User" >
     insert  into user(id,username,password) values (#{user.id},#{user.username},#{user.password})
</insert>

MyBatis能自动以 . 为界进行分割,前面的部分作为对象,后面作为属性不断递归,此处就能获取 user 对象的 id 属性了。

3. 多个参数

(1)加@Param注解之前

此时 Mapper 层方法为:

User findUser(int id, String name)

此时xml语句为:

<select id="findUser" resultType="com.zhanfu.springboot.demo.entity.User">
     SELECT * FROM user where id =#{id} and username =#{name}
</select>

此时我们的入参是多个对象, 所以会使用整理结果2,导致 parameterObject 成为了一个map,.内含四个键值对,如下:其中“name” 、“id” 键值对是来源于形参名, ‘param1’、 ‘param2’ 是MyBtais为我们添加的别名

在这里插入图片描述
而 parameterMappings 需要的参数为三个,即‘id’ 、‘name’

所以最终则会循环走入代入场景3,然后从我们的map中分别取出 ‘id’ 、‘name’ ,放入对应的位置,因此此时必须要保证 #{id} 与方法的 User findUser(int id, String name) 形参名 id 是同样的,不能随便写。

(2)加@Param注解之后

此时 Mapper 层方法为:

User findUser(@Param("id")int id, @Param("name")String name)

此时xml语句为:

<select id="findUser" resultType="com.zhanfu.springboot.demo.entity.User">
     SELECT * FROM user where id =#{id} and username =#{name}
</select>

此时我们的入参是多个对象, 所以会使用整理结果2,最终 parameterObject 成为了一个map,.内含四个键值对,如下:其中“name” 、“id” 键值对是来源于我们@Param里填的字符串, ‘param1’、 ‘param2’ 是MyBtais为我们添加的别名

在这里插入图片描述

最终它的表现是和没加@Param一致的,同样会循环走入代入场景3,然后从 map 中以 ‘id’ 、‘name’ 去取值。

4. 集合参数与动态SQL

我们向集合中添加两个User元素,然后调用Mapper

(1)加@Param注解之前

此时 Mapper 层方法为:

boolean batchAddUser(List<User> userList);

此时xml语句为:

<insert id="batchAddUser" parameterType="com.zhanfu.springboot.demo.entity.User">
     insert into user(id,username,password) values
     <foreach collection="list" item="user" separator=",">
            (#{user.id}, #{user.username}, #{user.password})
     </foreach>
</insert>

此时我们的入参是个单独对象,没有@Param,所以会使用整理结果1,但因为对象是集合类型,最终 parameterObject 还是成为了一个map,需要注意的是,此时除了 还有了一个parameterObject ,还有 additionalParameters 如下:

在这里插入图片描述

而从动态SQL中解析出的 parameterMappings 需要的参数为2 * 3 = 6个,如下:
在这里插入图片描述

因为有了 additionalParameters ,所以最终则会循环走入代入场景1即从 additionalParameters 获取 __frch_user_0.username。这样也是可以取到的,和我们最开始取user.id是一样的。

需要注意的是,我们在 xml 中的动态SQL里使用了 collection=“list” ,这里也不是乱填的,list 正是parameterObject 的三个key 之一,因此此处还可以替换成 collection=“collection” 、collection = ”userList“

(2)加@Param注解之后

此时 Mapper 层方法为:

boolean batchAddUser(@Param("uList")List<User> userList);

此时xml语句为:

<insert id="batchAddUser" parameterType="com.zhanfu.springboot.demo.entity.User">
     insert into user(id,username,password) values
     <foreach collection="uList" item="user" separator=",">
            (#{user.id}, #{user.username}, #{user.password})
     </foreach>
</insert>

此时我们的入参是个单独对象,有@Param。所以会使用整理结果2,导致 parameterObject 成为了一个map,也还有 additionalParameters, 如下:

在这里插入图片描述

而从动态SQL中解析出的 parameterMappings 需要的参数还是一样的,为2 * 3 = 6个,如下:
在这里插入图片描述

因为有了 additionalParameters ,所以最终则会循环走入代入场景1即从 additionalParameters 获取 __frch_user_0.username

此时不同的因为parameterObject 里现在的key 只有 ‘ulist’ 和 ‘param1’ ,所以我们的动态SQL里使用的 collection=“ulist” ,就只能填这个或 collection=“param1” 了。

三、总结

本次的涉及的知识点比较琐碎,如果刨去动态SQL的那部分来说,我们其实可以归纳一下 @Param 注解的作用:就是对方法的参数名做一个重命名:

①使用了 @Param("AAA"),就需要在xml中使用到参数的部分,也与此保持一致,如#{AAA}
②特别的,对于方法仅有一个入参对象(如User对象)的情况,如果你使用了 @Param("AAA"),那么你在xml中,如果要使用对象的属性,如User对象的id属性,那就只能使用#{AAA.id}。如果你此时不使用 @Param 注解,在xml中反而能直接使用 #{id} 来表示入参的id属性

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

解决所有终端中文输出乱码的问题

一、系统自带的cmd.exe 以及 Git的bash.exe、sh.exe、git-bash.exe和git-cmd.exe&#xff0c;和PowerShell默认使用“当前系统区域设置”设定好的936 (ANSI/OEM - 简体中文 GBK)语言编码。 1、[当前代码页] 的936 (ANSI/OEM - 简体中文 GBK) 是导致中文乱码的原因 在控制面板→…

【基于深度学习方法的激光雷达点云配准系列之GeoTransformer】——模型部分浅析(1)

【GeoTransformer系列】——模型部分 1. create_model2. model的本质3. 模型的主要结构3.1 backbone3.2 transformer本篇继续对GeoTransformer/experiments/geotransformer.kitti.stage5.gse.k3.max.oacl.stage2.sinkhorn/下面的trainval.py进行详细的解读,主要是模型部分, 可以…

单位转换:将kb转换为 MB ,GB等形式

写法一&#xff1a; function formatSizeUnits(kb) {let units [KB, MB, GB, TB, PB,EB,ZB,YB];let unitIndex 0;while (kb > 1024 && unitIndex < units.length - 1) {kb / 1024;unitIndex;}return ${kb.toFixed(2)} ${units[unitIndex]}; } console.log(for…

linux 下配置docker mirrors

一、配置mirrors vi /etc/docker/daemon.json {"registry-mirrors": ["https://docker.blfrp.cn"],"log-opts": {"max-size": "10m","max-file": "3"} }#完成配置后重启docker systemctl restart dock…

SAP ALV 负号提前

FUNCTION CONVERSION_EXIT_ZSIGN_OUTPUT. *"---------------------------------------------------------------------- *"*"本地接口&#xff1a; *" IMPORTING *" REFERENCE(INPUT) *" EXPORTING *" REFERENCE(OUTPUT) *"…

labview排错

源代码正常跑&#xff0c;应用程序报这个错&#xff0c;是因为源代码的可以找到项目路径内所有dll的路径&#xff0c;而应用程序只能找到data文件夹的dll文件 解决查看源代码中.net的程序集的路径&#xff0c;复制对应的dll到data文件夹下

24V 350W开关电源电路原理图+PCB工程文件 UC3843AD lm193芯片

资料下载地址&#xff1a;24V 350W开关电源电路原理图PCB工程文件 UC3843AD lm193芯片 1、原理图 2、PCB

昇思25天学习打卡营第5天|数据变换Transforms

数据变换 Transforms 在完成数据加载后&#xff0c;还应该对数据进行预处理。之前在数据集篇介绍过map函数&#xff0c;这里的transform就是和map一起使用的。transform有针对图像、文本、音频等不同类型的&#xff0c;并且也支持lambda函数。 环境配置 import numpy as np …

Echarts地图实现:各省市计划录取人数

Echarts地图实现&#xff1a;各省市计划录取人数 实现功能 本文将介绍如何使用 ECharts 制作一个展示中国人民大学2017年各省市计划录取人数的地图。我们将实现以下图表形式&#xff1a; 地图&#xff1a;基础的地图展示&#xff0c;反映不同省市的录取人数。散点图&#xf…

华为od 2024 | 什么是华为od,od 薪资待遇,od机试题清单

目录 专栏导读华为OD机试算法题太多了&#xff0c;知识点繁杂&#xff0c;如何刷题更有效率呢&#xff1f; 一、逻辑分析二、数据结构1、线性表① 数组② 双指针 2、map与list3、队列4、链表5、栈6、滑动窗口7、二叉树8、并查集9、矩阵 三、算法1、基础算法① 贪心思维② 二分查…

【系统架构设计师】四、嵌入式基础知识(软件|软件设计|硬件|式总线逻辑)

目录 一、嵌入式软件 1.1 嵌入式软件分类 1.2 板级支持包(BSP) 1.3 BootLoader 1.4 设备驱动程序 二、嵌入式软件设计 2.1 编码 2.2 交叉编译 2.3 交叉调试 三、嵌入式系统硬件的分类 3.1 根据用途分类 3.2 存储器分类 四、内&#xff08;外&#xff09;总线逻辑 …

江科大笔记—FLASH闪存

FLASH闪存 程序现象&#xff1a; 1、读写内部FLASH 这个代码的目的&#xff0c;就是利用内部flash程序存储器的剩余空间&#xff0c;来存储一些掉电不丢失的参数。所以这里的程序是按下K1变换一下测试数据&#xff0c;然后存储到内部FLASH&#xff0c;按下K2把所有参数清0&…

力扣每日一题 6/23 字符串/模拟

博客主页&#xff1a;誓则盟约系列专栏&#xff1a;IT竞赛 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ 520.检测大写字母【简单】 题目&#xff1a; 我们定义&#xff0c;在以下…

【web开发】chrome拦截localhost跨域请求原因

在设置中&#xff0c;默认屏蔽了&#xff0c;请求不会到localhost服务器 chrome://flags/#block-insecure-private-network-requests 设置disable即可

solidworks钣金工厂共享云桌面方案

随着信息技术的飞速发展和企业数字化转型的深入&#xff0c;传统的钣金工厂面临着诸多挑战&#xff0c;其中之一就是如何在保证数据安全的前提下&#xff0c;提高设计、生产和管理的效率。 SolidWorks是一款专业的三维3D设计软件&#xff0c;功能强悍&#xff0c;支持分布式数…

Word页眉横线怎么删除?5个方法,记得收藏!

在数字化办公日益普及的今天&#xff0c;Word文档成为了我们日常工作中不可或缺的一部分。然而&#xff0c;在编辑和排版Word文档时&#xff0c;我们有时会面临一些看似微小却令人头疼的问题&#xff0c;比如页眉中的横线。这条不起眼的横线&#xff0c;就像是在整洁的页面上划…

基于SpringBoot的藏区特产销售平台

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a; Java 数据库&#xff1a; MySQL 技术&#xff1a; SpringBoot框架 工具&#xff1a; MyEclipse 系统展示 首页 个人中心 特产信息管理 订单管…

CentOS安装Docker教程(包含踩坑的经验)

目录 一.基础安装 ▐ 安装Docker 二.启动Docker服务 三.配置Docker镜像加速 一.基础安装 在安装Docker之前可能需要先做以下准备 首先如果系统中已经存在旧的Docker&#xff0c;则先卸载&#xff1a; yum remove docker \docker-client \docker-client-latest \docker-…

现货黄金如何操作:黄金技术性止损的运用

止损是现货黄金如何操作中不得不提及的方法。在现货黄金投资过程中&#xff0c;风险是存在的&#xff0c;重要的是如何将风险把控好。这里的一个重要概念就是&#xff0c;要对每一笔交易设定好止损&#xff0c;可以讲&#xff0c;这就是现货黄金如何操作的方法中最重要的一种。…

重磅!免费一键批量混剪工具它来了,一天上万短视频不是梦

很多做短视频营销的朋友需要批量生成大量的短视频&#xff0c;但是市面上的工具一是不好用&#xff0c;二是要收费。 今天给大家介绍一款免费的&#xff0c;可以自动化批量生成短视频的工具MoneyPrinterPlus。 同时支持windows和linux平台。 有了它&#xff0c;一天生成上万短…