江苏中益建设官方网站/精准营销通俗来说是什么
2.核心功能
刚才的案例中都是以id
为条件的简单CRUD
,一些复杂条件的SQL
语句就要用到一些更高级的功能了。
2.1.条件构造器
除了新增以外,修改、删除、查询的SQL
语句都需要指定where
条件。因此BaseMapper
中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件。
参数中的Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Wrapper
的子类AbstractWrapper
提供了where
中包含的所有条件构造方法:
而QueryWrapper
在AbstractWrapper
的基础上拓展了一个select
方法,允许指定查询字段:
而UpdateWrapper
在AbstractWrapper
的基础上拓展了一个set
方法,允许指定SQL
中的SET
部分:
2.1.1.QueryWrapper
无论是修改、删除、查询,都可以使用QueryWrapper
来构建查询条件。接下来看一些例子:
1.查询:查询出名字中带o
的,存款大于等于1000
元的人。代码如下:
@Test
void testQueryWrapper() {// 1.构建查询条件 where name like "%o%" AND balance >= 1000QueryWrapper<User> wrapper = new QueryWrapper<User>().select("id", "username", "info", "balance").like("username", "o").ge("balance", 1000);// 2.查询数据List<User> users = userMapper.selectList(wrapper);users.forEach(System.out::println);
}
更新:更新用户名为Jack
的用户的余额为2000
,代码如下:
@Test
void testUpdateByQueryWrapper() {// 1.构建查询条件 where name = "Jack"QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");// 2.更新数据,user中非null字段都会作为set语句User user = new User();user.setBalance(2000);userMapper.update(user, wrapper);
}
2.1.2.UpdateWrapper
基于BaseMapper
中的update
方法更新时只能直接赋值,对于一些复杂的需求就难以实现。
例如:更新id
为1,2,4
的用户的余额,扣200
,对应的SQL
应该是:
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
SET
的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper
中的setSql
功能了:
// 更新id为1,2,4的用户的余额,扣200@Testvoid testUpdateWrapper() {// List<Long> ids = new ArrayList<>(Arrays.asList(1L, 2L, 4L)); //JDK8 可以使用这种方式List<Long> ids = List.of(1L, 2L, 4L); // JDK9 之后才有的of方法// 1.生成SQLUpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<User>().setSql("balance = balance -200").in("id", ids); // WHERE id in (1, 2, 4)// 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,// 而是基于UpdateWrapper中的setSql来更新userMapper.update(null, userUpdateWrapper);}
2.1.3.LambdaQueryWrapper
无论是QueryWrapper
还是UpdateWrapper
在构造条件的时候都需要写死字段名称,会出现字符串魔法值
。这在编程规范中显然是不推荐的。
那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的gettter
方法结合反射技术。因此我们只要将条件对应的字段的getter
方法传递给MybatisPlus
,它就能计算出对应的变量名了。而传递方法可以使用JDK8
中的方法引用和Lambda
表达式。
因此MybatisPlus
又提供了一套基于Lambda的Wrapper
,包含两个:
-LambdaQueryWrapper
LambdaUpdateWrapper
分别对应QueryWrapper
和UpdateWrapper
其使用方式如下:
@Testvoid testLambdaQueryWrapper() {// 1.构建查询条件 where name like "%o%" AND balance >= 1000LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<User>().select(User::getId, User::getUsername, User::getInfo, User::getBalance).like(User::getUsername, "o").ge(User::getBalance, 1000);// 2.查询数据List<User> users = userMapper.selectList(userLambdaQueryWrapper);users.forEach(System.out::println);}// 更新用户名为jack的用户的余额为2000@Testvoid testLambdaUpdateByQueryWrapper() {// 1.构建查询条件 where name = "Jack"LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().eq(User::getUsername, "Jack");// 2.更新数据,user中非null字段都会作为set语句User user = new User();user.setBalance(2000);userMapper.update(user, wrapper);}
2.2.自定义SQL
在演示UpdateWrapper
的案例中,我们在代码中编写了更新的SQL
语句:
这种写法在某些企业也是不允许的,因为SQL
语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in
语句,只能将SQL
写在Mapper.xml
文件,利用foreach
来生成动态SQL
。
这实在是太麻烦了。假如查询条件更复杂,动态SQL
的编写也会更加复杂。
所以,MybatisPlus
提供了自定义SQL
功能,可以让我们利用Wrapper
生成查询条件,再结合Mapper.xml
编写SQL
2.2.1.基本用法
以当前案例来说,我们可以这样写:
@Test
void testCustomWrapper() {// 1.准备自定义查询条件List<Long> ids = List.of(1L, 2L, 4L);QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);// 2.调用mapper的自定义方法,直接传递WrapperuserMapper.deductBalanceByIds(200, wrapper);
}
然后在UserMapper
中自定义SQL
:
package com.itheima.mp.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Param;public interface UserMapper extends BaseMapper<User> {@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}
这样就省去了编写复杂查询条件的烦恼了。
总结
在
mapper
方法参数中用Param
注解声明wrapper
变量名称,必须是ew
。
为什么必须要用ew
?mybatisPlus
源码规定的,这也成为一种约定俗成的规范。当然,你也可以手动修改生成的代码,将ew
改为其他名字,但这样可能导致一些示例代码不适用。
2.2.2.多表关联
理论上来讲MyBatisPlus
是不支持多表查询的,不过我们可以利用Wrapper
中自定义条件结合自定义SQL
来实现多表查询的效果。
例如,我们要查询出所有收货地址在北京的并且用户id
在1、2、4
之中的用户
要是自己基于mybatis
实现SQL
,大概是这样的:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">SELECT *FROM user uINNER JOIN address a ON u.id = a.user_idWHERE u.id<foreach collection="ids" separator="," item="id" open="IN (" close=")">#{id}</foreach>AND a.city = #{city}</select>
可以看出其中最复杂的就是WHERE
条件的编写,如果业务复杂一些,这里的SQL
会更变态。
但是基于自定义SQL
结合Wrapper
的玩法,我们就可以利用Wrapper
来构建查询条件,然后手写SELECT
及FROM
部分,实现多表查询。
查询条件这样来构建:
@Test
void testCustomJoinWrapper() {// 1.准备自定义查询条件QueryWrapper<User> wrapper = new QueryWrapper<User>().in("u.id", List.of(1L, 2L, 4L)).eq("a.city", "北京");// 2.调用mapper的自定义方法List<User> users = userMapper.queryUserByWrapper(wrapper);users.forEach(System.out::println);
}
然后在UserMapper
中自定义方法:
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);
当然,也可以在UserMapper.xml
中写SQL
:
<select id="queryUserByWrapper" resultType="com.itheima.mp.domain.po.User">SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
</select>
2.3.Service接口
MybatisPlus
不仅提供了BaseMapper
,还提供了通用的Service
接口及默认实现,封装了一些常用的service
模板方法。
通用接口为IService
,默认实现为ServiceImpl
,其中封装的方法可以分为以下几类:
save
:新增remove
:删除update
:更新get
:查询单个结果list
:查询集合结果count
:计数page
:分页查询
2.3.1.CRUD
我们先俩看下基本的CRUD
接口。
新增:
save
是新增单个元素saveBatch
是批量新增saveOrUpdate
是根据id
判断,如果数据存在就更新,不存在则新增saveOrUpdateBatch
是批量的新增或修改
删除:
removeById
:根据id
删除removeByIds
:根据id
批量删除removeByMap
:根据Map
中的键值对为条件删除remove(Wrapper<T>)
:根据Wrapper
条件删除:暂不支持removeBatchByIds
修改:
updateById
:根据id
修改update(Wrapper<T>)
:根据UpdateWrapper
修改,Wrapper
中包含set
和where
部分update(T,Wrapper<T>)
:按照T内的数据修改与Wrapper
匹配到的数据updateBatchById
:根据id
批量修改
查询
Get
:
getById
:根据id
查询1
条数据getOne(Wrapper<T>)
:根据Wrapper
查询1
条数据getBaseMapper
:获取Service
内的BaseMapper
实现,某些时候需要直接调用Mapper
内的自定义SQL
时可以用这个方法获取到Mapper
List
:
listByIds
:根据id
批量查询list(Wrapper<T>)
:根据Wrapper
条件查询多条数据list()
:查询所有
Count
:
count()
:统计所有数量count(Wrapper<T>)
:统计符合Wrapper
条件的数据数量
getBaseMapper
:
当我们在service
中要调用Mapper
中自定义SQL
时,就必须获取service
对应的Mapper
,就可以通过这个方法:
项目结构如下:
来到 UserService
中创建 测试类
接下来,我们快速实现下面4个接口:
首先,我们在项目中引入几个依赖:
pom.xml
<!--swagger-->
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>4.1.0</version>
</dependency>
<!--web-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
然后在application.xml
需要配置swagger
信息:
knife4j:enable: trueopenapi:title: 用户管理接口文档description: "用户管理接口文档"email: zhanghuyi@itcast.cnconcat: 墨苒孤url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.itheima.mp.controller
Alt + 8
选择 springboot
服务,启动,浏览器输入 http://localhost:8080/doc.html#/home
然后,接口需要两个实体:
UserFormDTO
:代表新增时的用户表单UserVO
:代表查询的返回结果
首先是UserFormDTO
:
package com.itheima.mp.domain.dto;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {@ApiModelProperty("id")private Long id;@ApiModelProperty("用户名")private String username;@ApiModelProperty("密码")private String password;@ApiModelProperty("注册手机号")private String phone;@ApiModelProperty("详细信息,JSON风格")private String info;@ApiModelProperty("账户余额")private Integer balance;
}
然后是UserVO
:
package com.itheima.mp.domain.vo;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用户VO实体")
public class UserVO {@ApiModelProperty("用户id")private Long id;@ApiModelProperty("用户名")private String username;@ApiModelProperty("详细信息")private String info;@ApiModelProperty("使用状态(1正常 2冻结)")private Integer status;@ApiModelProperty("账户余额")private Integer balance;
}
用 @Autowired
注入,Spring
并不推荐,而是推荐我们使用构造函数注入
构造函数注入如下:
但是这样会带来一个问题,如果成员很多,构造函数就会很多,看起来很繁琐。那么就可以使用Lombok
注解帮我们简化
上图这样看起来是可以了,但是,一个类可以有很多的成员,并不是每一个都是需要注入的。那么怎么区分哪些需要注入,哪些不需要注入呢?
可以给 成员 加上 final
来区分, 这样就必须在类初始化的时候,对加了 final
的成员进行初始化,所以最终写法如下:
@RequiredArgsConstructor
表示 会对 final
修饰的成员进行创建构造函数,没有修饰就不会生成构造函数
新增接口实现:
@PostMapping@ApiOperation("新增用户")public void saveUser(@RequestBody UserFormDTO userFormDTO) {// 1.转换DTO为POUser user = BeanUtil.copyProperties(userFormDTO, User.class);// 2.新增userService.save(user);}
由于传入的 是 DTO
, 而我们需要保存的是 PO
,所以需要先把DTO
转换为PO
。这里使用胡图工具包,其余接口同理
@DeleteMapping("/{id}")@ApiOperation("删除用户")public void removeUserById(@PathVariable("id") Long userId){userService.removeById(userId);}@GetMapping("/{id}")@ApiOperation("根据id查询用户")public UserVO queryUserById(@PathVariable("id") Long userId){// 1.查询用户User user = userService.getById(userId);// 2.处理po转voreturn BeanUtil.copyProperties(user, UserVO.class);}@GetMapping@ApiOperation("根据id集合查询用户")public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){// 1.查询用户List<User> users = userService.listByIds(ids);// 2.处理po转vo (集合用copyToList)return BeanUtil.copyToList(users, UserVO.class);}
代码在github
https://github.com/RanGuMo/mp-demo
相关文章:

【JAVA】黑马MybatisPlus 学习笔记【二】【核心功能】
2.核心功能 刚才的案例中都是以id为条件的简单CRUD,一些复杂条件的SQL语句就要用到一些更高级的功能了。 2.1.条件构造器 除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id作为where条件以外&…...

区块链实验室(30) - 区块链期刊:Distributed Ledger Technologies: Research and Practice
区块链涉及多学科及技术,众多期刊接收区块链文章。Distributed Ledger Technologies: Research and Practice是ACM出版集团的一本期刊。 Distributed Ledger Technologies: Research and Practice创刊历史很短,始于2022年,出版期数也不多。 载…...

Nginx【通俗易懂】《中篇》
目录 1.Url重写rewrite 2.防盗链 3.静态资源压缩 4.跨域问题 1.Url重写rewrite 🤩🤩🤩 1.1.rewrite书写格式 rewrite是实现URL重写的关键指令,根据regex(正则表达式)部分内容,重定向到rep…...

组件的二次封装
在React中,使用扩展运算符(...)来传递props的作用是将一个对象的所有可枚举属性(包括自身的和继承的)复制到新创建的对象中。当我们在二次封装组件时使用它,可以方便地将所有传递给我们的props传递给基础组…...

curl+postman 在java开发中的使用(提高效率)
概念 curl 是一个常用的命令行工具,用于发送各种类型的 HTTP 请求,包括 GET、POST、PUT、DELETE 等。它也可以用来下载文件、上传文件、设置 cookie、发送 multipart/form-data 等等。 使用 调用post接口 实际中的接口: curl --location…...

【电子取证:FTK IMAGER 篇】DD、E01系统镜像动态仿真
文章目录 【电子取证:FTK Imager 篇】DD、E01系统镜像动态仿真一、DD、E01系统镜像动态仿真 (一)使用到的软件 1、FTK Imager (v4.5.0.3)2、VMware Workstation 15 Pro (v15.5.2)(二)FTK Imager 挂载镜像 1、选择 …...

netcat瑞士军刀
netcat瑞士军刀 1、nc简介3、从示例中学习2、命令格式及常用参数 1、nc简介 nc(netcat)是一个短小精悍、功能实用、简单可靠的网络工具,主要有如下作用: (1)端口侦听,nc 可以作为 server 以 TC…...

【征稿倒计时十天】第三届高性能计算与通信工程国际学术会议(HPCCE 2023)
【有ISSN、ISBN号!!往届均已完成EI检索】 第三届高性能计算与通信工程国际学术会议(HPCCE 2023) 2023 3rd International Conference on High Performance Computing and Communication Engineering (HPCCE 2023) 2023年12月22-24日 | 中国哈尔滨 第三…...

编程应用实际场景:台球厅怎么样用电脑给客人计时,台球计时收费系统操作教程
一、前言 准确控制顾客在店内游玩的时间,从而控制店内的各项成本,并提升店内的客流量。在顾客享受计时项目的时候,可以同时添加其他食物消费,并将单据合并统一结账。软件中的会员功能可以为客户办理会员可以使用灯控器控灯&#…...

云计算大屏,可视化云计算分析平台(云实时数据大屏PSD源文件)
大屏组件可以让UI设计师的工作更加便捷,使其更高效快速的完成设计任务。现分享可视化云分析系统、可视化云计算分析平台、云实时数据大屏的大屏Photoshop源文件,开箱即用! 若需 更多行业 相关的大屏,请移步小7的另一篇文章&#…...

高频js-----js执行机制 Event Loop
修改代码,让代码每隔1秒输出1-5 for (var i 0; i < 5;i) {setTimeout(() > {console.log(i)}, 1000)} 首先我们需要了解js的执行机制 (Event Loop) js是单线层,如果现在执行上面代码的话 会输出 5个5 这里不明白的同学可以去看一下我以前发布的关于EventLoop的文章 …...

恢复出厂设置后在 Android 上恢复照片的 6 种常用方法
恢复出厂设置可帮助您删除电子设备的所有信息并将其恢复到原始系统状态。但是,如果您不小心按下了恢复出厂设置按钮并从 Android 设备中删除了所有难忘的照片,该怎么办?好吧,您无需担心,因为可以通过以下一些方法来恢复…...

人工智能_机器学习065_SVM支持向量机KKT条件_深度理解KKT条件下的损失函数求解过程_公式详细推导_---人工智能工作笔记0105
之前我们已经说了KKT条件,其实就是用来解决 如何实现对,不等式条件下的,目标函数的求解问题,之前我们说的拉格朗日乘数法,是用来对 等式条件下的目标函数进行求解. KKT条件是这样做的,添加了一个阿尔法平方对吧,这个阿尔法平方肯定是大于0的,那么 可以结合下面的文章去看,也…...

网线市场现状与发展趋势预测
随着物联网、5G、云计算等技术的迅速发展,全球对于高速、稳定的网络需求急剧增长,这进一步推动了网线市场的发展。各种网络应用场景,从家庭到企业、数据中心到智能城市,都需要大量的高质量网线来支持数据传输和通信需求。本文将对…...

力扣二叉树--第四十一天
前言 写完这三道题,二叉树部分就先告一段落了。其实还有很多模糊的地方。 内容 一、修剪二叉搜索树 669. 修剪二叉搜索树 给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[l…...

计算机视觉(P2)-计算机视觉任务和应用
一、说明 在本文中,我们将探讨主要的计算机视觉任务以及每个任务最流行的应用程序。 二、图像内容分类 2.1. 图像分类 图像分类是计算机视觉领域的主要任务之一[1]。在该任务中,经过训练的模型根据预定义的类集为图像分配特定的类。下图是著名的CIFAR…...

redis-学习笔记(Jedis zset 简单命令)
zadd & zrange zadd , 插入的第一个参数是 zset , 第二个参数是 score, 第三个参数是 member 成员 内部依据 score 排序 zrange 返回 key 对应的 对应区间内的值 zrangeWithScore 返回 key 对应的 对应区间内的值和分数 示例代码 zcard 返回 key 对应的 zset 的长度 示例代…...

uniapp实战 —— 弹出层 uni-popup (含vue3子组件调父组件的方法)
效果预览 弹出的内容 src\pages\goods\components\ServicePanel.vue <script setup lang"ts"> // 子组件调父组件的方法 const emit defineEmits<{(event: close): void }>() </script><template><view class"service-panel"…...

智能优化算法应用:基于平衡优化器算法3D无线传感器网络(WSN)覆盖优化 - 附代码
智能优化算法应用:基于平衡优化器算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于平衡优化器算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.平衡优化器算法4.实验参数设定5.算法…...

Netty详细文档
Netty教程 文章目录 Netty教程 Netty简介Netty 的介绍Netty 的应用场景互联网行业游戏行业大数据领域其它开源项目使用到 Netty Netty 的学习资料参考 Java BIO编程I/O 模型BIO、NIO、AIO 使用场景分析Java BIO 基本介绍Java BIO 工作机制Java BIO 应用实例问题分析 Java NIO编…...

C语言结构体和位段
自定义类型:结构体及联合和枚举 一.结构体类型的声明1.1 结构体的概念1.2结构的声明1.3特殊的声明1.4结构体的自引用1.5可以使用typedef重命名 二.结构体变量的创建和初始化2.1结构体变量的初始化使用{}2.2初始化:定义变量的同时赋初值。2.3结构体嵌套及…...

【剑指offer|图解|数组】寻找文件副本 + 螺旋遍历二维数组
🌈个人主页:聆风吟 🔥系列专栏:数据结构、剑指offer每日一练 🔖少年有梦不应止于心动,更要付诸行动。 文章目录 一. ⛳️寻找文件副本(题目难度:简单)1.1 题目1.2 示例1.3 限制1.4 解题思路一c代…...

Python核心编程之文件和输入输出
目录 一、文件对象 二、 文件内建函数[open()和file()] 1、工厂函数 file() 2、通用换行符支持(UNS)...

Axure 9基本元件,表单及表格元件简介,表单案例
目录 一.基本元件 1.元件基本介绍 2.基本元件的使用 二.表单及表格元件 三.表单案例 四.简单简历绘制 一.基本元件 1.元件基本介绍 概述 - 在Axure RP中,元件是**构建原型图的基础模块**。 将元件从元件库里拖拽到画布中,即可添加元件到你的原型…...

ARM I2C通信
1.概念 I2C总线是PHLIPS公司在八十年代初推出的一种串行的半双工同步总线,主要用于连接整体电路2.IIC总线硬件连接 1.IIC总线支持多主机多从机,但是在实际开发过程中,大多数采用单主机多从机模式 2.挂接到IIC总线上,每个从机设备都…...

Cent OS7 磁盘挂载:扩展存储空间和自动挂载
文章目录 (1)概述(2)查看磁盘使用情况(3)VMware虚拟机挂载磁盘(4)物理机磁盘挂载(5)ntfs硬盘处理 (1)概述 在Linux系统中,…...

《使用ThinkPHP6开发项目》 - 创建应用
《使用ThinkPHP6开发项目》 - 安装ThinkPHP框架-CSDN博客 《使用ThinkPHP6开发项目》 - 设置项目环境变量-CSDN博客 《使用ThinkPHP6开发项目》 - 项目使用多应用开发-CSDN博客 根据前面的步骤,我们现在就可以开发我们的项目开发了,根据项目开发的需要…...

SpringBoot进行自然语言处理,利用Hanlp进行文本情感分析
. # 📑前言 本文主要是SpringBoot进行自然语言处理,利用Hanlp进行文本情感分析,如果有什么需要改进的地方还请大佬指出⛺️ 🎬作者简介:大家好,我是青衿🥇 ☁️博客首页:CSDN主页放风…...

MySQL 报错 You can‘t specify target table for update in FROM clause解决办法
You can’t specify target table for update in FROM clause 其含义是:不能在同一表中查询的数据作为同一表的更新数 单独执行复合查询是正常的,如下: 但是当执行子查询删除命令时,报如下错误 DELETE FROM abpusers WHERE Id I…...

Linux中使用podman管理容器
本章主要介绍使用podman管理容器 了解什么是容器,容器和镜像的关系安装和配置podman拉取和删除镜像给镜像打标签导出和导入镜像创建和删除镜像数据卷的使用管理容器的命令使用普通用户管理容器 对于初学者来说,不太容易理解什么是容器,这里…...