MybatisPlus 构造器wrapper的使用与原理

系列文章目录

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


在这里插入图片描述

上一次我们给大家讲解了何为MybatisPlus,我们使用它的目的以及它的主要特性。不过,对于一线开发而言,如何在代码中使用上它的特性才是重中之重,本期我们就来好好讲一下 MybatisPlus 中的条件构造器

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


一、构造器的分类

我们还是使用一张老图来说明

在这里插入图片描述
构造器都有一个核心父类= AbstractWrapper =,其他的构造器都是它的子类,现在两种分类方式,一种分类是用途;即查询更新 构造器,另一种则是按使用方式,即一般lambda 构造器。两种分类交错,最后我们就看到了四个构造器实现类。

1. AbstractWrapper 的作用

作为所有条件构造器的父类,AbstractWrapper 肩负着绝大部分的功能,来帮助我们实现各类复杂的SQL. 它实现了下面几个接口:

  • Compare
    -定义了一组方法用于比较操作,包括等于(eq)、不等于(ne)、大于(gt)
  • Nested
    -定义了一组方法用于构建嵌套条件,即在查询条件中可以使用括号包裹的子条件
  • Join
    -用于实现表的关联查询,通过指定关联条件、连接方式,可以将多个表的数据进行关联查询。
  • Func
    -定义了一组方法用于构建SQL函数表达式,包括COUNT、SUM、AVG、MAX、MIN等函数

2. 普通构造器与lambda构造器

我们首先看带不带lambda有什么区别,其实两者几乎一致,我们以查询为例,也就是对比一下LambdaQueryWrapperQueryWrapper ,不难发现,两者的差异重点是因为两人实现Query接口的定义不一样

在这里插入图片描述
在这里插入图片描述
普通的构造器实现query,定义了只能使用String,而Lambda构造器的入参则是允许是一个function函数。这样的区别,将导致两种不同的写法,见如下:

// 普通构造器
        QueryWrapper<CsdnUserInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("is_delete", 0);
        wrapper.orderByDesc("user_weight");
        return this.list(wrapper);
// lambda构造器
        LambdaQueryWrapper<CsdnUserInfo> lambdaWrapper = new LambdaQueryWrapper<>();
        lambdaWrapper.eq(CsdnUserInfo::getIsDelete, 0);
        lambdaWrapper.orderByDesc(CsdnUserInfo::getUserWeight);
        return this.list(lambdaWrapper);

普通构造器在使用时,字段名只能直接写字符串,而lambda构造器只能写方法。因为直接写字符串出错了不容易发现,因此推荐大家还是尽量使用lambda构造器,以方便在编码阶段就减少错漏可能性

3. query构造器与update构造器

首先两者因为都继承了 AbstractWrapper 的,所以大部分的SQL功能两者是都具备的,比如SQL中where子句、join、排序那些东西。两者最大的区别是它们分别实现了两个不同的接口,一个继承Query,一个继承Update

在这里插入图片描述

在这里插入图片描述
不难看出,两者核心的不同就是语法的区别,查询哪些字段或更新哪些字段


二、使用方式

关于构造器的基本方法,内容非常多,我们不再做搬运工,大家可以直接去下面的官网看方法的文档:条件构造器文档

在这里插入图片描述

1. 基础使用

我们以举例说明,比方说现在我们想要实现一个查询功能,根据以下条件来获取学生的信息:

性别为男性; 年龄在18到20岁之间; 成绩大于80分; 班级为A班,然后按成绩由高到底排

使用QueryWrapper来实现这个查询的代码如下

QueryWrapper<Student> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("gender", "男性")
        .between("age", 18, 20)
        .gt("score", 80)
        .eq("class", "A班")
        .orderByDesc("score");

List<Student> students = studentMapper.selectList(queryWrapper);

又比如说 我们想将姓名为"张三"的学生年龄更新为20。则可以使用UpdateWrapper实现

String name = "张三";
Integer newAge = 20;

UpdateWrapper<Student> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("name", name)
             .set("age", newAge);

int result = studentMapper.update(null, updateWrapper);

2. 易错点-逻辑范围

由于条件构造器的语法非常符合自然语言,所以有的时候反而让人疏忽,我们仍举一个例子,比如我们想获取这样的用户

用户状态为有效的,名字为 张三 或 李四 或 王五 的用户

你可能想当然写成如下的样子

// 错误写法
Set<String> set = new HashSet<>();
set.add("zhangsan");
set.add("lisi");
set.add("wangwu");

LambdaQueryWrapper<CsdnUserInfo> wrapper = new LambdaQueryWrapper<>();

wrapper.eq(CsdnUserInfo::getIsDelete, 0);
for (String name : set) {
    wrapper.or(item -> item.eq(CsdnUserInfo::getUserName, name));
}
return this.list(wrapper);

但事实上,最终预编译产生的SQL是这样的:

SELECT id,user_name,nick_name,like_status,collect_status,comment_status,user_weight,user_home_url,curr_blog_url,article_type,create_time,update_time,is_delete FROM csdn_user_info WHERE (is_delete = ? OR (user_name = ?) OR (user_name = ?) OR (user_name = ?))

不难发现这样我们用户状态筛选 和 用户名筛选 用 or 关联起来了,这样其实就会查出所有有效的用户,不符合我们的预期。产生这种问题的原因,其实是少用了个括号。我们应该把用户名的 or 限制在一个括号内,不能让它扩散出去。

此时我们可以使用 nested 来修复这个问题,把我们的for循环扔进 nested

Set<String> set = new HashSet<>();
set.add("zhangsan");
set.add("lisi");
set.add("wangwu");
wrapper.eq(CsdnUserInfo::getIsDelete, 0);
wrapper.nested(wp -> {
            for (String name : set) {
                wp.or(item -> item.eq(CsdnUserInfo::getUserName, name));
            }
});
return this.list(wrapper);

这样最后的编译的SQL是这样的

SELECT id,user_name,nick_name,like_status,collect_status,comment_status,user_weight,user_home_url,curr_blog_url,article_type,create_time,update_time,is_delete FROM csdn_user_info WHERE (is_delete = ? AND ((user_name = ?) OR (user_name = ?) OR (user_name = ?)))

3. 易错点-null处理

使用 MybatisPlus 还有一个坑点容易被忽略,那就是 null 值的处理,比如我们在插入或更新一条数据时,如果我们对象内某一个字段为null,这个null值可能不会插入或更新进表中。如果 null 在你的表中有业务意义,此刻就格外需要注意了。

比如我们想把张三的昵称置为空,写了这么一段代码

CsdnUserInfo user = new CsdnUserInfo();
user.setId(99999);
user.setUserName("张三");
user.setNickName(null);
csdnUserInfoService.updateById(user);

但我们的目的是不会生效的,因为此时 NickName 根本不会更新,它的SQL是这样的

UPDATE csdn_user_info SET user_name=? WHERE id=?

此时我们可以在实体类上加一个注解 @TableField(updateStrategy= FieldStrategy.IGNORED) 这是因为对字段的变更默认会有空值校验,只有显示的指定为 忽略校验 才能把空值更新进表中

在这里插入图片描述

经过这样的改动,我们再来看看编译的SQL

UPDATE csdn_user_info SET user_name=?, nick_name=? WHERE id=?

nick_name 的 null 就可以成功更新进表中了。


三、生效原理

如果你看过我们之前的《MyBatis+Springboot 启动到SQL执行全流程》,就不难理解,我们其实可以把整个 MybatisPlus 分为项目启动阶段做的准备阶段,和真正要执行某段SQL的执行阶段。准备阶段就是把我们写在xml的SQL进行解析,构造出一个个映射声明(Mappedstatement),里面包含了我们的SQL主体。执行阶段则是通过方法全称找到自己的映射声明(Mappedstatement),对其进行拼接形成真正的可执行SQL。

那在MybatisPlus下,我们明明没写SQL,SQL又是从哪来的呢?

正如上面说的,比如一个简单的插入,我们没有写SQL,甚至连xml文件都没有,而是直接使用的BaseMapper 中的 insert 方法。

在这里插入图片描述

所以在启动的时候,如下面的 service其实就在做准备工作了,如果说我们以前的xml文件写了现成的SQL语句,从而解析XML文件内容,产生mappedStatement
而像下面这种service其实也是这样,不过他多了一步,它得先把service翻译成xml格式,然后再来转成mappedStatement。

@Service
public class AlgorithmicProblemServiceImpl extends ServiceImpl<AlgorithmicProblemMapper, AlgorithmicProblem> implements AlgorithmicProblemService {

}

就拿上面的 insert 举例,MybatisPlus 中有一个 insert 类,即 com.baomidou.mybatisplus.core.injector.methods.Insert 其中就有关键方法

在这里插入图片描述

1. 获取语法模板SQL

所谓获取模板SQL,其实就是在MybatisPlus中内置了很多方法的基本语法,这些内容只要在填上对应的表名、字段名就能构成一个基础 SQL 的轮廓了

在这里插入图片描述
还是以我们的插入语句为例,比如我们想插入一条数据,它的基础语法模板就是

"<script>\nINSERT INTO %s %s VALUES %s\n</script>"

其中三个 ‘%s’ 就是待定的表名和字段名信息,将在后面代入表信息

2. 代入表及字段信息

我们上面看到了这样一句话,它的作用就是根据语法模板,和表信息,构造出一个基础的SQL

String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);

上面的第一个入参 sqlMethod.getSql() 就是我们上面说的语法模板,第二个入参是表名,三和四字段则是根据字段名拼出来的SQL,表名的话,我们在实体类里已经写了,即 @TableName 注解后面的内容

@Data
@ApiModel("算法题实体类")
@TableName("algorithmic_problem")
public class AlgorithmicProblem extends Model<AlgorithmicProblem> {
    @ApiModelProperty("主键id")
    private Integer id;
    @ApiModelProperty("问题名称")
    private String questionName;
    @ApiModelProperty("问题类型")
    private String questionType;
    @ApiModelProperty("1~10的分值")
    private Integer degreeOfImportance;
    @ApiModelProperty("1:简单;2:中等;3:困难")
    private Integer degreeOfDifficulty;
    @ApiModelProperty("困难指数")
    private Integer difficultyOfScore;
    @ApiModelProperty("力扣的问题号")
    private Integer leetcodeNumber;
    @ApiModelProperty("力扣的问题链接")
    private String leetcodeLink;
    @ApiModelProperty("标签")
    private String tag;
    @ApiModelProperty("创建时间")
    private Date createTime;
    @ApiModelProperty("逻辑删除,0未删除,1已删除")
    private Integer isDelete;
}

而后面的 columnScript, valuesScript 则是根据我们的字段值,生成的一些脚本,其中包含了我们以前在mybatis 的 xml 文件中会写的动态标签,这些标签的作用可以参考此片文章《数据库操作不再困难,MyBatis动态Sql标签解析》
在这里插入图片描述
在这里插入图片描述

通过上面的操作,不难发现,我们以前在XML文件里需要写的 SQL 在此刻就被拼接出来了,它们形式是一样的。需要注意的是,这里的字段全都是非空才会插入的。

<script>
INSERT INTO algorithmic_problem <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if><if test="questionName != null">question_name,</if>
<if test="questionType != null">question_type,</if>
<if test="degreeOfImportance != null">degree_of_importance,</if>
<if test="degreeOfDifficulty != null">degree_of_difficulty,</if>
<if test="difficultyOfScore != null">difficulty_of_score,</if>
<if test="leetcodeNumber != null">leetcode_number,</if>
<if test="leetcodeLink != null">leetcode_link,</if>
<if test="tag != null">tag,</if>
<if test="createTime != null">create_time,</if>
<if test="isDelete != null">is_delete,</if>
</trim> VALUES <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id},
</if><if test="questionName != null">#{questionName},</if>
<if test="questionType != null">#{questionType},</if>
<if test="degreeOfImportance != null">#{degreeOfImportance},</if>
<if test="degreeOfDifficulty != null">#{degreeOfDifficulty},</if>
<if test="difficultyOfScore != null">#{difficultyOfScore},</if>
<if test="leetcodeNumber != null">#{leetcodeNumber},</if>
<if test="leetcodeLink != null">#{leetcodeLink},</if>
<if test="tag != null">#{tag},</if>
<if test="createTime != null">#{createTime},</if>
<if test="isDelete != null">#{isDelete},</if>
</trim>
</script>

3. 代入条件构造器逻辑

上面的部分对于简单的 insert来说,其实已经够用了,但是对于一些用户的查询或修改逻辑,比如我们在 servece 中写的那些筛选条件,排序等,它们又是如何起作用的呢?这里我们要分两个阶段来说:启动阶段执行阶段

1. 启动阶段的 “ew” 参数

首先,我们在代码中写的所有wrapper(条件构造器),这些wrapper包含了筛选条件,排序规则之类的,其实最终都被视为一个参数“ew”(Entity Wrapper)放入BaseMapper中进行操作

// service 层
 @Override
public List<CsdnUserInfo> allUser() {
     QueryWrapper<CsdnUserInfo> wrapper = new QueryWrapper<>();
     wrapper.eq("is_delete", 0);
     wrapper.orderByDesc("user_weight");
     return this.list(wrapper);
}

// IService 类
default List<T> list(Wrapper<T> queryWrapper) {
     return this.getBaseMapper().selectList(queryWrapper);
}
    
// BaseMapper 类
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);

这个ew参数在下面就是核心,我们上面提到了,在应用启动阶段,经过基本语法和表的代入后,可以形成一些基础SQL脚本。而对于像selectList 这种,会带有ew参数的,他们的基础SQL就会比较复杂了,它们会在SQL中大量预留ew参数相关的内容。比如selectList 最终就会生成如下的一大串脚本:

<script>
<if test="ew != null and ew.sqlFirst != null">
    ${ew.sqlFirst}
</if> 
SELECT 
<choose>
	<when test="ew != null and ew.sqlSelect != null">
      ${ew.sqlSelect}
    </when>
<otherwise>id,user_name,nick_name,like_status,collect_status,comment_status,user_weight,user_home_url,curr_blog_url,article_type,create_time,update_time,is_delete</otherwise>
</choose> FROM csdn_user_info 
	<if test="ew != null">
		<where>
			<if test="ew.entity != null">
				<if test="ew.entity.id != null">id=#{ew.entity.id}</if>
				<if test="ew.entity['userName'] != null"> AND user_name=#{ew.entity.userName}</if>
				<if test="ew.entity['nickName'] != null"> AND nick_name=#{ew.entity.nickName}</if>
				<if test="ew.entity['likeStatus'] != null"> AND like_status=#{ew.entity.likeStatus}</if>
				<if test="ew.entity['collectStatus'] != null"> AND collect_status=#{ew.entity.collectStatus}</if>
				<if test="ew.entity['commentStatus'] != null"> AND comment_status=#{ew.entity.commentStatus}</if>
				<if test="ew.entity['userWeight'] != null"> AND user_weight=#{ew.entity.userWeight}</if>
				<if test="ew.entity['userHomeUrl'] != null"> AND user_home_url=#{ew.entity.userHomeUrl}</if>
				<if test="ew.entity['currBlogUrl'] != null"> AND curr_blog_url=#{ew.entity.currBlogUrl}</if>
				<if test="ew.entity['articleType'] != null"> AND article_type=#{ew.entity.articleType}</if>
				<if test="ew.entity['createTime'] != null"> AND create_time=#{ew.entity.createTime}</if>
				<if test="ew.entity['updateTime'] != null"> AND update_time=#{ew.entity.updateTime}</if>
				<if test="ew.entity['isDelete'] != null"> AND is_delete=#{ew.entity.isDelete}</if>
			</if>
			<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
				<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal"> AND </if>
				 ${ew.sqlSegment}
			</if>
		</where>
		<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
          ${ew.sqlSegment}
        </if>
	</if>
<if test="ew != null and ew.sqlComment != null">
   ${ew.sqlComment}
</if>
</script>

这里面,我们看到这段SQL中,涉及ew参数的有这么几个非常重要的元素:

  • ew.sqlFirst :表示SQL片段,可以自定义SQL片段放在SQL的最开始位置

  • ew.sqlSelect:表示要查询的字段,默认为"*",即查询所有字段。可以通过该属性指定要查询的字段,多个字段之间用逗号分隔

  • ew.entity:表示要查询的实体对象。可以通过该属性指定要查询的实体对象,可以根据实体对象的属性进行条件封装

  • ew.sqlSegment:表示SQL片段,可以自定义SQL片段,用于在动态SQL中添加自定义的SQL语句

  • ew.sqlComment:表示SQL注释,可以为SQL语句添加注释。可以通过该属性指定要为SQL语句添加的注释内容。

2. 执行阶段的 “ew” 参数

以我们上面提到过的代码为例

public List<CsdnUserInfo> allUser() {
     QueryWrapper<CsdnUserInfo> wrapper = new QueryWrapper<>();
     wrapper.eq("is_delete", 0);
     wrapper.orderByDesc("user_weight");
     return this.list(wrapper);
}

这样的 wrapper 最终会形成这样一个对象,我们为它添加了两个属性,即

在这里插入图片描述
有些眼尖的同学发现了,xml中用的是 ew.sqlSegment, 而我们这里的条件全部都进了 ew.expression 中,这两也没对上啊。其实在条件构造器中 ew.sqlSegment = expression.getSqlSegment + lastSql

// AbstractWrapper
public String getSqlSegment() {
     return this.expression.getSqlSegment() + this.lastSql.getStringValue();
}

而 expression.getSqlSegment() 又是其下的各个小 Segment 拼出来的

this.sqlSegment = this.normal.getSqlSegment() + this.groupBy.getSqlSegment() + this.having.getSqlSegment() + this.orderBy.getSqlSegment();

如此一来,利用ognl,筛选条件也被我们拼凑出来了,最后拼接出 ew.sqlSegment 实际解析成为了一个字符串

(is_delete = #{ew.paramNameValuePairs.MPGENVAL1}) ORDER BY user_weight DESC

这个时候,又有眼尖的同学发现了,我们在代码中明明已经写死了 is_delete = 0,为什么解析完却成了 is_delete = #{ew.paramNameValuePairs.MPGENVAL1}

实际上这里有一个兼容的作用,我们可以在业务代码里写 wrapper.eq(“is_delete”, 0),也可以填上一个未知的变量 wrapper.eq(“is_delete”, var1),我们都知道为了防止SQL注入,实际上对于入参我们都是在最后才提交进SQL中的,不可能因为你是写死的0,就提前把0填写在这里。所以,此处会先把你填的东西存放在 paramNameValuePairs 参数键值对中,再后面提交SQL的时候才取出来。其源码如下:

// AbstractWrapper

    // 把一个条件翻译sqlSegment,以上述wrapper.eq("is_delete", 0)为例,此处column 为字符串“is_delete” ;sqlKeyword 为EQ的枚举值,会被翻译成字符串,最后的参数值则会被翻译成成如 {ew.paramNameValuePairs.MPGENVAL}=
    protected Children addCondition(boolean condition, R column, SqlKeyword sqlKeyword, Object val) {
        return this.maybeDo(condition, () -> {
            this.appendSqlSegments(this.columnToSqlSegment(column), sqlKeyword, () -> {
                return this.formatParam((String)null, val);
            });
        });
    }
    
    protected final String formatParam(String mapping, Object param) {
        String genParamName = "MPGENVAL" + this.paramNameSeq.incrementAndGet();
        String paramStr = this.getParamAlias() + ".paramNameValuePairs." + genParamName;
        this.paramNameValuePairs.put(genParamName, param);
        return SqlScriptUtils.safeParam(paramStr, mapping);
    }

那么现在我们已经拥有了两件东西:1.一个大而全的基础SQL语句(里面依赖了大量的ew内容),2.一个完整的“ew”对象。同时拥有这两者后,就能够利用 Mybatis 的动态解析能力去构建出一个真正的可执行的SQL了


总结

本次我们介绍了MybatisPlus 构造器wrapper的使用方式及其易错点,同时也针对其运行的原理进行了解释,只有深刻理解了它的原理,我们才能更灵活的使用,并且更快的排查出问题。所以也希望大家能结合源码再思考一下,以便更好地掌握这部分内容。

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

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

相关文章

极简—springMVC工作流程

1、流程图 2、流程 发起请求&#xff1a;客户端通过 HTTP 协议向服务器发起请求。前端控制器&#xff1a;这个请求会先到前端控制器 DispatcherServlet&#xff0c;它是整个流程的入口点&#xff0c;负责接收请求并将其分发给相应的处理器。处理器映射&#xff1a;DispatcherS…

SDN和SD-WAN的对比

在数字化浪潮的推动下&#xff0c;SDN&#xff08;软件定义网络&#xff09;和SD-WAN&#xff08;软件定义广域网&#xff09;作为企业网络技术的两大支柱&#xff0c;正逐步引领网络架构的革新。尽管两者在理念和基础上有所共通&#xff0c;但在实际应用、功能特性和部署策略上…

视频号小店不直播怎么出单?这里面的秘密,一篇文章全曝光!

大家好&#xff0c;我是电商糖果 这两年关于视频号搞电商的话题度非常高&#xff0c;也吸引了很多商家入驻。 视频号因为背后巨大的私域流量池扶持&#xff0c;所以它的转化率非常高。 根据官方发出来的战报&#xff0c;我们也可以看出它的数据是翻倍增长。 在2024微信公开…

52. 【Android教程】网页视图:WebView

在前面的章节我们所围绕的全部都是纯客户端开发&#xff0c;我们叫 Native 开发。这样的好处就是体验和性能会非常好&#xff0c;但是在实际的使用中我们会发现存在大量的 H5 页面。这样就可以结合 Native / H5 双端的优势完成一个混合开发&#xff0c;而在这种开发模式中首当其…

Photoshop 2022 for Mac/win:释放创意,打造专业级的图像编辑体验

在数字图像编辑的世界里&#xff0c;Adobe Photoshop 2022无疑是那颗璀璨的明星。这款专为Mac和Windows用户设计的图像处理软件&#xff0c;以其卓越的性能和丰富的功能&#xff0c;赢得了全球数百万创作者的青睐。 Photoshop 2022在继承前代版本强大功能的基础上&#xff0c;…

QGraphicsView实现简易地图11『指定层级-定位坐标』

前文链接&#xff1a;QGraphicsView实现简易地图10『自适应窗口大小』 提供一个地图初始化函数&#xff0c;指定地图显示的中心点和地图缩放层级 能够让地图显示某一层级的瓦片&#xff0c;并将中心点坐标显示在视图中心。 1、动态演示效果 7级地图-大连-老虎滩 定位到 8级地图…

ChatGLM3大模型本地化部署、应用开发与微调

文章目录 写在前面ChatGLM3推荐图书作者简介推荐理由粉丝福利写在后面 写在前面 本期博主给大家推荐一本初学者学习并部署大模型的入门书籍&#xff0c;一起来看看吧&#xff01; ChatGLM3 ChatGLM3是继一系列先进语言模型之后的又一力作&#xff0c;专为追求高精度和广泛适…

nature《自然》期刊文献怎么在家查看下载

nature《自然》期刊我们都知道&#xff0c;是世界上历史悠久的、最有名望的科学杂志之一。下载该期刊文献是需要使用权限的&#xff0c;如果你没有nature《自然》期刊的资源&#xff0c;又该如何获取呢&#xff1f;请看本文的经验分享。 一、先百度“文献党下载器” 在文献党下…

力扣HOT100 - 153. 寻找旋转排序数组中的最小值

解题思路&#xff1a; 与33题类似。 class Solution {public int findMin(int[] nums) {int l 0;int r nums.length - 1;if (nums[r] > nums[l]) return nums[0];while (l < r) {int mid l (r - l) / 2;if (nums[0] > nums[mid]) {r mid - 1;} else {l mid 1…

如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问

文章目录 前言1. 安装 Raspberry Pi OS2. 测试 web 站点3. 安装静态样例站点4. 将web站点发布到公网4.1 安装 Cpolar4.2 cpolar进行token认证4.3 生成cpolar随机域名网址4.4 生成cpolar二级子域名4.5 将参数保存到cpolar配置文件中4.6 测试修改后配置文件4.7 配置cpolar服务开机…

100G ZR4 80KM光模块产品亮点有哪些

之前的文章我们介绍了100G ZR4 80KM光模块的产品特征以及技术原理等&#xff0c;那本期文章我们来了解一下易天第二代100G ZR4 80KM光模块的产品亮点。 首先我们通过下面这张表格以最直观的方式来了解第一代和第二代100G ZR4 80KM光模块在工作温度、功耗、FEC纠错等方面有哪些…

Vue CLI配置代理、2.0、3.0

一、vue cli2.0 代理配置 proxy: {/api:{target: "http://localhost:8067",pathRewrite: {/api: }}, } 一、vue cli3.0 代理配置 proxy: {/api: {target: http://localhost:8067,pathRewrite: {/api: }} }

文件快递柜-免费开源-FileCodeBox

像拿快递一样取文件 什么FileCodeBox FileCodeBox 中文名是 文件快递柜&#xff0c;取文件像取快递一样&#xff0c;支持通过匿名口令分享文本&#xff0c;文件。 很多时候&#xff0c;我们都想将一些文件或文本传送给别人&#xff0c;或者跨端传递一些信息&#xff0c;但是我…

商务分析方法与工具(六):Python的趣味快捷-字符串巧妙破解密码本、身份证号码、词云图问题

Tips&#xff1a;"分享是快乐的源泉&#x1f4a7;&#xff0c;在我的博客里&#xff0c;不仅有知识的海洋&#x1f30a;&#xff0c;还有满满的正能量加持&#x1f4aa;&#xff0c;快来和我一起分享这份快乐吧&#x1f60a;&#xff01; 喜欢我的博客的话&#xff0c;记得…

vue路由知识补充(updating···)

1路由守卫中的next() next()方法表示放行&#xff0c;如果不执行此方法路由不会跳转&#xff0c;此方法可以接收一个参数 字符串路径&#xff1a;如果传递一个字符串路径&#xff0c;那么路由会重定向到该路径。 next(/home); 路由对象&#xff1a;如果传递一个路由对象&…

MySQL#MySql数据库的操作

目录 一、创建数据库 二、字符集和校验规则 1.查看系统默认字符集以及校验规则 2.查看数据库支持的字符集 3.查看数据库支持的字符集校验规则 4.校验规则对数据库的影响 1.以UTF-8格式创建数据库 2.不区分大小写 3.区分大小写 4 大小写对数据库的影响 三、操纵数据…

C++从入门到入土(二)——初步认识类与对象

目录 前言 类与对象的引入 类的定义 类的访问限定符及封装 访问限定符&#xff1a; 封装&#xff1a; 类的作用域 类的实例化 类的大小 this指针 this指针的特性 前言 各位佬们&#xff0c;在开始本篇文章的内容之前&#xff0c;我想先向大家道个歉&#xff0c;由于…

跨越智能建筑桥梁:西门子PLC无缝对接BACnet楼宇自动化系统化

智能楼宇每一个环节的互联互通都至关重要&#xff0c;而PLC&#xff08;可编程逻辑控制器&#xff09;作为自动化领域的基石&#xff0c;其与BACnet协议的融合无疑成为了构建智能楼宇神经系统的关键节点。今天&#xff0c;让我们深入探讨如何利用先进的PLC转BACnet协议网关&…

Windows平台PyCharm之PySide6开发环境搭建与配置

PySide6 是一个用于创建跨平台 GUI 应用程序的库&#xff0c;它是 Qt for Python 的官方库。Qt 是一个跨平台的 C 应用程序框架&#xff0c;用于开发具有图形用户界面&#xff08;GUI&#xff09;的应用程序。PySide6 允许开发者使用 Python 语言访问 Qt 的功能&#xff0c;从而…

从一毫秒到十秒:“最纯硅”激增相干时间,引领百万量子比特芯片革命

“量子计算的未来正在崭露头角”——至少专家们是这么认为的。 量子计算机并非使用传统计算中的常规1和0&#xff0c;而是利用量子物理学那些神奇的特性&#xff0c;以指数级速度执行某些计算任务。然而&#xff0c;制造一台实用的大型量子计算机面临着巨大的挑战&#xff0c;其…
最新文章