许昌工程建设信息网站,专业外贸网站制作公司,河南郑州软件定制开发,学seo需要学什么专业如果你每天还在重复写 CRUD 的 SQL#xff0c;如果你对这些 SQL 已经不耐烦了#xff0c;那么你何不花费一些时间来阅读这篇文章#xff0c;然后对已有的老项目进行改造#xff0c;必有收获#xff01;一、MP 是什么MP 全称 Mybatis-Plus #xff0c;套用官方的解释便是成… 如果你每天还在重复写 CRUD 的 SQL如果你对这些 SQL 已经不耐烦了那么你何不花费一些时间来阅读这篇文章然后对已有的老项目进行改造必有收获一、MP 是什么MP 全称 Mybatis-Plus 套用官方的解释便是成为 MyBatis 最好的搭档,简称基友。它是在 MyBatis 的基础上只做增强不做改变为简化开发、提高效率而生。1. 三大特性1润物无声只做增强不做改变引入它不会对现有工程产生影响如丝般顺滑。2效率至上只需简单配置即可快速进行单表 CRUD 操作从而节省大量时间。3丰富功能代码生成、物理分页、性能分析等功能一应俱全。2. 支持数据库mysql 、mariadb 、oracle 、db2 、h2 、hsql 、sqlite 、postgresql 、sqlserver 、presto 、Gauss 、FirebirdPhoenix 、clickhouse 、Sybase ASE 、 OceanBase 、达梦数据库 、虚谷数据库 、人大金仓数据库 、南大通用数据库3. 框架结构实话说以上这些内容只要你打开官网也能看到那么我们接下来就先来实际操作一番二、MP实战1. 手摸手式项目练习1数据库及表准备sql 语句use test;
CREATE TABLE student (id int(0) NOT NULL AUTO_INCREMENT,dept_id int(0) NULL DEFAULT NULL,name varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,remark varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,PRIMARY KEY (id) USING BTREE
) ENGINE InnoDB AUTO_INCREMENT 7 CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ROW_FORMAT Dynamic;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO student VALUES (1, 1, 小菜, 关注小菜不迷路);
INSERT INTO student VALUES (2, 2, 小明, 好好学习天天向上);
2pom 依赖dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactId
/dependency
!--lombok--
dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.16.16/version
/dependency
!--MP插件--
dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.2.0/version
/dependency
!--Mysql--
dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion8.0.21/version
/dependency
!-- 连接池 --
dependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.2.1/version
/dependency
!--JUNIT--
dependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion4.13.1/version
/dependency
3配置文件spring:datasource:url: jdbc:mysql://localhost:3306/testusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver
4实体类Data
Builder
TableName(student)
public class User {TableId(type IdType.AUTO)private Integer id;private Integer deptId;private String name;private String remark;
}
5Mapperpublic interface UserMapper extends BaseMapperUser {}
6测试类RunWith(SpringRunner.class)
SpringBootTest
public class MapperTest {Autowiredprivate UserMapper userMapper;Testpublic void getAll() {ListUser users userMapper.selectList(null);users.forEach(System.out::println);}
}
/** OUTPUT:
User(id1, deptId1, name小菜, remark关注小菜不迷路)
User(id2, deptId1, name小明, remark好好学习天天向上)
**/
小菜结在以上的结果我们可以看到已经打印出了数据库中的全部数据两条。而并没有看到平时我们需要写的 mapper.xml 文件只是用到了 usermapper 中的 selectList() 方法而 UserMapper 继承了 BaseMapper 这个接口这个接口便是 MybatisPlus 提供给我们的我们再来看下这个接口给我们提供了哪些方法。2. CRUD 基操1insertTest
public void insert() {//这里使用了 lombok 中的建造者模式构建对象User user User.builder().deptId(1).name(小华).remark(小华爱学习).build();int insertFlag userMapper.insert(user);log.info(插入影响行数,{} | 小华的ID: {}, insertFlag, user.getId());
}
/** OUTPUT:
插入影响行数,1 | 小华的ID: 8
**/
可以看到我们不仅插入了数据而且还获取到了插入数据的ID但是值得注意的是这里的 ID 虽然是自增的但并非是 MP 默认的 ID生成策略而是我们在实体类中指定的在 MP 中支持的主键生成策略有以下几种我们既然已经看到了 TableId 这个注解那我们再来关注一个常用注解 TableField从注解名上我们就可以看出TableId 是用来标记主键 ID 的而 TableField 是用来标记其他字段的。可以看得出来这个注解中存在的值还是比较多的下面介绍几个常用的值value用于解决字段名不一致问题和驼峰命名比如实体类中属性名为 remark但是表中的字段名为 describe 这个时候就可以使用 TableField(valuedescribe) 来进行转换。驼峰转换如果在全局中有配置驼峰命名这个地方可不写。exist用于在数据表中不存在的字段我们可以使用 TableField(exist false) 来进行标记condition用在预处理 WHERE 实体条件自定义运算规则比如我配置了 TableField(condition SqlCondition.LIKE)输出 SQL 为select 表 where name LIKE CONCAT(%,值,%)其中 SqlCondition 值如下update用在预处理 set 字段自定义注入比如我配置了 TableField(update %s1)其中 %s 会填充字段输出 SQL 为update 表名 set 字段 字段1 where 条件select用于是否查询时约束如果我们有个字段 remark 是 text 类型的查询的时候不想查询该字段那么就可以使用 TableField(select false) 来约束查询的时候不查询该字段2updateMybatisPlus 的更新操作存在两种int updateById(Param(et) T entity);int update(Param(et) T entity, Param(ew) WrapperT updateWrapper);
根据 ID 更新Test
public void update() {User user User.builder().id(3).name(小华).remark(小华爱玩游戏).build();userMapper.updateById(user);
}
/** 更新结果:
User(id3, deptId1, name小华, remark小华爱玩游戏)
**/
根据条件更新Test
public void update() {UpdateWrapperUser updateWrapper new UpdateWrapper();updateWrapper.eq(name,小华).set(remark,小华爱下棋);userMapper.update(null, updateWrapper);
}
/** 更新结果:
User(id3, deptId1, name小华, remark小华爱下棋)
**/
我们也可以将要更新的条件放进 user 对象 里面Test
public void update() {UpdateWrapperUser updateWrapper new UpdateWrapper();updateWrapper.eq(name,小华);User user User.builder().remark(小华爱游泳).build();userMapper.update(user, updateWrapper);
}
/** 更新结果:
User(id3, deptId1, name小华, remark小华爱游泳)
**/
3delete在 MybatisPlus 中删除的方式相对于更新多总共有四种int deleteById(Serializable id);int deleteByMap(Param(cm) MapString, Object columnMap);int delete(Param(ew) WrapperT wrapper);int deleteBatchIds(Param(coll) Collection? extends Serializable idList);
根据 ID 删除Test
public void deleteById() {userMapper.deleteById(3);
}
/** SQL语句
DELETE FROM student WHERE id 3;
**/
根据 Map 删除Test
public void deleteByMap() {HashMapString, Object columnMap new HashMap();columnMap.put(name,小华);columnMap.put(remark,小华爱游泳);userMapper.deleteByMap(columnMap);
}
/** SQL语句
DELETE FROM student WHRE name 小华 AND remark 小华爱游泳;
**/
根据 Wrapper 删除Test
public void delete() {UpdateWrapperUser wrapper new UpdateWrapper();wrapper.eq(remark,小华爱下棋);userMapper.delete(wrapper);
}
/** SQL语句
DELETE FROM student WHRE remark 小华爱下棋;
**/
根据 Wrapper 删除还有另外一种方式直接将实体类放入 Wrapper 中包装Test
public void delete() {User user User.builder().remark(小华爱下棋).build();UpdateWrapperUser wrapper new UpdateWrapper(user);userMapper.delete(wrapper);
}
/** SQL语句
DELETE FROM student WHRE remark 小华爱下棋;
**/
根据 ID 批量删除Test
public void deleteBatchIds() {ListInteger idList new ArrayList();idList.add(4);idList.add(7);userMapper.deleteBatchIds(idList);
}
/** SQL语句
DELETE FROM student WHERE id In (4,7)
**/
4select查询操作在我们开发中是最经常用到的也是重中之重。MybatisPlus 中支持查询的方法也比较多如下T selectById(Serializable id);ListT selectBatchIds(Param(coll) Collection? extends Serializable idList);ListT selectByMap(Param(cm) MapString, Object columnMap);T selectOne(Param(ew) WrapperT queryWrapper);Integer selectCount(Param(ew) WrapperT queryWrapper);ListT selectList(Param(ew) WrapperT queryWrapper);ListMapString, Object selectMaps(Param(ew) WrapperT queryWrapper);ListObject selectObjs(aram(ew) WrapperT queryWrapper);IPageT selectPage(IPageT page, Param(ew) WrapperT queryWrapper);IPageMapString, Object selectMapsPage(IPageT page, Param(ew) WrapperT queryWrapper);
可以看到总共有 10 个方法我们接下来一个一个测试查询所有Test
public void selectList() {ListUser users userMapper.selectList(null);users.forEach(System.out::println);
}
/** OUTPUT:
User(id1, deptId1, name小菜, remark关注小菜不迷路)
User(id2, deptId1, name小明, remark好好学习天天向上)SQL语句:
SELECT id, dept_id, name, remark FROM student;
**/
查询数量Test
public void selectCount() {QueryWrapperUser queryWrapper new QueryWrapper();queryWrapper.like(name,小);System.out.println(userMapper.selectCount(queryWrapper));
}
/** OUTPUT:
2SQL语句:
SELECT COUNT( 1 ) FROM student WHERE (name LIKE %小%);
**/
根据 ID 查询Test
public void selectById() {User user userMapper.selectById(1);System.out.println(user);
}
/** OUTPUT:
User(id1, deptId1, name小菜, remark关注小菜不迷路)SQL语句:
SELECT id, dept_id, name, remark FROM student WHERE ID 1;
**/
根据 ID 批量查询Test
public void selectBatchIds() {ListUser users userMapper.selectBatchIds(Arrays.asList(1, 2));users.forEach(System.out::println);
}
/** OUTPUT:
User(id1, deptId1, name小菜, remark关注小菜不迷路)
User(id2, deptId1, name小明, remark好好学习天天向上)SQL语句:
SELECT id, dept_id, name, remark FROM student WHERE ID IN (1, 2);
**/
根据条件查询单条Test
public void selectOne() {QueryWrapperUser queryWrapper new QueryWrapper();queryWrapper.eq(name,小菜);User user userMapper.selectOne(queryWrapper);System.out.println(user);
}
/**OUTPUT:
User(id1, deptId1, name小菜, remark关注小菜不迷路)SQL语句:SELECT id, name, dept_id, remark FROM student WHERE (name 小菜);
**/
根据条件查询多条通过 map 传递参数不是通过 LIKE 查询而是通过 查询Test
public void selectByMap() {HashMapString, Object columnMap new HashMap();columnMap.put(name,小);ListUser users userMapper.selectByMap(columnMap);users.forEach(System.out::println);
}
/**OUTPUT:
nullSQL语句:
SELECT id, name, dept_id, remark FROM student WHERE name 小;
**/
如果我们没有新建实体类进行结果封装我们还可以用 Map 来接收结果集Test
public void selectMaps() {QueryWrapperUser queryWrapper new QueryWrapper();queryWrapper.like(name,小);ListMapString, Object maps userMapper.selectMaps(queryWrapper);maps.forEach(System.out::println);
}
/**OUTPUT:
{name小菜, remark关注小菜不迷路, id1, dept_id1}
{name小明, remark好好学习天天向上, id2, dept_id1}SQL语句
SELECT id, name, dept_id, remark FROM student WHERE (name LIKE %小%);
**/
也可以用 Object 对象来接收结果集Test
public void selectObjs() {QueryWrapperUser queryWrapper new QueryWrapper();queryWrapper.like(name, 小);ListObject objects userMapper.selectObjs(queryWrapper);
}
/**OUTPUT:
{name小菜, remark关注小菜不迷路, id1, dept_id1}
{name小明, remark好好学习天天向上, id2, dept_id1}SQL语句
SELECT id, name, dept_id, remark FROM student WHERE (name LIKE %小%);
**/
分页查询Test
public void selectPage() {QueryWrapperUser queryWrapper new QueryWrapper();queryWrapper.like(name, 小);PageUser page new Page(1, 1);IPageUser userIPage userMapper.selectPage(page, queryWrapper);System.out.println(数据总数: userIPage.getTotal());System.out.println(总页数: userIPage.getPages());System.out.println(当前页: userIPage.getCurrent());System.out.println(页大小: userIPage.getSize());userIPage.getRecords().forEach(System.out::println);
}
/**OUTPUT:
数据总数:2
总页数:2
当前页:1
页大小:1
User(id1, deptId1, name小菜, remark关注小菜不迷路)SQL语句SELECT id, name, dept_id, remarkFROM studentWHERE (name LIKE %小%)LIMIT 0,1;
**/
3. 条件构造器在 CRUD 的基本操作中我们想要通过条件查询都是通过 Wrapper 类进行封装的上面只是简单的用到 eq 和 like 操作。事实上这个类十分强大我们在下面会详细进行介绍。1allEq全部 eq 或个别 isNullallEq(MapR, V params)
allEq(MapR, V params, boolean null2IsNull)
allEq(boolean condition, MapR, V params, boolean null2IsNull)allEq(BiPredicateR, V filter, MapR, V params)
allEq(BiPredicateR, V filter, MapR, V params, boolean null2IsNull)
allEq(boolean condition, BiPredicateR, V filter, MapR, V params, boolean null2IsNull)
参数说明param key 为数据库字段名value 为字段值**nullsIsNull**为 true 则在 map 的 value 为 null 时调用 isNull 方法为 false 时则忽略 value 为 null 时不调用 isNull 方法filter 过滤函数判断是否允许字段传入比对条件中使用示例allEq(MapR, V params)Test
public void testAllEq() {QueryWrapperUser queryWrapper new QueryWrapper();MapString,Object params new HashMap();params.put(name,小菜);params.put(dept_id,1);params.put(remark,null);queryWrapper.allEq(params); //会调用 isNull 方法userMapper.selectList(queryWrapper);
}
/** 结果:
SQL语句:SELECT id,name,dept_id,remarkFROM studentWHERE (name 小菜 AND dept_id 1 AND remark IS NULL);**/
allEq(MapR, V params, boolean null2IsNull)Test
public void testAllEq() {QueryWrapperUser queryWrapper new QueryWrapper();MapString,Object params new HashMap();params.put(name,小菜);params.put(dept_id,1);params.put(remark,null);queryWrapper.allEq(params, false); //不会调用 isNull 方法userMapper.selectList(queryWrapper);
}
/** 结果:
User(id1, deptId1, name小菜, remark关注小菜不迷路)SQL语句:SELECT id,name,dept_id,remarkFROM studentWHERE (name 小菜 AND dept_id 1);**/
allEq(boolean condition, MapR, V params, boolean null2IsNull)Test
public void testAllEq() {QueryWrapperUser queryWrapper new QueryWrapper();MapString,Object params new HashMap();params.put(name,小菜);params.put(dept_id,1);params.put(remark,null);queryWrapper.allEq(false,params,false); //不会带入条件进行查询userMapper.selectList(queryWrapper);
}
/** 结果:
{name小菜, remark关注小菜不迷路, id1, dept_id1}
{name小明, remark好好学习天天向上, id2, dept_id1}SQL语句:SELECT id,name,dept_id,remarkFROM student;**/
allEq(BiPredicateR, V filter, MapR, V params)Test
public void testAllEq() {QueryWrapperUser queryWrapper new QueryWrapper();MapString, Object params new HashMap();params.put(name, 小菜);params.put(dept_id, 1);params.put(remark, null);//只有 key 中含有 “m” 才会用作条件判断queryWrapper.allEq((k, v) - (k.contains(m)), params);userMapper.selectList(queryWrapper);
}
/** 结果:
0SQL语句:SELECT id,name,dept_id,remarkFROM studentWHERE (name 小菜 AND remark IS NULL);**/
2比较操作eq 相当于 ne 相当于 !gt 相当于 ge 相当于lt 相当于 le 相当于between 相当于between ... and ...notBetween 相当于not between ... and ...in 相当于in(.., .., ..)notIn 相当于not in(.., .., ..)3模糊查询like like(name,小菜) -- name like %小菜%notLike notLike(name,小菜) -- name not like %小菜%likeLeft like(name,小菜) -- name like %小菜likeRight like(name,小菜) -- name like 小菜%4排序orderByorderBy(boolean condition, boolean isAsc, R... columns)
orderBy(true, true, id, name) -- order by id ASC, name ASCorderByAscorderByAsc(id,name) -- order by id ASC, name ASCorderByDescorderByDesc(id,name) -- order by id Desc, name Desc5逻辑查询or拼接主动调用 or 表示紧接着下一个方法不是用 and 连接!(不调用 or 则默认为使用 and 连接) eq(id,1).or().eq(name,老王)嵌套or(i - i.eq(name, 李白).ne(status, 活着))and嵌套and(i - i.eq(name, 李白).ne(status, 活着))6select在MP查询中默认查询所有的字段如果有需要也可以通过select方法进行指定字段如select(id, name)4. 配置讲解1基本配置configLocation用于指明 **MyBatis ** 配置文件的位置如果我们有 MyBatis 的配置文件需将配置文件的路径配置到 configLocation 中SpringBootmybatis-plus.config-location classpath:mybatis-config.xml
SpringMvcbean idsqlSessionFactory
classcom.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean
property nameconfigLocation valueclasspath:mybatis-config.xml/
/bean
mapperLocations用于指明 Mapper 所对应的 XML 的文件位置我们在 通用 CRUD 中用到的 Mapper 是直接继承 MP 提供的 BaseMapper 我们也可以自定义方法然后在 XML 文件中自定义 SQL而这时我们需要告诉 Mapper 所对应 XML 文件的位置SpringBootmybatis-plus.mapper-locations classpath*:mybatis/*.xml
SpringMVCbean idsqlSessionFactory
classcom.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean
property namemapperLocations valueclasspath*:mybatis/*.xml/
/bean
typeAliasesPackage用于 MyBatis 别名包扫描路径通过该属性可以给包中的类注册别名注册后在 Mapper 对应的 XML 文件中可以直接使用类名而不用使用全限定的类名SpringBootmybatis-plus.type-aliases-package cbuc.life.bean
SpringMVCbean idsqlSessionFactory
classcom.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean
property nametypeAliasesPackage
valuecom.baomidou.mybatisplus.samples.quickstart.entity/
/bean
2进阶配置mapUnderScoreToCamelCase是否开启自动驼峰命名规则映射这个配置的默认值是 true但是这个属性在 MyBatis 中的默认值是 false所以在我们平时的开发中都会将这个配置开启。#关闭自动驼峰映射该参数不能和mybatis-plus.config-location同时存在
mybatis-plus.configuration.map-underscore-to-camel-case false
cacheEnabled全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存默认为 true。mybatis-plus.configuration.cache-enabled false
3DB 策略配置idType全局默认主键类型设置后即可省略实体对象中的TableId(type IdType.AUTO)配置。该配置的默认值为 ID_WORKERSpringBootmybatis-plus.global-config.db-config.id-type auto
SpringMVCbean idsqlSessionFactory
classcom.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBeanproperty namedataSource refdataSource/property nameglobalConfigbean classcom.baomidou.mybatisplus.core.config.GlobalConfigproperty namedbConfigbean classcom.baomidou.mybatisplus.core.config.GlobalConfig$DbConfigproperty nameidType valueAUTO//bean/property/bean/property
/bean
tablePrefix表名前缀全局配置后可省略TableName()配置。该配置的默认值为 nullSpringBootmybatis-plus.global-config.db-config.table-prefix yq_
SpringMVCbean idsqlSessionFactory
classcom.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBeanproperty namedataSource refdataSource/property nameglobalConfigbean classcom.baomidou.mybatisplus.core.config.GlobalConfigproperty namedbConfigbean classcom.baomidou.mybatisplus.core.config.GlobalConfig$DbConfigproperty nameidType valueAUTO/property nametablePrefix valueyq_//bean/property/bean/property
/bean
5. 其他扩展1自动填充有时候我们在插入或更新数据的时候希望有些字段可以自动填充。比如我们平时数据表里面会有个 插入时间 或者 更新时间 这种字段我们会默认以当前时间填充在 MP 中我们也可以进行配置。首先我们需要借助 TableField(fill FieldFill.INSERT) 这个注解在插入时进行填充。TableField(fill FieldFill.INSERT)
private String remark;
其中自动填充的模式如下public enum FieldFill {/*** 默认不处理*/DEFAULT,/*** 插入时填充字段*/INSERT,/*** 更新时填充字段*/UPDATE,/*** 插入和更新时填充字段*/INSERT_UPDATE
}
然后我们再编写自定义的填充处理模式Component
public class MyMetaObjectHandler implements MetaObjectHandler {Overridepublic void insertFill(MetaObject metaObject) {Object remark getFieldValByName(remark, metaObject);if (null remark) {setFieldValByName(remark, 好好学习, metaObject);}}Overridepublic void updateFill(MetaObject metaObject) {//自定义更新时填充}
}
测试Test
public void testObjectHandler() {User user User.builder().deptId(1).name(小明).build();userMapper.insert(user);
}
/**SQL语句
INSERT INTO student ( name, dept_id, remark )
VALUES ( 小明, 1, 好好学习 );
**/
可以看到插入时已经自动将我们填充的字段合并进去。2逻辑删除在开发中很多时候我们删除数据并不需要真正意义上的物理删除而是使用逻辑删除这样子查询的时候需要状态条件确保被标记的数据不被查询到。MP 当然也支持这样的功能。我们需要先为 student 表添加一个字段 status 来声明数据是否被删除0 表示被删除1表示未删除然后也需要在实体类上增加这个属性TableLogic
private Integer status;
在 application.yaml 中配置mybatis-plus:global-config:db-config:logic-delete-value: 0logic-not-delete-value: 1
测试Test
public void testLogicDelete() {userMapper.deleteById(1);
}
/**SQL语句
UPDATE student SET status0
WHERE id1 AND status1;
**/
可以看出这段 SQL 并没有真正删除而是进行了逻辑删除只是更新了删除标识3通用枚举如果有性别之类的字段我们通常会用 0 和 1 来表示但是查出来我们得进行值转换这个时候我们就可以使用枚举来解决这个问题首先为 student 表添加一个 sex 字段来表示性别0 表示女性1 表示男性然后定义一个枚举类public enum SexEnum implements IEnumInteger {MAN(1, 男),WOMEN(0, 女);private int code;private String value;SexEnum(int code, String value) {this.code code;this.value value;}Overridepublic Integer getValue() {return this.code;}//注意要重写此方法不然会将值转换成 ‘MAN’而不是 ‘男’Overridepublic String toString() {return this.value;}
}
然后在实体类中添加对应属性private SexEnum sex;
在 application.yaml 中配置mybatis-plus:type-enums-package: cbuc.life.enums
测试Test
public void selectOne() {QueryWrapperUser queryWrapper new QueryWrapper();queryWrapper.eq(name, 小菜);User user userMapper.selectOne(queryWrapper);System.out.println(user);
}
/**输出结果
User(id1, deptId1, name小菜, remark关注我不迷路, status1, sex男)SQL语句SELECT id,sex,name,dept_id,remark,statusFROM studentWHERE status1 AND (name 小菜);
**/往期推荐
千万不要这样写代码9种常见的OOM场景演示2020-11-30 这8种常见的SQL错误用法你还在用吗2020-11-27 Java中竟有18种队列45张图安排2020-12-03 关注我每天陪你进步一点点