【Mybatis Plus】
引入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.13</version> </dependency> </dependencies>
配置application.yml
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://192.168.60.100/mybatisplus?characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mysql 5.0以上版本驱动需要配置为 com.mysql.cj.jdbc.Driver
mysql 5.7以上url配置:jdbc:m ysql://192.168.60.100/mydb?characterEncoding=UTF-8&useSSL=false
mysql 8.0 url需要配置时区:jdbc:m ysql://192.168.60.100/mydb?characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl为mybatisplus自带的日志,可以在控制台输出sql和执行结果
1 BaseMapper
public interface BaseMapper<T> extends Mapper<T> { int insert(T entity); int deleteById(Serializable id); int deleteByMap(@Param("cm") Map<String, Object> columnMap); int delete(@Param("ew") Wrapper<T> wrapper); int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList); int updateById(@Param("et") T entity); int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper); T selectById(Serializable id); List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList); List<T> selectByMap(@Param("cm") Map<String, Object> columnMap); T selectOne(@Param("ew") Wrapper<T> queryWrapper); Integer selectCount(@Param("ew") Wrapper<T> queryWrapper); List<T> selectList(@Param("ew") Wrapper<T> queryWrapper); List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper); List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper); <E extends IPage<T>> E selectPage(E page, @Param("ew") Wrapper<T> queryWrapper); <E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param("ew") Wrapper<T> queryWrapper); }
1.1 创建继承BaseMapper的实现类
@Mapper public interface UserMapper extends BaseMapper<User> { }
1.2 在启动类添加@MapperScan扫描mapper
@SpringBootApplication @MapperScan public class MybatisplusApplication { public static void main(String[] args) { SpringApplication.run(MybatisplusApplication.class, args); } }
1.3 创建实体类
@Data @AllArgsConstructor @NoArgsConstructor @TableName("t_user") public class User { private Long id; private String name; private String email; private Integer age; }
如果不使用t_user则对应表名为类名首字母小写 即user
1.4 测试
@SpringBootTest class MybatisplusApplicationTests { @Autowired UserMapper userMapper; @Test void contextLoads() { List<User> users = userMapper.selectList(null); users.forEach(System.out::println); } }
1.5 新增 insert
User user = new User(); user.setName("ryuu"); user.setAge(25); user.setEmail("123456789@qq.com"); userMapper.insert(user); log.info("id" + user.getId());
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5a82ebf8] will be managed by Spring ==> Preparing: INSERT INTO t_user ( id, name, email, age ) VALUES ( ?, ?, ?, ? ) ==> Parameters: 1582699157263532033(Long), ryuu(String), 123456789@qq.com(String), 25(Integer) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@f096f37] 2022-10-19 19:44:18.924 INFO 20208 --- [ main] com.hikaru.MybatisplusApplicationTests : id1582699157263532033
mybatisplus的新增是能够获取到自增长的主键的:: id 15826991572635
这个值是由雪花算法生成的
这里还测试出了个有意思的地方,在测试类上开启声明式事务@Transactional测试结束之后会自动回滚。。想了半天数据库为啥没有新增,然后看到控制台打印如下:
Rolled back transaction for test: [DefaultTestContext@550dbc7a testClass = MybatisplusApplicationTests, testInstance = com.hikaru.MybatisplusApplicationTests@4f80542f, testMethod = contextLoads@MybatisplusApplicationTests, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@21282ed8 testClass = MybatisplusApplicationTests, locations = '{}', classes = '{class com.hikaru.MybatisplusApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@6ac13091, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@75329a49, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@1534f01b, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@2473d930, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@7382f612], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map[[empty]]]
1.6 删除
① deleteById
userMapper.deleteById(1582701639167422466L);
② deleteByMap
Map<String, Object> map = new HashMap<>(); map.put("name", "zhangsan"); map.put("age", 23); userMapper.deleteByMap(map);
deleteByMap会按照map进行删除,其中map存放的是删除的条件的键值对,条件之间为且的关系
==> Preparing: DELETE FROM t_user WHERE name = ? AND age = ? ==> Parameters: zhangsan(String), 23(Integer) <== Updates: 0
③ deleteBatchIds 批量删除
@Test void contextLoads() { List<Long> idList = Arrays.asList(1L, 2L, 3L); userMapper.deleteBatchIds(idList); }
deleteBatchIds采用的是in的批量删除
==> Preparing: DELETE FROM t_user WHERE id IN ( ? , ? , ? ) ==> Parameters: 1(Long), 2(Long), 3(Long) <== Updates: 0
1.7 修改
updateById(Entity)
@Test void contextLoads() { User user = new User(); //user.setId(1L); user.setEmail("5462316489@qq.com"); user.setName("zhangsan"); userMapper.updateById(user); }
updateById会根据参数实体的id去按照条件修改
==> Preparing: UPDATE t_user SET name=?, email=? WHERE id=? ==> Parameters: zhangsan(String), 5462316489@qq.com(String), null <== Updates: 0
可以看到这里没有设置age字段就不会出现在sql语句的条件里面,
没有设置id则id为null
1.8 查询
① selectById
② selectBatchIds
③ selectByMaps
④ selectList()
条件查询,如果选择器为null则查询所有数据
1.9 自定义查询
① 在application.yml配置mapper.xml文件位置
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://192.168.60.100/mybatisplus?characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath*:/mapper/**/*.xml
这里mapper-locations的值即为默认值
② 创建UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.hikaru.mapper.UserMapper"> <select id="selectMapById" resultType="map"> select * from t_user where id = #{id} </select> </mapper>
③ 创建UserMapper接口的方法
@Mapper public interface UserMapper extends BaseMapper<User> { public Map<String, Object> selectMapById(@Param("id") long id); }
这里可能出现@MapKey is required 的错误警告,无视就好,mybatisplus会自动将字段名和值对应key-value
2 IService
2.1 创建service
public interface UserService extends IService<User> { }
2.2 创建ServiceImpl
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }
注意泛型对应
2.3 查询总记录数 count
@Autowired UserService userService; @Test void contextLoads() { userService.count(); }
2.6 批量添加
@Test void contextLoads() { List<User> list = new ArrayList<>(); for(int i = 0; i < 10; i++) { User user = new User(); user.setName("ryuu" + i); user.setAge(20 + i); user.setEmail("123123213@qq.com"); list.add(user); } boolean b = userService.saveBatch(list); log.info(Boolean.toString(b)); }
因为mapper的insert只有单个添加,这是因为批量添加会导致sql语句过长,这也是有service层的原因,而上面service的本质也是多次调用了mapper:
==> Preparing: INSERT INTO t_user ( id, name, email, age ) VALUES ( ?, ?, ?, ? ) ==> Parameters: 1582722877105381377(Long), ryuu0(String), 123123213@qq.com(String), 20(Integer) ==> Parameters: 1582722877289930754(Long), ryuu1(String), 123123213@qq.com(String), 21(Integer) ==> Parameters: 1582722877289930755(Long), ryuu2(String), 123123213@qq.com(String), 22(Integer) ==> Parameters: 1582722877289930756(Long), ryuu3(String), 123123213@qq.com(String), 23(Integer) ==> Parameters: 1582722877289930757(Long), ryuu4(String), 123123213@qq.com(String), 24(Integer) ==> Parameters: 1582722877348651010(Long), ryuu5(String), 123123213@qq.com(String), 25(Integer) ==> Parameters: 1582722877348651011(Long), ryuu6(String), 123123213@qq.com(String), 26(Integer) ==> Parameters: 1582722877348651012(Long), ryuu7(String), 123123213@qq.com(String), 27(Integer) ==> Parameters: 1582722877348651013(Long), ryuu8(String), 123123213@qq.com(String), 28(Integer) ==> Parameters: 1582722877348651014(Long), ryuu9(String), 123123213@qq.com(String), 29(Integer)
3 MybatisPlus常用注解
3.1 @TableName
在实体类上添加用于指定实体类对应的数据库表名,不添加则默认为首字母小写的类名
@Data @AllArgsConstructor @NoArgsConstructor @TableName("t_user") public class User { private Long id; private String name; private String email; private Integer age; }
3.2 @TableId
在实体类属性上添加@TableId将属性对应的字段指定为主键,如果不添加则默认主键为id并且会使用雪花算法进行生成。即当表的主键不是id则需要使用该注解。
属性对应的字段作为主键,是因为mp的框架结构需要先扫描实体类,然后抽取属性对字段进行操作的
而注解的value属性则适用于实体类属性名和字段名不同的情况,不写则与属性相同。
@Data @AllArgsConstructor @NoArgsConstructor @TableName("t_user") public class User { @TableId("uid") private Long id; private String name; private String email; private Integer age; }
TableId的type属性
type属性用于指定主键的生成策略,默认为雪花算法,即使数据库使用了自动递增也是雪花算法。 而如果想要使用自动递增,还需要设置type:
@Data @AllArgsConstructor @NoArgsConstructor @TableName("t_user") public class User { @TableId(value = "uid", type = IdType.AUTO) private Long id; private String name; private String email; private Integer age; }
其中IdType为一个枚举类,常用的主键生成策略主要有两种:
AUTO(0):自动递增
ASSIGN_ID(3):雪花算法(id为空的时候默认的策略)
public enum IdType { AUTO(0), NONE(1), INPUT(2), ASSIGN_ID(3), ASSIGN_UUID(4), /** @deprecated */ @Deprecated ID_WORKER(3), /** @deprecated */ @Deprecated ID_WORKER_STR(3), /** @deprecated */ @Deprecated UUID(4); private final int key; private IdType(int key) { this.key = key; } public int getKey() { return this.key; } }
通过全局配置主键生成策略和表前缀
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto table-prefix: t_
4 雪花算法
数据库扩展的方式主要包括:
业务分库
:
主从复制
:即在保证数据一致的前提下,主服务器和从服务器实现表的读写分离。
数据库分表
:分为水平和垂直分表。
雪花算法是Twitter公布的一种分布式主键生成算法,它能够保证不同表的主键的不重复性和相同表的主键的有序性。
5 MBP常用注解
5.1 @TableFiled 设置属性对应普通字段名
MBP默认的规则是下划线字段名对应实体的驼峰写法,除此之外不一致的情况需要使用@TableFiled:
@Data @AllArgsConstructor @NoArgsConstructor @TableName("t_user") public class User { @TableId(value = "uid") private Long id; @TableField("user_name") private String name; private String email; private Integer age; }
@TableField(exist = false)
注解表明该属性不是数据库表的字段
5.2 @TableLogic 逻辑删除
-
物理删除:真实删除,将数据从数据库表中删除,之后查询不到此条被删除的数据
-
逻辑删除:假删除,只是将数据库中该数据设置为删除状态,之后仍然能从数据库中看到
-
使用场景:数据恢复
① 首先在逻辑删除属性上添加注解
@Data @AllArgsConstructor @NoArgsConstructor @TableName("t_user") public class User { @TableId(value = "uid") private Long id; @TableField("user_name") private String name; private String email; private Integer age; @TableLogic private Integer isDelete; }
② 进行删除操作
@Autowired UserService userService; @Test void contextLoads() { userService.removeById(1); }
③ 观察这时候的sql语句,删除被替换为了更新:
==> Preparing: UPDATE t_user SET is_delete=1 WHERE uid=? AND is_delete=0 ==> Parameters: 1(Integer) <== Updates: 0
如果进行数据恢复,只需要将isDelete恢复为0即可
6 条件构造器 wrapper
-
Wrapper:条件构造器的抽象类,最顶端的父类
-
AbstractWrapper:用于查询条件封装,生成sql的where条件
-
QueryWrapper:查询条件封装
-
UpdateWrapper:Update条件封装
-
AbstractLambdaWrapper:使用Lambda语法
-
LambdaQueryWrapper:Lambda查询封装
-
LambdaUpdateWrapper:Lambda update封装
-
-
-
updateWrapper不但需要封装修改的条件还需要封装修改的字段,而删除是不需要提供字段只需要条件,因此查询和删除都使用QueryMapper做条件封装。
6.1 组装查询条件
@Test void contextLoads() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.like("user_name", 2) .between("age", 20, 25) .isNotNull("email"); System.out.println(userService.list(queryWrapper)); }
对应的SQL
==> Preparing: SELECT uid AS id,user_name AS name,email,age,is_delete FROM t_user WHERE is_delete=0 AND (user_name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) ==> Parameters: %2%(String), 20(Integer), 25(Integer) <== Columns: id, name, email, age, is_delete <== Row: 3, ryuu2, 123123213@qq.com, 22, 0 <== Row: 13, ryuu2, 123123213@qq.com, 22, 0 <== Total: 2
6.2 组装排序条件
@Test void contextLoads() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.orderByDesc("age").orderByAsc("uid"); System.out.println(userService.list(queryWrapper)); }
对应SQL
==> Preparing: SEECT uid AS id,user_name AS name,email,age,is_delete FROM t_user WHERE is_delete=0 ORDER BY age DESC,uid ASC
6.3 组装删除条件
@Test void contextLoads() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.isNull("email"); System.out.println(userMapper.delete(queryWrapper)); }
对应sql
==> Preparing: UPDATE t_user SET is_delete=1 WHERE is_delete=0 AND (email IS NULL) ==> Parameters: <== Updates: 1
6.4 组装修改功能
① 使用queryWrapper实现修改
@Test void contextLoads() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.like("uid", 1) .between("age", 20, 25) .isNotNull("email"); User user = new User(); user.setName("小明"); user.setEmail("xiaoming@163.com"); userMapper.update(user, queryWrapper); }
对应SQL
==> Preparing: UPDATE t_user SET user_name=?, email=? WHERE is_delete=0 AND (uid LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) ==> Parameters: 小明(String), xiaoming@163.com(String), %1%(String), 20(Integer), 25(Integer)
② 使用UpdateWrapper实现修改
@Test void contextLoads() { // 将用户名中含有a 并且 (年龄大于20 或者 邮箱为null)的用户信息进行修改 UpdateWrapper<User> updateWrapper = new UpdateWrapper<>(); updateWrapper.like("user_name", "a") .and(i -> i.gt("age", 20) .or() .isNotNull("email") ); updateWrapper.set("user_name", "小黑"); int update = userMapper.update(null, updateWrapper); log.info(String.valueOf(update)); }
对应SQL
==> Preparing: UPDATE t_user SET user_name=? WHERE is_delete=0 AND (user_name LIKE ? AND (age > ? OR email IS NOT NULL)) ==> Parameters: 小黑(String), %a%(String), 20(Integer)
6.5 组装select语句 实现查询指定字段
@Test void contextLoads() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("user_name", "age"); List<Map<String, Object>> list = userMapper.selectMaps(queryWrapper); System.out.println(list); }
注意需要使用selectMaps接收
6.6 组装子查询
@Test void contextLoads() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.inSql("uid", "select uid from t_user where age > 25"); List<User> users = userMapper.selectList(queryWrapper); System.out.println(users); }
对应SQL
==> Preparing: SELECT uid AS id,user_name AS name,email,age,is_delete FROM t_user WHERE is_delete=0 AND (uid IN (select uid from t_user where age > 25)) ==> Parameters: <== Columns: id, name, email, age, is_delete
6.7 模拟开发中的条件组装
@Test void contextLoads() { UpdateWrapper<User> updateWrapper = new UpdateWrapper<>(); String userName = "r"; Integer ageBegin =20; Integer ageEnd =25; if(StringUtils.isBlank(userName)) { updateWrapper.like("user_name", "a"); } if(ageBegin != null) { updateWrapper.gt("age", ageBegin); } if(ageEnd != null) { updateWrapper.lt("age", ageEnd); } updateWrapper.set("user_name", "小黑"); int update = userMapper.update(null, updateWrapper); log.info(String.valueOf(update)); }
StringUtils.isBlank 判断字符串
① 不为空串
② 不为null
③ 不为空白符
==> Preparing: UPDATE t_user SET user_name=? WHERE is_delete=0 AND (age > ? AND age < ?) ==> Parameters: 小黑(String), 20(Integer), 25(Integer) <== Updates: 8
6.8 使用condition进行条件判断
上面的写法需要用户传入条件后对每个条件进行编写判断,变得十分复杂
7 Lambda表达式优先执行实现条件优先级
考虑下面两条sql的条件优先级:
// 将用户名包含r 并且 年龄大于20 或者 邮箱为null的用户信息进行修改
// 将用户名包含r 并且 (年龄大于20 或者 邮箱为null)的用户信息进行修改
@Test void contextLoads() { // 将用户名包含r 并且 年龄大于20 或者 邮箱为null的用户信息进行修改 // 将用户名包含r 并且 (年龄大于20 或者 邮箱为null)的用户信息进行修改 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.like("user_name", "r") .and(i -> i.gt("age", 20) .or() .isNull("email") ); User user = new User(); user.setName("小Li"); user.setEmail("xiaoLi@163.com"); userMapper.update(user, queryWrapper); }
利用Lambda表达式优先执行实现条件的优先级
对应SQL
==> Preparing: UPDATE t_user SET user_name=?, email=? WHERE is_delete=0 AND (user_name LIKE ? AND (age > ? OR email IS NULL)) ==> Parameters: 小Li(String), xiaoLi@163.com(String), %r%(String), 20(Integer) <== Updates: 13
8 LambdaQueryWraper
上面的代码中充斥着大量的字段名魔法值,为了防止字段写错,使用Lambda表达式来获取属性对应的字段名
@Test public void lambadaQueryMapperTest() { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); String userName = "8"; queryWrapper.like(StringUtils.isNotBlank(userName), User::getName, userName) .ge(User::getAge, 20) .le(User::getAge, 21); List<User> users = userMapper.selectList(queryWrapper); System.out.println(users); }
对应SQL
==> Preparing: SELECT uid AS id,user_name AS name,email,age,is_delete FROM t_user WHERE is_delete=0 AND (user_name LIKE ? AND age >= ? AND age <= ?) ==> Parameters: %8%(String), 20(Integer), 21(Integer) <== Total: 0
9 LambdaUpdateWraper
@Test void contextLoads() { LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>(); String userName = "r"; Integer ageBegin =20; Integer ageEnd =25; updateWrapper.like(StringUtils.isNotBlank(userName), User::getName, userName) .gt(ageBegin!=null, User::getAge, ageBegin) .lt(ageEnd!=null, User::getAge, ageEnd); updateWrapper.set(User::getName, "小黑"); int update = userMapper.update(null, updateWrapper); log.info(String.valueOf(update)); }
对应SQL
==> Preparing: UPDATE t_user SET user_name=? WHERE is_delete=0 AND (user_name LIKE ? AND age > ? AND age < ?) ==> Parameters: 小黑(String), %r%(String), 20(Integer), 25(Integer) <== Updates: 0
10 MybatisPlus插件
Mybatis自带分页插件,只需要简单的配置就可实现分页功能
配置插件
@Configuration @MapperScan("com.hikaru.mapper.UserMapper") public class MybatisConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
插件使用
@Test public void testPage() { Page<User> page = new Page<>(2, 3); userMapper.selectPage(page, null); System.out.println(page.getRecords()); }
对应SQL:
Preparing: SELECT uid AS id,user_name AS name,email,age,is_delete FROM t_user WHERE is_delete=0 LIMIT ?,?
自定义分页功能
① 在mapper.xml中编写SQL
<mapper namespace="com.hikaru.mapper.UserMapper"> <select id="selectMapById" resultType="map"> select * from t_user where id = #{id} </select> <select id="selectUserVOPage" resultType="User"> select * from t_user where age > #{age} </select> </mapper>
这里能够直接在resultType中写User是因为使用了类型别名(为不区分大小写的类名),类型别名的包在配置文件中配置:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: table-prefix: t_ id-type: auto type-aliases-package: com.hikaru.entity
② 在Mapper接口中定义对应的方法
Page<User> selectUserVOPage(@Param("page") Page<User> page, @Param("age") int age);
注意这里第一个参数必须为对应泛型型的page,返回值类型也必须为对应泛型型的Page
乐观锁与悲观锁
剩下的内容等以后再看吧。。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步