文章目录
一、插入操作
添加测试类,进行功能测试:
@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();
总结
之前看的尚硅谷的在线教育项目学习视频 记录顺便复习