一、插入操作
添加测试类,进行功能测试:
@SpringBootTest public class CRUDTests { @Autowired private UserMapper userMapper; @Test public void testInsert(){ User user = new User(); user.setName("Helen"); user.setAge(18); user.setEmail("1941310246@qq.com"); int result = userMapper.insert(user); System.out.println("影响的行数:" + result); //影响的行数 System.out.println("id:" + user); //id自动回填 }
}
运行 添加成功
注意:数据库插入id值默认为:全局唯一id
二、数据库分库分表策略
背景
随着业务规模的不断扩大,需要选择合适的方案去应对数据规模的增长,以应对逐渐增长的访问压力和数据量。
数据库的扩展方式主要包括:
1. 业务分库
2. 主从复制
3. 数据库分表。
1、业务分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。
例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。这样的就变成了3个数据库同时承担压力,系统的吞吐量自然就提高了。
虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题,接下来我进行详细分析。
- join 操作问题
业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查询。
- 事务问题
原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。
- 成本问题
业务分库同时也带来了成本的代价,本来 1 台服务器搞定的事情,现在要 3 台,如果考虑备份,那就是 2 台变成了 6 台。
2、主从复制和读写分离
读写分离的基本原理是将数据库读写操作分散到不同的节点上。读写分离的基本实现是:
- 数据库服务器搭建主从集群,一主一从、一主多从都可以。
- 数据库主机负责读写操作,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
3、数据库分表
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。
例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。
单表数据拆分有两种方式:垂直分表和水平分表。示意图如下:
- 垂直分表:将表中某些不常用占用空间的列拆分出去
- 水平分表:水平分表适合表行数特别大的表
4.雪花算法:分布式ID生成器
雪花算法:
- 主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性。
- 整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高。
三、MP的主键策略
1、ASSIGN_ID
MyBatis-Plus默认的主键策略是:ASSIGN_ID (使用了雪花算法)
在实体字段中配置
2、AUTO 自增策略
1.需要在创建数据表的时候设置主键自增
2.实体字段中配置 @TableId(type = IdType.AUTO)
要想影响所有实体的配置,可以设置全局主键配置
在properties文件中添加
#全局设置主键生成策略 #mybatis-plus.global-config.db-config.id-type=auto
四、更新操作
注意:update时生成的sql自动是动态sql:
UPDATE user SET age=? WHERE id=?
在CRUDTests测试类中 添加testUpdateById方法
@Test public void testUpdateById(){ User user = new User(); user.setId(1427258609506459650L);//要更改的id user.setAge(28); user.setName("Annie"); int result = userMapper.updateById(user); System.out.println("影响的行数:" + result); }
运行测试成功
一、自动填充
需求描述:
项目中经常会遇到一些数据,每次都使用相同的方式填充,例如记录的
创建时间,更新时间等。
我们可以使用MyBatis Plus的自动填充功能,完成这些字段的赋值工作
1、数据库修改
在User表中添加datetime类型的新的字段 create_time、update_time
2、实体类修改
实体上增加字段并添加自动填充注解
@Data public class User { ...... @TableField(fill = FieldFill.INSERT) private Date createTime; //@TableField(fill = FieldFill.UPDATE) @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; }
3、实现元对象处理器接口
注意:不要忘记添加 @Component 注解
package com.atguigu.mybatisplus.handler; @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); this.setFieldValByName("createTime", new Date(), metaObject); this.setFieldValByName("updateTime", new Date(), metaObject); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); this.setFieldValByName("updateTime", new Date(), metaObject); } }
4、测试
运行之前的testInsert、testUpdateById添加和修改方法
结果:
- 创建时间与修改时间自动填充
五、乐观锁
1.场景
并发:当前用户和其他用户一起操作
没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题。
实现并发控制的主要手段分为乐观并发控制和悲观并发控制两种。
2.乐观锁与悲观锁
- 乐观锁:
-在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决 定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。 - 悲观锁:
- -对该数据进行加锁以防止并发
-改数据之前先锁定,再修改的方式被称之为悲观并发控制
3、模拟修改冲突
(1)数据库中增加商品表
CREATE TABLE product ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称', price INT(11) DEFAULT 0 COMMENT '价格', version INT(11) DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id) );
(2)添加数据
INSERT INTO product (id, NAME, price) VALUES (1, '冰箱', 100);
(3)创建实体类
package com.atguigu.mybatis_plus.entity; @Data public class Product { private Long id; private String name; private Integer price; private Integer version; }
(4)Mapper
package com.atguigu.mybatis_plus.mapper; @Repository public interface ProductMapper extends BaseMapper<Product> { }
(5)测试
@Autowired private ProductMapper productMapper; @Test public void testConcurrentUpdate() { //1、小李 Product p1 = productMapper.selectById(1L); System.out.println("小李取出的价格:" + p1.getPrice()); //2、小王 Product p2 = productMapper.selectById(1L); System.out.println("小王取出的价格:" + p2.getPrice()); //3、小李将价格加了50元,存入了数据库 p1.setPrice(p1.getPrice() + 50); productMapper.updateById(p1); //4、小王将商品减了30元,存入了数据库 p2.setPrice(p2.getPrice() - 30); int result = productMapper.updateById(p2); if(result == 0){//更新失败,重试 //重新获取数据 p2 = productMapper.selectById(1L); //更新 p2.setPrice(p2.getPrice() - 30); productMapper.updateById(p2); } //最后的结果 Product p3 = productMapper.selectById(1L); System.out.println("最后的结果:" + p3.getPrice()); }
原价100 同时取到值100元 小李加入50元 这时数据库数据为150 小王减去30元失败
重新获取数据为150元 这时在减去30元为120元
两人同时对数据操作 导致数据出现错误
4、解决方案
- 数据库中添加version字段
- 取出记录时,获取当前version
SELECT id,`name`,price,`version` FROM product WHERE id=1
更新时,version + 1,如果where语句中的version版本不对,则更新失败
UPDATE product SET price=price+50, `version`=`version` + 1 WHERE id=1 AND `version`=1
接下来介绍如何在Mybatis-Plus项目中,使用乐观锁:
5、乐观锁实现流程
(1)修改实体类
在实体类添加 @Version 注解
@Version private Integer version;
(2)创建配置文件
创建包config,创建文件MybatisPlusConfig.java
此时可以删除主类MybatisPlusApplication中的 @MapperScan 扫描注解
@EnableTransactionManagement//事务处理 @Configuration @MapperScan("com.atguigu.mybatis_plus.mapper") public class MybatisPlusConfig { }
(3)注册乐观锁插件
在 MybatisPlusConfig 中注册 Bean
/** * 乐观锁插件 */ @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); }
(4)测试
更改数据两次 version+2
六、查询
1、通过多个id批量查询
完成了动态sql的foreach的功能
@Test public void testSelectBatchIds(){ List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3)); users.forEach(System.out::println); }
查询结果:
2、简单的条件查询
通过map封装查询条件
注意:
map中的key对应数据库中的列名。如:数据库user_id,实体类是userId,这时map的key需要填写user_id
@Test public void testSelectByMap(){ HashMap<String, Object> map = new HashMap<>(); map.put("name", "Helen"); map.put("age", 18); List<User> users = userMapper.selectByMap(map); users.forEach(System.out::println); }
运行结果:
七、分页
1、分页插件
MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能
(1)添加分页插件
之前写的MyBatisPlusConfig配置类中添加@Bean配置
**** * 分页插件 */ @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); }
(2)测试selectPage分页
测试:最终通过page对象获取相关数据
@Test public void testSelectPage() { Page<User> page = new Page<>(1,5); Page<User> pageParam = userMapper.selectPage(page, null); pageParam.getRecords().forEach(System.out::println); System.out.println(pageParam.getCurrent()); System.out.println(pageParam.getPages()); System.out.println(pageParam.getSize()); System.out.println(pageParam.getTotal()); System.out.println(pageParam.hasNext()); System.out.println(pageParam.hasPrevious()); }
结果:这里只输出了第一页的数据
2、返回指定的列
当指定了特定的查询列时,希望分页结果列表只返回被查询的列,而不是很多null值
测试selectMapsPage分页:结果集是Map
@Test public void testSelectMapsPage() { //返回很多null列 //Page<User> page = new Page<>(1, 5); //QueryWrapper<User> queryWrapper = new QueryWrapper<>(); //queryWrapper.select("name", "age"); //Page<User> pageParam = userMapper.selectPage(page, queryWrapper); // //pageParam.getRecords().forEach(System.out::println); //Page不需要泛型 Page<Map<String, Object>> page = new Page<>(1, 5); QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("name", "age"); Page<Map<String, Object>> pageParam = userMapper.selectMapsPage(page, queryWrapper); List<Map<String, Object>> records = pageParam.getRecords(); records.forEach(System.out::println); System.out.println(pageParam.getCurrent()); System.out.println(pageParam.getPages()); System.out.println(pageParam.getSize()); System.out.println(pageParam.getTotal()); System.out.println(pageParam.hasNext()); System.out.println(pageParam.hasPrevious()); }
运行结果:输出特定查询列
七、删除
1、根据id删除记录
@Test public void testDeleteById(){ int result = userMapper.deleteById(1427270191766597638L); System.out.println(result); }
2、批量删除
@Test public void testDeleteBatchIds() { int result = userMapper.deleteBatchIds(Arrays.asList(8, 9, 10)); System.out.println(result); }
3、简单条件删除
@Test public void testDeleteByMap() { HashMap<String, Object> map = new HashMap<>(); map.put("name", "Annie"); map.put("age", 28); int result = userMapper.deleteByMap(map); System.out.println(result); }
八、逻辑删除
1、物理删除和逻辑删除
- 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除数据
- 逻辑删除:假删除,将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录
逻辑删除的使用场景:
- 可以进行数据恢复
- 有关联数据,不便删除
2、逻辑删除实现流程
(1)数据库修改
添加 deleted字段
ALTER TABLE `user` ADD COLUMN `deleted` boolean DEFAULT false
(2)实体类修改
添加deleted 字段,并加上 @TableLogic 注解
@TableLogic private Integer deleted;
(3)配置(可选)
application.properties 加入以下配置,此为默认值,如果你的默认值和mp默认的一样,该配置可无
mybatis-plus.global-config.db-config.logic-delete-value=1 mybatis-plus.global-config.db-config.logic-not-delete-value=0
application.yml
mybatis-plus: global-config: db-config: logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
(4)测试
- 测试后发现,数据并没有被删除,deleted字段的值由0变成了1
- 测试后分析打印的sql语句,是一条update
- 注意:被删除前,数据的deleted 字段的值必须是 0,才能被选取出来执行逻辑删除的操作
@Test public void testLogicDelete() { int result = userMapper.deleteById(1L); System.out.println(result); }
(5)测试逻辑删除后的查询
MyBatis Plus中查询操作也会自动添加逻辑删除字段的判断
@Test public void testLogicDeleteSelect() { List<User> users = userMapper.selectList(null); users.forEach(System.out::println); }
sql:SELECT id,name,age,email,create_time,update_time,deleted FROM user WHERE deleted=0
这里查询deleted为0的也就是未删除的数据 为1的数据为逻辑删除的数据不查询
Wapper
Wrapper即条件构造器,用来写一些复杂的sql语句。
1.eq 等于 = 、 ne不等于<> 、gt大于 > 、 ge大于等于 >= 、 lt 小于 < 、le小于等于 <=
eq 等于 =
- 例: eq(“name”, “老王”)—>name = ‘老王’
ne 不等于 <>
- 例: ne(“name”, “老王”)—>name <> ‘老王’
gt大于等于 >=
- 例: gt(“age”, 18)—>age > 18
ge 大于等于 >=
- 例: ge(“age”, 18)—>age >= 18
lt 小于 <
- 例: lt(“age”, 18)—>age < 18
le小于等于 <=
- 例: le(“age”, 18)—>age <= 18
@Test public void testSelectOne() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", "Tom"); User user = userMapper.selectOne(queryWrapper);//只能返回一条记录,多余一条则抛出异常 System.out.println(user); }
2. between 、 notBetween、 like、 notLike、 likeLeft 、likeRight
BETWEEN 值1 AND 值2
- 例: between(“age”, 18, 30)—>age between 18 and 30
NOT BETWEEN 值1 AND 值2
- 例: notBetween(“age”, 18, 30)—>age not between 18 and 30
LIKE ‘%值%’
- 例: like(“name”, “王”)—>name like ‘%王%’
NOT LIKE ‘%值%’
- 例: notLike(“name”, “王”)—>name not like ‘%王%’
likeLeft LIKE ‘%值’
- 例: likeLeft(“name”, “王”)—>name like ‘%王’
likeRight LIKE ‘值%’
- 例: likeRight(“name”, “王”)—>name like ‘王%’
3.isNull isNotNull in notIn inSql notInSql groupBy orderByAsc
字段 IS NULL
- 例: isNull(“name”)—>name is null
字段 IS NOT NULL
- 例: isNotNull(“name”)—>name is not null
字段 IN (value.get(0), value.get(1), …)
例: in(“age”,{1,2,3})—>age in (1,2,3)
字段 NOT IN (value.get(0), value.get(1), …)
- 例: notIn(“age”,{1,2,3})—>age not in (1,2,3)
字段 NOT IN (v0, v1, …)
- 例: notIn(“age”, 1, 2, 3)—>age not in (1,2,3)
字段 IN ( sql语句 )
例: inSql(“age”, “1,2,3,4,5,6”)—>age in (1,2,3,4,5,6) 例: inSql(“id”,
“select id from table where id < 3”)—>id in (select id from table
where id < 3)
字段 NOT IN ( sql语句 )
例: notInSql(“age”, “1,2,3,4,5,6”)—>age not in (1,2,3,4,5,6) 例:
notInSql(“id”, “select id from table where id < 3”)—>id not in
(select id from table where id < 3)
分组:GROUP BY 字段, …
- 例: groupBy(“id”, “name”)—>group by id,name
排序:ORDER BY 字段, … ASC
- 例: orderByAsc(“id”, “name”)—>order by id ASC,name ASC
排序:ORDER BY 字段, … DESC
- 例: orderByDesc(“id”, “name”)—>order by id DESC,name DESC
排序:ORDER BY 字段, …
- 例: orderBy(true, true, “id”, “name”)—>order by id ASC,name ASC
HAVING ( sql语句 )
- 例: having(“sum(age) > 10”)—>having sum(age) > 10 例:
having(“sum(age) > {0}”, 11)—>having sum(age) > 11
拼接 OR
注意事项:
主动调用or表示紧接着下一个方法不是用and连接!(不调用or则默认为使用and连接)
- 例: eq(“id”,1).or().eq(“name”,“老王”)—>id = 1 or name = ‘老王’
AND 嵌套
- 例: and(i -> i.eq(“name”, “李白”).ne(“status”, “活着”))—>and (name =
‘李白’ and status <> ‘活着’)
链式调用 lambda 式
// 区分: // 链式调用 普通 UpdateChainWrapper<T> update(); // 链式调用 lambda 式。注意:不支持 Kotlin LambdaUpdateChainWrapper<T> lambdaUpdate(); // 等价示例: query().eq("id", value).one(); lambdaQuery().eq(Entity::getId, value).one(); // 等价示例: update().eq("id", value).remove(); lambdaUpdate().eq(Entity::getId, value).remove();
总结
之前看的尚硅谷的在线教育项目学习视频 记录顺便复习