点击关注公众号,Java干货及时送达
作者:yogurtzzz
juejin.cn/post/6961721367846715428
MyBatis-plus 是一款 Mybatis 增强工具,用于简化开发,提高效率。下文使用缩写 mp来简化表示 MyBatis-plus,本文主要介绍 mp 搭配 Spring Boot 的使用。
注:本文使用的 mp 版本是当前最新的3.4.2,早期版本的差异请自行查阅文档
官方网站:baomidou.com/
1.创建一个Spring Boot项目。
2.导入依赖
4.0.0 org.springframework.boot spring-boot-starter-parent 2.3.4.RELEASE com.example mybatis-plus 0.0.1-SNAPSHOT mybatis-plus 1.8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-configuration-processor com.baomidou mybatis-plus-boot-starter 3.4.2 mysql mysql-connector-java runtime org.projectlombok lombok org.springframework.boot spring-boot-maven-plugin
3.配置数据库
# application.yml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai username: root password: root mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启SQL语句打印
4.创建一个实体类
package com.example.mp.po; import lombok.Data; import java.time.LocalDateTime; @Data public class User { private Long id; private String name; private Integer age; private String email; private Long managerId; private LocalDateTime createTime; }
5.创建一个mapper接口
package com.example.mp.mappers; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User; public interface UserMapper extends BaseMapper { }
6.在SpringBoot启动类上配置mapper接口的扫描路径
package com.example.mp; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.example.mp.mappers") public class MybatisPlusApplication { public static void main(String[] args) { SpringApplication.run(MybatisPlusApplication.class, args); } }
7.在数据库中创建表
DROP TABLE IF EXISTS user; CREATE TABLE user ( id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键', name VARCHAR(30) DEFAULT NULL COMMENT '姓名', age INT(11) DEFAULT NULL COMMENT '年龄', email VARCHAR(50) DEFAULT NULL COMMENT '邮箱', manager_id BIGINT(20) DEFAULT NULL COMMENT '直属上级id', create_time DATETIME DEFAULT NULL COMMENT '创建时间', CONSTRAINT manager_fk FOREIGN KEY(manager_id) REFERENCES user (id) ) ENGINE=INNODB CHARSET=UTF8; INSERT INTO user (id, name, age ,email, manager_id, create_time) VALUES (1, '大BOSS', 40, 'boss@baomidou.com', NULL, '2021-03-22 09:48:00'), (2, '李经理', 40, 'boss@baomidou.com', 1, '2021-01-22 09:48:00'), (3, '黄主管', 40, 'boss@baomidou.com', 2, '2021-01-22 09:48:00'), (4, '吴组长', 40, 'boss@baomidou.com', 2, '2021-02-22 09:48:00'), (5, '小菜', 40, 'boss@baomidou.com', 2, '2021-02-22 09:48:00')
8.编写一个SpringBoot测试类
package com.example.mp; import com.example.mp.mappers.UserMapper; import com.example.mp.po.User; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest public class SampleTest { @Autowired private UserMapper mapper; @Test public void testSelect() { List list = mapper.selectList(null); assertEquals(5, list.size()); list.forEach(System.out::println); } }
准备工作完成,数据库情况如下:
项目目录如下:
运行测试类
可以看到,针对单表的基本CRUD操作,只需要创建好实体类,并创建一个继承自BaseMapper的接口即可,可谓非常简洁。并且,我们注意到,User类中的managerId,createTime属性,自动和数据库表中的manager_id,create_time对应了起来,这是因为mp自动做了数据库下划线命名,到Java类的驼峰命名之间的转化。
BaseMapper
User
managerId
createTime
manager_id
create_time
mp一共提供了8个注解,这些注解是用在Java的实体类上面的。
@TableName
注解在类上,指定类和数据库表的映射关系。实体类的类名(转成小写后)和数据库表名相同时,可以不指定该注解。
@TableId
注解在实体类的某一字段上,表示这个字段对应数据库表的主键。当主键名为id时(表中列名为id,实体类中字段名为id),无需使用该注解显式指定主键,mp会自动关联。若类的字段名和表的列名不一致,可用value属性指定表的列名。另,这个注解有个重要的属性type,用于指定主键策略。
value
type
@TableField
注解在某一字段上,指定Java实体类的字段和数据库表的列的映射关系。这个注解有如下几个应用场景。
排除非表字段
若Java实体类中某个字段,不对应表中的任何列,它只是用于保存一些额外的,或组装后的数据,则可以设置exist属性为false,这样在对实体对象进行插入时,会忽略这个字段。排除非表字段也可以通过其他方式完成,如使用static或transient关键字,但个人觉得不是很合理,不做赘述
exist
false
static
transient
字段验证策略
通过insertStrategy,updateStrategy,whereStrategy属性进行配置,可以控制在实体对象进行插入,更新,或作为WHERE条件时,对象中的字段要如何组装到SQL语句中。
insertStrategy
updateStrategy
whereStrategy
字段填充策略
通过fill属性指定,字段为空时会进行自动填充
fill
@Version
乐观锁注解
@EnumValue
注解在枚举字段上
@TableLogic
逻辑删除
KeySequence
序列主键策略(oracle)
oracle
InterceptorIgnore
插件过滤规则
mp封装了一些最基础的CRUD方法,只需要直接继承mp提供的接口,无需编写任何SQL,即可食用。mp提供了两套接口,分别是Mapper CRUD接口和Service CRUD接口。并且mp还提供了条件构造器Wrapper,可以方便地组装SQL语句中的WHERE条件。
Wrapper
只需定义好实体类,然后创建一个接口,继承mp提供的BaseMapper,即可食用。mp会在mybatis启动时,自动解析实体类和表的映射关系,并注入带有通用CRUD方法的mapper。BaseMapper里提供的方法,部分列举如下:
insert(T entity) 插入一条记录
insert(T entity)
deleteById(Serializable id) 根据主键id删除一条记录
deleteById(Serializable id)
delete(Wrapper wrapper) 根据条件构造器wrapper进行删除
delete(Wrapper wrapper)
selectById(Serializable id) 根据主键id进行查找
selectById(Serializable id)
selectBatchIds(Collection idList) 根据主键id进行批量查找
selectBatchIds(Collection idList)
selectByMap(Map map) 根据map中指定的列名和列值进行等值匹配查找
selectByMap(Map map)
selectMaps(Wrapper wrapper) 根据 wrapper 条件,查询记录,将查询结果封装为一个Map,Map的key为结果的列,value为值
selectMaps(Wrapper wrapper)
selectList(Wrapper wrapper) 根据条件构造器wrapper进行查询
selectList(Wrapper wrapper)
wrapper
update(T entity, Wrapper wrapper) 根据条件构造器wrapper进行更新
update(T entity, Wrapper wrapper)
updateById(T entity)
...
下面讲解几个比较特别的方法
BaseMapper接口还提供了一个selectMaps方法,这个方法会将查询结果封装为一个Map,Map的key为结果的列,value为值
selectMaps
该方法的使用场景如下:
只查部分列
当某个表的列特别多,而SELECT的时候只需要选取个别列,查询出的结果也没必要封装成Java实体类对象时(只查部分列时,封装成实体后,实体对象中的很多属性会是null),则可以用selectMaps,获取到指定的列后,再自行进行处理即可
比如
@Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.select("id","name","email").likeRight("name","黄"); List> maps = userMapper.selectMaps(wrapper); maps.forEach(System.out::println); }
进行数据统计
// 按照直属上级进行分组,查询每组的平均年龄,最大年龄,最小年龄 /** select avg(age) avg_age ,min(age) min_age, max(age) max_age from user group by manager_id having sum(age) < 500; **/ @Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.select("manager_id", "avg(age) avg_age", "min(age) min_age", "max(age) max_age") .groupBy("manager_id").having("sum(age) < {0}", 500); List> maps = userMapper.selectMaps(wrapper); maps.forEach(System.out::println); }
只会返回第一个字段(第一列)的值,其他字段会被舍弃
@Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.select("id", "name").like("name", "黄"); List objects = userMapper.selectObjs(wrapper); objects.forEach(System.out::println); } 得到的结果,只封装了第一列的id selectCount 查询满足条件的总数,注意,使用这个方法,不能调用QueryWrapper的select方法设置要查询的列了。这个方法会自动添加select count(1) 比如 @Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like("name", "黄"); Integer count = userMapper.selectCount(wrapper); System.out.println(count); } 图片 Service CRUD 接口 另外一套CRUD是Service层的,只需要编写一个接口,继承IService,并创建一个接口实现类,即可食用。(这个接口提供的CRUD方法,和Mapper接口提供的功能大同小异,比较明显的区别在于IService支持了更多的批量化操作,如saveBatch,saveOrUpdateBatch等方法。 食用示例如下 1.首先,新建一个接口,继承IService package com.example.mp.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.mp.po.User; public interface UserService extends IService { } 2.创建这个接口的实现类,并继承ServiceImpl,最后打上@Service注解,注册到Spring容器中,即可食用 package com.example.mp.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.mp.mappers.UserMapper; import com.example.mp.po.User; import com.example.mp.service.UserService; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl implements UserService { } 3.测试代码 package com.example.mp; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.example.mp.po.User; import com.example.mp.service.UserService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class ServiceTest { @Autowired private UserService userService; @Test public void testGetOne() { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.gt(User::getAge, 28); User one = userService.getOne(wrapper, false); // 第二参数指定为false,使得在查到了多行记录时,不抛出异常,而返回第一条记录 System.out.println(one); } } 4.结果 图片 另,IService也支持链式调用,代码写起来非常简洁,查询示例如下 @Test public void testChain() { List list = userService.lambdaQuery() .gt(User::getAge, 39) .likeRight(User::getName, "王") .list(); list.forEach(System.out::println); } 更新示例如下 @Test public void testChain() { userService.lambdaUpdate() .gt(User::getAge, 39) .likeRight(User::getName, "王") .set(User::getEmail, "w39@baomidou.com") .update(); } 图片 删除示例如下 @Test public void testChain() { userService.lambdaUpdate() .like(User::getName, "青蛙") .remove(); } 图片 条件构造器 mp让我觉得极其方便的一点在于其提供了强大的条件构造器Wrapper,可以非常方便的构造WHERE条件。条件构造器主要涉及到3个类,AbstractWrapper。QueryWrapper,UpdateWrapper,它们的类关系如下 图片 在AbstractWrapper中提供了非常多的方法用于构建WHERE条件,而QueryWrapper针对SELECT语句,提供了select()方法,可自定义需要查询的列,而UpdateWrapper针对UPDATE语句,提供了set()方法,用于构造set语句。条件构造器也支持lambda表达式,写起来非常舒爽。 下面对AbstractWrapper中用于构建SQL语句中的WHERE条件的方法进行部分列举 eq:equals,等于 allEq:all equals,全等于 ne:not equals,不等于 gt:greater than ,大于 > ge:greater than or equals,大于等于≥ lt:less than,小于< le:less than or equals,小于等于≤ between:相当于SQL中的BETWEEN notBetween like:模糊匹配。like("name","黄"),相当于SQL的name like '%黄%' likeRight:模糊匹配右半边。likeRight("name","黄"),相当于SQL的name like '黄%' likeLeft:模糊匹配左半边。likeLeft("name","黄"),相当于SQL的name like '%黄' notLike:notLike("name","黄"),相当于SQL的name not like '%黄%' isNull isNotNull in and:SQL连接符AND or:SQL连接符OR apply:用于拼接SQL,该方法可用于数据库函数,并可以动态传参 ....... 使用示例 下面通过一些具体的案例来练习条件构造器的使用。(使用前文创建的user表) // 案例先展示需要完成的SQL语句,后展示Wrapper的写法 // 1. 名字中包含佳,且年龄小于25 // SELECT * FROM user WHERE name like '%佳%' AND age < 25 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like("name", "佳").lt("age", 25); List users = userMapper.selectList(wrapper); // 下面展示SQL时,仅展示WHERE条件;展示代码时, 仅展示Wrapper构建部分 // 2. 姓名为黄姓,且年龄大于等于20,小于等于40,且email字段不为空 // name like '黄%' AND age BETWEEN 20 AND 40 AND email is not null wrapper.likeRight("name","黄").between("age", 20, 40).isNotNull("email"); // 3. 姓名为黄姓,或者年龄大于等于40,按照年龄降序排列,年龄相同则按照id升序排列 // name like '黄%' OR age >= 40 order by age desc, id asc wrapper.likeRight("name","黄").or().ge("age",40).orderByDesc("age").orderByAsc("id"); // 4.创建日期为2021年3月22日,并且直属上级的名字为李姓 // date_format(create_time,'%Y-%m-%d') = '2021-03-22' AND manager_id IN (SELECT id FROM user WHERE name like '李%') wrapper.apply("date_format(create_time, '%Y-%m-%d') = {0}", "2021-03-22") // 建议采用{index}这种方式动态传参, 可防止SQL注入 .inSql("manager_id", "SELECT id FROM user WHERE name like '李%'"); // 上面的apply, 也可以直接使用下面这种方式做字符串拼接,但当这个日期是一个外部参数时,这种方式有SQL注入的风险 wrapper.apply("date_format(create_time, '%Y-%m-%d') = '2021-03-22'"); // 5. 名字为王姓,并且(年龄小于40,或者邮箱不为空) // name like '王%' AND (age < 40 OR email is not null) wrapper.likeRight("name", "王").and(q -> q.lt("age", 40).or().isNotNull("email")); // 6. 名字为王姓,或者(年龄小于40并且年龄大于20并且邮箱不为空) // name like '王%' OR (age < 40 AND age > 20 AND email is not null) wrapper.likeRight("name", "王").or( q -> q.lt("age",40) .gt("age",20) .isNotNull("email") ); // 7. (年龄小于40或者邮箱不为空) 并且名字为王姓 // (age < 40 OR email is not null) AND name like '王%' wrapper.nested(q -> q.lt("age", 40).or().isNotNull("email")) .likeRight("name", "王"); // 8. 年龄为30,31,34,35 // age IN (30,31,34,35) wrapper.in("age", Arrays.asList(30,31,34,35)); // 或 wrapper.inSql("age","30,31,34,35"); // 9. 年龄为30,31,34,35, 返回满足条件的第一条记录 // age IN (30,31,34,35) LIMIT 1 wrapper.in("age", Arrays.asList(30,31,34,35)).last("LIMIT 1"); // 10. 只选出id, name 列 (QueryWrapper 特有) // SELECT id, name FROM user; wrapper.select("id", "name"); // 11. 选出id, name, age, email, 等同于排除 manager_id 和 create_time // 当列特别多, 而只需要排除个别列时, 采用上面的方式可能需要写很多个列, 可以采用重载的select方法,指定需要排除的列 wrapper.select(User.class, info -> { String columnName = info.getColumn(); return !"create_time".equals(columnName) && !"manager_id".equals(columnName); }); Condition 条件构造器的诸多方法中,均可以指定一个boolean类型的参数condition,用来决定该条件是否加入最后生成的WHERE语句中,比如 String name = "黄"; // 假设name变量是一个外部传入的参数 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like(StringUtils.hasText(name), "name", name); // 仅当 StringUtils.hasText(name) 为 true 时, 会拼接这个like语句到WHERE中 // 其实就是对下面代码的简化 if (StringUtils.hasText(name)) { wrapper.like("name", name); } 实体对象作为条件 调用构造函数创建一个Wrapper对象时,可以传入一个实体对象。后续使用这个Wrapper时,会以实体对象中的非空属性,构建WHERE条件(默认构建等值匹配的WHERE条件,这个行为可以通过实体类里各个字段上的@TableField注解中的condition属性进行改变) 示例如下 @Test public void test3() { User user = new User(); user.setName("黄主管"); user.setAge(28); QueryWrapper wrapper = new QueryWrapper<>(user); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 执行结果如下。可以看到,是根据实体对象中的非空属性,进行了等值匹配查询。 若希望针对某些属性,改变等值匹配的行为,则可以在实体类中用@TableField注解进行配置,示例如下 package com.example.mp.po; import com.baomidou.mybatisplus.annotation.SqlCondition; import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import java.time.LocalDateTime; @Data public class User { private Long id; @TableField(condition = SqlCondition.LIKE) // 配置该字段使用like进行拼接 private String name; private Integer age; private String email; private Long managerId; private LocalDateTime createTime; } 运行下面的测试代码 @Test public void test3() { User user = new User(); user.setName("黄"); QueryWrapper wrapper = new QueryWrapper<>(user); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 从下图得到的结果来看,对于实体对象中的name字段,采用了like进行拼接 图片 @TableField中配置的condition属性实则是一个字符串,SqlCondition类中预定义了一些字符串以供选择 package com.baomidou.mybatisplus.annotation; public class SqlCondition { //下面的字符串中, %s 是占位符, 第一个 %s 是列名, 第二个 %s 是列的值 public static final String EQUAL = "%s=#{%s}"; public static final String NOT_EQUAL = "%s<>#{%s}"; public static final String LIKE = "%s LIKE CONCAT('%%',#{%s},'%%')"; public static final String LIKE_LEFT = "%s LIKE CONCAT('%%',#{%s})"; public static final String LIKE_RIGHT = "%s LIKE CONCAT(#{%s},'%%')"; } SqlCondition中提供的配置比较有限,当我们需要<或>等拼接方式,则需要自己定义。比如 package com.example.mp.po; import com.baomidou.mybatisplus.annotation.SqlCondition; import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import java.time.LocalDateTime; @Data public class User { private Long id; @TableField(condition = SqlCondition.LIKE) private String name; @TableField(condition = "%s > #{%s}") // 这里相当于大于, 其中 > 是字符实体 private Integer age; private String email; private Long managerId; private LocalDateTime createTime; } 测试如下 @Test public void test3() { User user = new User(); user.setName("黄"); user.setAge(30); QueryWrapper wrapper = new QueryWrapper<>(user); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 从下图得到的结果,可以看出,name属性是用like拼接的,而age属性是用>拼接的 图片 allEq方法 allEq方法传入一个map,用来做等值匹配 @Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); Map param = new HashMap<>(); param.put("age", 40); param.put("name", "黄飞飞"); wrapper.allEq(param); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 图片 当allEq方法传入的Map中有value为null的元素时,默认会设置为is null @Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); Map param = new HashMap<>(); param.put("age", 40); param.put("name", null); wrapper.allEq(param); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 若想忽略map中value为null的元素,可以在调用allEq时,设置参数boolean null2IsNull为false @Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); Map param = new HashMap<>(); param.put("age", 40); param.put("name", null); wrapper.allEq(param, false); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 图片 若想要在执行allEq时,过滤掉Map中的某些元素,可以调用allEq的重载方法allEq(BiPredicate filter, Map params) @Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); Map param = new HashMap<>(); param.put("age", 40); param.put("name", "黄飞飞"); wrapper.allEq((k,v) -> !"name".equals(k), param); // 过滤掉map中key为name的元素 List users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 图片 lambda条件构造器 lambda条件构造器,支持lambda表达式,可以不必像普通条件构造器一样,以字符串形式指定列名,它可以直接以实体类的方法引用来指定列。示例如下 @Test public void testLambda() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.like(User::getName, "黄").lt(User::getAge, 30); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); } 像普通的条件构造器,列名是用字符串的形式指定,无法在编译期进行列名合法性的检查,这就不如lambda条件构造器来的优雅。 另外,还有个链式lambda条件构造器,使用示例如下 @Test public void testLambda() { LambdaQueryChainWrapper chainWrapper = new LambdaQueryChainWrapper<>(userMapper); List users = chainWrapper.like(User::getName, "黄").gt(User::getAge, 30).list(); users.forEach(System.out::println); } 更新操作 上面介绍的都是查询操作,现在来讲更新和删除操作。 BaseMapper中提供了2个更新方法 updateById(T entity) 根据入参entity的id(主键)进行更新,对于entity中非空的属性,会出现在UPDATE语句的SET后面,即entity中非空的属性,会被更新到数据库,示例如下 @RunWith(SpringRunner.class) @SpringBootTest public class UpdateTest { @Autowired private UserMapper userMapper; @Test public void testUpdate() { User user = new User(); user.setId(2L); user.setAge(18); userMapper.updateById(user); } } 图片 update(T entity, Wrapper wrapper) 根据实体entity和条件构造器wrapper进行更新,示例如下 @Test public void testUpdate2() { User user = new User(); user.setName("王三蛋"); LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); wrapper.between(User::getAge, 26,31).likeRight(User::getName,"吴"); userMapper.update(user, wrapper); } 额外演示一下,把实体对象传入Wrapper,即用实体对象构造WHERE条件的案例 @Test public void testUpdate3() { User whereUser = new User(); whereUser.setAge(40); whereUser.setName("王"); LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(whereUser); User user = new User(); user.setEmail("share@baomidou.com"); user.setManagerId(10L); userMapper.update(user, wrapper); } 注意到我们的User类中,对name属性和age属性进行了如下的设置 @Data public class User { private Long id; @TableField(condition = SqlCondition.LIKE) private String name; @TableField(condition = "%s > #{%s}") private Integer age; private String email; private Long managerId; private LocalDateTime createTime; } 执行结果 图片 图片 再额外演示一下,链式lambda条件构造器的使用 @Test public void testUpdate5() { LambdaUpdateChainWrapper wrapper = new LambdaUpdateChainWrapper<>(userMapper); wrapper.likeRight(User::getEmail, "share") .like(User::getName, "飞飞") .set(User::getEmail, "ff@baomidou.com") .update(); } 图片 反思 由于BaseMapper提供的2个更新方法都是传入一个实体对象去执行更新,这在需要更新的列比较多时还好,若想要更新的只有那么一列,或者两列,则创建一个实体对象就显得有点麻烦。针对这种情况,UpdateWrapper提供有set方法,可以手动拼接SQL中的SET语句,此时可以不必传入实体对象,示例如下 @Test public void testUpdate4() { LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); wrapper.likeRight(User::getEmail, "share").set(User::getManagerId, 9L); userMapper.update(null, wrapper); } 图片 删除操作 BaseMapper一共提供了如下几个用于删除的方法 deleteById 根据主键id进行删除 deleteBatchIds 根据主键id进行批量删除 deleteByMap 根据Map进行删除(Map中的key为列名,value为值,根据列和值进行等值匹配) delete(Wrapper wrapper) 根据条件构造器Wrapper进行删除 与前面查询和更新的操作大同小异,不做赘述 自定义SQL 当mp提供的方法还不能满足需求时,则可以自定义SQL。 原生mybatis 示例如下 注解方式 package com.example.mp.mappers; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @Author yogurtzzz * @Date 2021/3/18 11:21 **/ public interface UserMapper extends BaseMapper { @Select("select * from user") List selectRaw(); } xml方式 SELECT * FROM user package com.example.mp.mappers; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User; import org.apache.ibatis.annotations.Select; import java.util.List; public interface UserMapper extends BaseMapper { List selectRaw(); } 使用xml时,若xml文件与mapper接口文件不在同一目录下,则需要在application.yml中配置mapper.xml的存放路径 mybatis-plus: mapper-locations: /mappers/* 若有多个地方存放mapper,则用数组形式进行配置 mybatis-plus: mapper-locations: - /mappers/* - /com/example/mp/* 测试代码如下 @Test public void testCustomRawSql() { List users = userMapper.selectRaw(); users.forEach(System.out::println); } 结果 图片 mybatis-plus 也可以使用mp提供的Wrapper条件构造器,来自定义SQL 示例如下 注解方式 package com.example.mp.mappers; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.example.mp.po.User; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; public interface UserMapper extends BaseMapper { // SQL中不写WHERE关键字,且固定使用${ew.customSqlSegment} @Select("select * from user ${ew.customSqlSegment}") List findAll(@Param(Constants.WRAPPER)Wrapper wrapper); } xml方式 package com.example.mp.mappers; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User; import java.util.List; public interface UserMapper extends BaseMapper { List findAll(Wrapper wrapper); } SELECT * FROM user ${ew.customSqlSegment} 分页查询 BaseMapper中提供了2个方法进行分页查询,分别是selectPage和selectMapsPage,前者会将查询的结果封装成Java实体对象,后者会封装成Map。分页查询的食用示例如下 1. 创建mp的分页拦截器,注册到Spring容器中 package com.example.mp.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { /** 新版mp **/ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } /** 旧版mp 用 PaginationInterceptor **/ } 2. 执行分页查询 @Test public void testPage() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.ge(User::getAge, 28); // 设置分页信息, 查第3页, 每页2条数据 Page page = new Page<>(3, 2); // 执行分页查询 Page userPage = userMapper.selectPage(page, wrapper); System.out.println("总记录数 = " + userPage.getTotal()); System.out.println("总页数 = " + userPage.getPages()); System.out.println("当前页码 = " + userPage.getCurrent()); // 获取分页查询结果 List records = userPage.getRecords(); records.forEach(System.out::println); } 3. 结果 图片 4. 其他 注意到,分页查询总共发出了2次SQL,一次查总记录数,一次查具体数据。若希望不查总记录数,仅查分页结果。可以通过Page的重载构造函数,指定isSearchCount为false即可 public Page(long current, long size, boolean isSearchCount) 在实际开发中,可能遇到多表联查的场景,此时BaseMapper中提供的单表分页查询的方法无法满足需求,需要自定义SQL,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,修改SQL语句即可) 1. 在mapper接口中定义一个函数,接收一个Page对象为参数,并编写自定义SQL // 这里采用纯注解方式。当然,若SQL比较复杂,建议还是采用XML的方式 @Select("SELECT * FROM user ${ew.customSqlSegment}") Page selectUserPage(Page page, @Param(Constants.WRAPPER) Wrapper wrapper); 2. 执行查询 @Test public void testPage2() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.ge(User::getAge, 28).likeRight(User::getName, "王"); Page page = new Page<>(3,2); Page userPage = userMapper.selectUserPage(page, wrapper); System.out.println("总记录数 = " + userPage.getTotal()); System.out.println("总页数 = " + userPage.getPages()); userPage.getRecords().forEach(System.out::println); } 3. 结果 AR模式 ActiveRecord模式,通过操作实体对象,直接操作数据库表。与ORM有点类似。 示例如下 让实体类User继承自Model package com.example.mp.po; import com.baomidou.mybatisplus.annotation.SqlCondition; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.extension.activerecord.Model; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; @EqualsAndHashCode(callSuper = false) @Data public class User extends Model { private Long id; @TableField(condition = SqlCondition.LIKE) private String name; @TableField(condition = "%s > #{%s}") private Integer age; private String email; private Long managerId; private LocalDateTime createTime; } 直接调用实体对象上的方法 @Test public void insertAr() { User user = new User(); user.setId(15L); user.setName("我是AR猪"); user.setAge(1); user.setEmail("ar@baomidou.com"); user.setManagerId(1L); boolean success = user.insert(); // 插入 System.out.println(success); } 结果 图片 其他示例 // 查询 @Test public void selectAr() { User user = new User(); user.setId(15L); User result = user.selectById(); System.out.println(result); } // 更新 @Test public void updateAr() { User user = new User(); user.setId(15L); user.setName("王全蛋"); user.updateById(); } //删除 @Test public void deleteAr() { User user = new User(); user.setId(15L); user.deleteById(); } 主键策略 在定义实体类时,用@TableId指定主键,而其type属性,可以指定主键策略。 mp支持多种主键策略,默认的策略是基于雪花算法的自增id。全部主键策略定义在了枚举类IdType中,IdType有如下的取值 AUTO数据库ID自增,依赖于数据库。在插入操作生成SQL语句时,不会插入主键这一列 NONE未设置主键类型。若在代码中没有手动设置主键,则会根据主键的全局策略自动生成(默认的主键全局策略是基于雪花算法的自增ID) INPUT需要手动设置主键,若不设置。插入操作生成SQL语句时,主键这一列的值会是null。oracle的序列主键需要使用这种方式 ASSIGN_ID当没有手动设置主键,即实体类中的主键属性为空时,才会自动填充,使用雪花算法 ASSIGN_UUID当实体类的主键属性为空时,才会自动填充,使用UUID ....(还有几种是已过时的,就不再列举) 可以针对每个实体类,使用@TableId注解指定该实体类的主键策略,这可以理解为局部策略。若希望对所有的实体类,都采用同一种主键策略,挨个在每个实体类上进行配置,则太麻烦了,此时可以用主键的全局策略。只需要在application.yml进行配置即可。比如,配置了全局采用自增主键策略 # application.yml mybatis-plus: global-config: db-config: id-type: auto 下面对不同主键策略的行为进行演示 AUTO在User上对id属性加上注解,然后将MYSQL的user表修改其主键为自增。 @EqualsAndHashCode(callSuper = false) @Data public class User extends Model { @TableId(type = IdType.AUTO) private Long id; @TableField(condition = SqlCondition.LIKE) private String name; @TableField(condition = "%s > #{%s}") private Integer age; private String email; private Long managerId; private LocalDateTime createTime; } 测试 @Test public void testAuto() { User user = new User(); user.setName("我是青蛙呱呱"); user.setAge(99); user.setEmail("frog@baomidou.com"); user.setCreateTime(LocalDateTime.now()); userMapper.insert(user); System.out.println(user.getId()); } 结果 图片 可以看到,代码中没有设置主键ID,发出的SQL语句中也没有设置主键ID,并且插入结束后,主键ID会被写回到实体对象。 NONE在MYSQL的user表中,去掉主键自增。然后修改User类(若不配置@TableId注解,默认主键策略也是NONE) @TableId(type = IdType.NONE) private Long id; 插入时,若实体类的主键ID有值,则使用之;若主键ID为空,则使用主键全局策略,来生成一个ID。 其余的策略类似,不赘述 小结 AUTO依赖于数据库的自增主键,插入时,实体对象无需设置主键,插入成功后,主键会被写回实体对象。 INPUT完全依赖于用户输入。实体对象中主键ID是什么,插入到数据库时就设置什么。若有值便设置值,若为null则设置null 其余的几个策略,都是在实体对象中主键ID为空时,才会自动生成。 NONE会跟随全局策略,ASSIGN_ID采用雪花算法,ASSIGN_UUID采用UUID 全局配置,在application.yml中进行即可;针对单个实体类的局部配置,使用@TableId即可。对于某个实体类,若它有局部主键策略,则采用之,否则,跟随全局策略。 配置 mybatis plus有许多可配置项,可在application.yml中进行配置,如上面的全局主键策略。下面列举部分配置项 基本配置 configLocation:若有单独的mybatis配置,用这个注解指定mybatis的配置文件(mybatis的全局配置文件) mapperLocations:mybatis mapper所对应的xml文件的位置 typeAliasesPackage:mybatis的别名包扫描路径 ..... 进阶配置 mapUnderscoreToCamelCase:是否开启自动驼峰命名规则映射。(默认开启) dbTpe:数据库类型。一般不用配,会根据数据库连接url自动识别 fieldStrategy:(已过时)字段验证策略。该配置项在最新版的mp文档中已经找不到了,被细分成了insertStrategy,updateStrategy,selectStrategy。默认值是NOT_NULL,即对于实体对象中非空的字段,才会组装到最终的SQL语句中。 有如下几种可选配置 这个配置项,可在application.yml中进行全局配置,也可以在某一实体类中,对某一字段用@TableField注解进行局部配置 这个字段验证策略有什么用呢?在UPDATE操作中能够体现出来,若用一个User对象执行UPDATE操作,我们希望只对User对象中非空的属性,更新到数据库中,其他属性不做更新,则NOT_NULL可以满足需求。 而若updateStrategy配置为IGNORED,则不会进行非空判断,会将实体对象中的全部属性如实组装到SQL中,这样,执行UPDATE时,可能就将一些不想更新的字段,设置为了NULL。 IGNORED:忽略校验。即,不做校验。实体对象中的全部字段,无论值是什么,都如实地被组装到SQL语句中(为NULL的字段在SQL语句中就组装为NULL)。 NOT_NULL:非NULL校验。只会将非NULL的字段组装到SQL语句中 NOT_EMPTY:非空校验。当有字段是字符串类型时,只组装非空字符串;对其他类型的字段,等同于NOT_NULL NEVER:不加入SQL。所有字段不加入到SQL语句 tablePrefix:添加表名前缀 比如 mybatis-plus: global-config: db-config: table-prefix: xx_ 然后将MYSQL中的表做一下修改。但Java实体类保持不变(仍然为User)。 测试 @Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like("name", "黄"); Integer count = userMapper.selectCount(wrapper); System.out.println(count); } 可以看到拼接出来的SQL,在表名前面添加了前缀 图片 代码生成器 mp提供一个生成器,可快速生成Entity实体类,Mapper接口,Service,Controller等全套代码。 示例如下 public class GeneratorTest { @Test public void generate() { AutoGenerator generator = new AutoGenerator(); // 全局配置 GlobalConfig config = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); // 设置输出到的目录 config.setOutputDir(projectPath + "/src/main/java"); config.setAuthor("yogurt"); // 生成结束后是否打开文件夹 config.setOpen(false); // 全局配置添加到 generator 上 generator.setGlobalConfig(config); // 数据源配置 DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai"); dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver"); dataSourceConfig.setUsername("root"); dataSourceConfig.setPassword("root"); // 数据源配置添加到 generator generator.setDataSource(dataSourceConfig); // 包配置, 生成的代码放在哪个包下 PackageConfig packageConfig = new PackageConfig(); packageConfig.setParent("com.example.mp.generator"); // 包配置添加到 generator generator.setPackageInfo(packageConfig); // 策略配置 StrategyConfig strategyConfig = new StrategyConfig(); // 下划线驼峰命名转换 strategyConfig.setNaming(NamingStrategy.underline_to_camel); strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel); // 开启lombok strategyConfig.setEntityLombokModel(true); // 开启RestController strategyConfig.setRestControllerStyle(true); generator.setStrategy(strategyConfig); generator.setTemplateEngine(new FreemarkerTemplateEngine()); // 开始生成 generator.execute(); } } 运行后,可以看到生成了如下图所示的全套代码 图片 高级功能 高级功能的演示需要用到一张新的表user2 DROP TABLE IF EXISTS user2; CREATE TABLE user2 ( id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键id', name VARCHAR(30) DEFAULT NULL COMMENT '姓名', age INT(11) DEFAULT NULL COMMENT '年龄', email VARCHAR(50) DEFAULT NULL COMMENT '邮箱', manager_id BIGINT(20) DEFAULT NULL COMMENT '直属上级id', create_time DATETIME DEFAULT NULL COMMENT '创建时间', update_time DATETIME DEFAULT NULL COMMENT '修改时间', version INT(11) DEFAULT '1' COMMENT '版本', deleted INT(1) DEFAULT '0' COMMENT '逻辑删除标识,0-未删除,1-已删除', CONSTRAINT manager_fk FOREIGN KEY(manager_id) REFERENCES user2(id) ) ENGINE = INNODB CHARSET=UTF8; INSERT INTO user2(id, name, age, email, manager_id, create_time) VALUES (1, '老板', 40 ,'boss@baomidou.com' ,NULL, '2021-03-28 13:12:40'), (2, '王狗蛋', 40 ,'gd@baomidou.com' ,1, '2021-03-28 13:12:40'), (3, '王鸡蛋', 40 ,'jd@baomidou.com' ,2, '2021-03-28 13:12:40'), (4, '王鸭蛋', 40 ,'yd@baomidou.com' ,2, '2021-03-28 13:12:40'), (5, '王猪蛋', 40 ,'zd@baomidou.com' ,2, '2021-03-28 13:12:40'), (6, '王软蛋', 40 ,'rd@baomidou.com' ,2, '2021-03-28 13:12:40'), (7, '王铁蛋', 40 ,'td@baomidou.com' ,2, '2021-03-28 13:12:40') 并创建对应的实体类User2 package com.example.mp.po; import lombok.Data; import java.time.LocalDateTime; @Data public class User2 { private Long id; private String name; private Integer age; private String email; private Long managerId; private LocalDateTime createTime; private LocalDateTime updateTime; private Integer version; private Integer deleted; } 以及Mapper接口 package com.example.mp.mappers; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User2; public interface User2Mapper extends BaseMapper { } 逻辑删除 首先,为什么要有逻辑删除呢?直接删掉不行吗?当然可以,但日后若想要恢复,或者需要查看这些数据,就做不到了。逻辑删除是为了方便数据恢复,和保护数据本身价值的一种方案。 日常中,我们在电脑中删除一个文件后,也仅仅是把该文件放入了回收站,日后若有需要还能进行查看或恢复。当我们确定不再需要某个文件,可以将其从回收站中彻底删除。这也是类似的道理。 mp提供的逻辑删除实现起来非常简单 只需要在application.yml中进行逻辑删除的相关配置即可 mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除的实体字段名 logic-delete-value: 1 # 逻辑已删除值(默认为1) logic-not-delete-value: 0 # 逻辑未删除值(默认为0) # 若逻辑已删除和未删除的值和默认值一样,则可以不配置这2项 测试代码 package com.example.mp; import com.example.mp.mappers.User2Mapper; import com.example.mp.po.User2; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; @RunWith(SpringRunner.class) @SpringBootTest public class LogicDeleteTest { @Autowired private User2Mapper mapper; @Test public void testLogicDel() { int i = mapper.deleteById(6); System.out.println("rowAffected = " + i); } } 结果 图片 可以看到,发出的SQL不再是DELETE,而是UPDATE 此时我们再执行一次SELECT @Test public void testSelect() { List users = mapper.selectList(null); } 图片 可以看到,发出的SQL语句,会自动在WHERE后面拼接逻辑未删除的条件。查询出来的结果中,没有了id为6的王软蛋。 若想要SELECT的列,不包括逻辑删除的那一列,则可以在实体类中通过@TableField进行配置 @TableField(select = false) private Integer deleted; 可以看到下图的执行结果中,SELECT中已经不包含deleted这一列了 图片 前面在application.yml中做的配置,是全局的。通常来说,对于多个表,我们也会统一逻辑删除字段的名称,统一逻辑已删除和未删除的值,所以全局配置即可。当然,若要对某些表进行单独配置,在实体类的对应字段上使用@TableLogic即可 @TableLogic(value = "0", delval = "1") private Integer deleted; 小结 开启mp的逻辑删除后,会对SQL产生如下的影响 INSERT语句:没有影响 SELECT语句:追加WHERE条件,过滤掉已删除的数据 UPDATE语句:追加WHERE条件,防止更新到已删除的数据 DELETE语句:转变为UPDATE语句 注意,上述的影响,只针对mp自动注入的SQL生效。如果是自己手动添加的自定义SQL,则不会生效。比如 public interface User2Mapper extends BaseMapper { @Select("select * from user2") List selectRaw(); } 调用这个selectRaw,则mp的逻辑删除不会生效。 另,逻辑删除可在application.yml中进行全局配置,也可在实体类中用@TableLogic进行局部配置。 自动填充 表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。比较原始的方式,是每次插入或更新时,手动进行设置。mp可以通过配置,对某些字段进行自动填充,食用示例如下 1. 在实体类中的某些字段上,通过@TableField设置自动填充 public class User2 { private Long id; private String name; private Integer age; private String email; private Long managerId; @TableField(fill = FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; @TableField(fill = FieldFill.UPDATE) // 更新时自动填充 private LocalDateTime updateTime; private Integer version; private Integer deleted; } 2. 实现自动填充处理器 package com.example.mp.component; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Component //需要注册到Spring容器中 public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 插入时自动填充 // 注意第二个参数要填写实体类中的字段名称,而不是表的列名称 strictFillStrategy(metaObject, "createTime", LocalDateTime::now); } @Override public void updateFill(MetaObject metaObject) { // 更新时自动填充 strictFillStrategy(metaObject, "updateTime", LocalDateTime::now); } } 测试 @Test public void test() { User2 user = new User2(); user.setId(8L); user.setName("王一蛋"); user.setAge(29); user.setEmail("yd@baomidou.com"); user.setManagerId(2L); mapper.insert(user); } 根据下图结果,可以看到对createTime进行了自动填充 注意,自动填充仅在该字段为空时会生效,若该字段不为空,则直接使用已有的值。如下 @Test public void test() { User2 user = new User2(); user.setId(8L); user.setName("王一蛋"); user.setAge(29); user.setEmail("yd@baomidou.com"); user.setManagerId(2L); user.setCreateTime(LocalDateTime.of(2000,1,1,8,0,0)); mapper.insert(user); } 图片 更新时的自动填充,测试如下 @Test public void test() { User2 user = new User2(); user.setId(8L); user.setName("王一蛋"); user.setAge(99); mapper.updateById(user); } 图片 乐观锁插件 当出现并发操作时,需要确保各个用户对数据的操作不产生冲突,此时需要一种并发控制手段。悲观锁的方法是,在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作;而乐观锁,正如其名,它先假设不存在冲突情况,而在实际进行数据操作时,再检查是否冲突。乐观锁的一种通常实现是版本号,在MySQL中也有名为MVCC的基于版本号的并发事务控制。 在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。 在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。 乐观锁的实现如下: 取出记录时,获取当前version 更新时,带上这个version 执行更新时, set version = newVersion where version = oldVersion 如果oldVersion与数据库中的version不一致,就更新失败 这种思想和CAS(Compare And Swap)非常相似。 乐观锁的实现步骤如下 1. 配置乐观锁插件 package com.example.mp.config; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { /** 3.4.0以后的mp版本,推荐用如下的配置方式 **/ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } /** 旧版mp可以采用如下方式。注意新旧版本中,新版的类,名称带有Inner, 旧版的不带, 不要配错了 **/ /* @Bean public OptimisticLockerInterceptor opLocker() { return new OptimisticLockerInterceptor(); } */ } 2. 在实体类中表示版本的字段上添加注解@Version @Data public class User2 { private Long id; private String name; private Integer age; private String email; private Long managerId; private LocalDateTime createTime; private LocalDateTime updateTime; @Version private Integer version; private Integer deleted; } 测试代码 @Test public void testOpLocker() { int version = 1; // 假设这个version是先前查询时获得的 User2 user = new User2(); user.setId(8L); user.setEmail("version@baomidou.com"); user.setVersion(version); int i = mapper.updateById(user); } 执行之前先看一下数据库的情况 图片 根据下图执行结果,可以看到SQL语句中添加了version相关的操作 图片 当UPDATE返回了1,表示影响行数为1,则更新成功。反之,由于WHERE后面的version与数据库中的不一致,匹配不到任何记录,则影响行数为0,表示更新失败。更新成功后,新的version会被封装回实体对象中。 实体类中version字段,类型只支持int,long,Date,Timestamp,LocalDateTime 注意,乐观锁插件仅支持**updateById(id)与update(entity, wrapper)方法** 注意:如果使用wrapper,则wrapper不能复用!示例如下 @Test public void testOpLocker() { User2 user = new User2(); user.setId(8L); user.setVersion(1); user.setAge(2); // 第一次使用 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User2::getName, "王一蛋"); mapper.update(user, wrapper); // 第二次复用 user.setAge(3); mapper.update(user, wrapper); } 图片 可以看到在第二次复用wrapper时,拼接出的SQL中,后面WHERE语句中出现了2次version,是有问题的。 性能分析插件 该插件会输出SQL语句的执行时间,以便做SQL语句的性能分析和调优。 注:3.2.0版本之后,mp自带的性能分析插件被官方移除了,而推荐食用第三方性能分析插件 食用步骤 1. 引入maven依赖 p6spy p6spy 3.9.1 2. 修改application.yml spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver #换成p6spy的驱动 url: jdbc:p6spy:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai #url修改 username: root password: root 3. 在src/main/resources资源目录下添加spy.properties #spy.properties #3.2.1以上使用 modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory # 真实JDBC driver , 多个以逗号分割,默认为空。由于上面设置了modulelist, 这里可以不用设置driverlist #driverlist=com.mysql.cj.jdbc.Driver # 自定义日志打印 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger #若要日志输出到文件, 把上面的appnder注释掉, 或者采用下面的appender, 再添加logfile配置 #不配置appender时, 默认是往文件进行输出的 #appender=com.p6spy.engine.spy.appender.FileLogger #logfile=log.log # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=2 # 执行时间设置, 只有超过这个执行时间的才进行记录, 默认值0, 单位毫秒 executionThreshold=10 随便运行一个测试用例,可以看到该SQL的执行时长被记录了下来 图片 多租户SQL解析器 多租户的概念:多个用户共用一套系统,但他们的数据有需要相对的独立,保持一定的隔离性。 多租户的数据隔离一般有如下的方式: 不同租户使用不同的数据库服务器优点是:不同租户有不同的独立数据库,有助于扩展,以及对不同租户提供更好的个性化,出现故障时恢复数据较为简单。缺点是:增加了数据库数量,购置成本,维护成本更高 不同租户使用相同的数据库服务器,但使用不同的数据库(不同的schema)优点是购置和维护成本低了一些,缺点是数据恢复较为困难,因为不同租户的数据都放在了一起 不同租户使用相同的数据库服务器,使用相同的数据库,共享数据表,在表中增加租户id来做区分优点是,购置和维护成本最低,支持用户最多,缺点是隔离性最低,安全性最低 食用实例如下 添加多租户拦截器配置。添加配置后,在执行CRUD的时候,会自动在SQL语句最后拼接租户id的条件 package com.example.mp.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { @Override public Expression getTenantId() { // 返回租户id的值, 这里固定写死为1 // 一般是从当前上下文中取出一个 租户id return new LongValue(1); } /** ** 通常会将表示租户id的列名,需要排除租户id的表等信息,封装到一个配置类中(如TenantConfig) **/ @Override public String getTenantIdColumn() { // 返回表中的表示租户id的列名 return "manager_id"; } @Override public boolean ignoreTable(String tableName) { // 表名不为 user2 的表, 不拼接多租户条件 return !"user2".equals(tableName); } })); // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false return interceptor; } } 测试代码 @Test public void testTenant() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.likeRight(User2::getName, "王") .select(User2::getName, User2::getAge, User2::getEmail, User2::getManagerId); user2Mapper.selectList(wrapper); } 动态表名SQL解析器 当数据量特别大的时候,我们通常会采用分库分表。这时,可能就会有多张表,其表结构相同,但表名不同。例如order_1,order_2,order_3,查询时,我们可能需要动态设置要查的表名。mp提供了动态表名SQL解析器,食用示例如下 先在mysql中拷贝一下user2表 图片 配置动态表名拦截器 package com.example.mp.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler; import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Random; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor(); HashMap map = new HashMap<>(); // 对于user2表,进行动态表名设置 map.put("user2", (sql, tableName) -> { String _ = "_"; int random = new Random().nextInt(2) + 1; return tableName + _ + random; // 若返回null, 则不会进行动态表名替换, 还是会使用user2 }); dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map); interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor); return interceptor; } } 测试 @Test public void testDynamicTable() { user2Mapper.selectList(null); } 图片 总结 条件构造器AbstractWrapper中提供了多个方法用于构造SQL语句中的WHERE条件,而其子类QueryWrapper额外提供了select方法,可以只选取特定的列,子类UpdateWrapper额外提供了set方法,用于设置SQL中的SET语句。除了普通的Wrapper,还有基于lambda表达式的Wrapper,如LambdaQueryWrapper,LambdaUpdateWrapper,它们在构造WHERE条件时,直接以方法引用来指定WHERE条件中的列,比普通Wrapper通过字符串来指定要更加优雅。另,还有链式Wrapper,如LambdaQueryChainWrapper,它封装了BaseMapper,可以更方便地获取结果。 条件构造器采用链式调用来拼接多个条件,条件之间默认以AND连接 当AND或OR后面的条件需要被括号包裹时,将括号中的条件以lambda表达式形式,作为参数传入and()或or()特别的,当()需要放在WHERE语句的最开头时,可以使用nested()方法 条件表达式时当需要传入自定义的SQL语句,或者需要调用数据库函数时,可用apply()方法进行SQL拼接 条件构造器中的各个方法可以通过一个boolean类型的变量condition,来根据需要灵活拼接WHERE条件(仅当condition为true时会拼接SQL语句) 使用lambda条件构造器,可以通过lambda表达式,直接使用实体类中的属性进行条件构造,比普通的条件构造器更加优雅 若mp提供的方法不够用,可以通过自定义SQL(原生mybatis)的形式进行扩展开发 使用mp进行分页查询时,需要创建一个分页拦截器(Interceptor),注册到Spring容器中,随后查询时,通过传入一个分页对象(Page对象)进行查询即可。单表查询时,可以使用BaseMapper提供的selectPage或selectMapsPage方法。复杂场景下(如多表联查),使用自定义SQL。 AR模式可以直接通过操作实体类来操作数据库。让实体类继承自Model即可 热门内容:还在写大量 if 来判断?试试用一个规则执行器来替代它 CTO 说了,如果发现谁用 kill -9 关闭程序就开除 面试官问:MySQL 的自增 ID 用完了,怎么办? 王者荣耀中一个英雄是怎么被产生的? 饿了么CTO:“不能被烂用的框架不是好框架”! 最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。 明天见(。・ω・。)ノ♡ 你可能感兴趣的:(数据库,glassfish,entity,oa办公,opera) 解决yum安装 docker 依赖安装 报错问题 星星向前看 docker容器运维 解决yum-docker依赖安装问题>https://blog.csdn.net/qq_46051366/article/details/140425381方法一:检查网络连接首先,确保虚拟机能够访问互联网。pingwww.baidu.compingdownload.docker.com方法二:检查和配置DNS服务器打开/etc/resolv.conf文件:sudovi/etc/resolv.co 微信小程序实现nfc功能(读取,写入) _lucky_boy 新手小白微信小程序nfc标签卡前端微信小程序小程序 标签读取功能可以放到onLoad中,也可以是一个点击事件//获取NFC实例constnfc=wx.getNFCAdapter()//绑定监听NFC标签nfc.onDiscovered(res=>{//监听到数据进行返回根据返回的数据在进行处理console.log(9999,res)})//开始监听不能缺少nfc.startDiscovery({success(res){console.log(5 Composer install 报错 要加油呀 问题整理composerphp Composerinstall报错项目构建命令:composerinstall--no-dev--no-interaction-o--ignore-platform-reqs构建输出:没有错误,陷入死循环>Illuminate\Foundation\ComposerScripts::postAutoloadDump>@phpartisanpackage:discover--ansiDiscover latex双列排版下,插入表格但在单独一页出现,换页出现 CheerfulMinions 笔记 问题描述:在双列排版中,由于需要插入单列的整块表格,但表格出现在新的一页,如图:解决:注意是hb,不是htbp\begin{figure*}[hb]\centering\includegraphics[scale=0.4]{img1.jpg}\caption{Thisisanon-floatingfigure}\label{fig_framework}\end{figure*} Fire Game CheerfulMinions 竞赛算法练习算法 FatbrotherandMazeareplayingakindofspecial(hentai)gameonanN*Mboard(Nrows,Mcolumns).Atthebeginning,eachgridofthisboardisconsistingofgrassorjustemptyandthentheystarttofireallthegrass.Firstlytheychoosetwo phpstorm重复试用 Sword-Holy PHPStorm试用期重置清理缓存注册表开发者指南 phpstorm重复试用1.关闭软件(建议先导出设置配置,操作会重置配置)2.删除文件(一般AppData会被隐藏,需要通过查看选项里面打开查看隐藏文件,不懂自行百度)删除目录C:\Users\用户名\AppData\Roaming\JetBrains\PhpStorm2020.2\eval删除文件C:\Users\用户名\AppData\Roaming\JetBrains\PhpStorm202 解决Composer依赖报错问题的方法 代码之旅 composerphp 在进行PHP项目开发过程中,我们经常会使用Composer来管理项目的依赖关系。然而,有时候在执行composerinstall或composerupdate命令时可能会遇到一些报错。本文将介绍一些常见的Composer依赖报错问题,并提供相应的解决方案。问题:依赖冲突错误错误信息示例:Yourrequirementscouldnotberesolvedtoaninstallablesetofpa Composer的报错及解决办法 代码之旅 composerandroidandroidstudio Composer是一个用于管理PHP项目依赖关系的工具,它使得项目的依赖包安装、更新和管理变得简单和高效。然而,在使用Composer的过程中,有时候会遇到一些报错。本文将介绍一些常见的Composer报错,并提供相应的解决办法。“Yourrequirementscouldnotberesolvedtoaninstallablesetofpackages.”这个错误通常发生在安装或更新依赖包时,表 uniapp实现点击图片预览放大,长按下载图片 luoluosheng07 uniappuni-app前端 1.使用uniapp中的uni.previewImageAPI预览图片2.使用uniapp中的uni.downloadFileAPI下载图片3.使用uniapp中的uni.saveImageToPhotosAlbumAPI将图片保存到相册exportdefault{data(){return{}},methods:{previewImage(e){varcurrent=e.target.datas composer install报错 h_ss4 composerlinux运维laravel Nocomposer.lockfilepresent.Updatingdependenciestolatestinsteadofinstallingfromlockfile.Seehttps://getcomposer.org/installformoreinformation.LoadingcomposerrepositorieswithpackageinformationUpdatingdep 【用java在控制台实现简单的图书管理系统】 A boy CDEF girl java开发语言 用java在控制台实现简单的图书管理系统book包Book类BookList类user包User类AdmiUser类(管理员)NormalUser类(管理员)opertion包IOperation接口FindOpertion(查找图书)AddOpertion(增加图书)DeletOpertion(删除图书)BorrowOpertion(借阅图书)ReturnOpertion(归还图书)ShowOp composer install 报错 Sword-Holy composerphp composerinstall报错curlerror28whiledownloadinghttps://cdn.asset-packagist.org/packages.json:Timeoutwasreached解决:将composer.json中https://asset-packagist.org替换成https://asset-packagist.cn mysql-bin.index_mysqlbin.index是什么文件 张太学 mysql-bin.index 匿名用户1级2016-06-01回答今天发现/usr/local/mysql/var下很多mysql-bin.000001、mysql-bin.000002文件,GOOGLE之。。这是数据库的操作日志,例如UPDATE一个表,或者DELETE一些数据,即使该语句没有匹配的数据,这个命令也会存储到日志文件中,还包括每个语句执行的时间,也会记录进去的。这样做主要有以下两个目的:1:数据恢复如果你的数据 Ubuntu系统上部署Node.js项目的完整流程 AF01 ubuntunode.jslinux 以下是在Ubuntu系统上部署Node.js项目的完整流程,分为系统初始化、环境配置、项目部署三个部分:一、系统初始化&环境准备bash#1.更新系统软件包sudoaptupdate&&sudoaptupgrade-y#2.安装基础工具sudoaptinstall-ybuild-essentialgitcurl#3.安装Node.js(推荐使用LTS版本)curl-fsSLhttps://deb. DS-3KM220250226 3K引擎修复版传奇2025版完整源码搭建教程 legendji oracle数据库delphi开源 DS-3KM2202502263K引擎修复版传奇2025版完整源码搭建教程本文将详细介绍如何搭建DS-3KM2202502263K引擎修复版传奇2025版,确保能顺利运行游戏。一、前期准备1.环境配置在服务器或本地电脑上安装以下必要环境:操作系统:WindowsServer2012/2016/2019或Windows10/11(建议使用64位)数据库:MicrosoftSQLServer2008及 node.js 文件上传_如何在Node.js中处理文件上传 cuk0051 nodejsvuepythonjavadjangoViewUI node.js文件上传InhowtouploadafileusingFetchIexplainedhowtouploadafiletoaserverusingFetch.在如何上传使用取文件我解释如何将文件上传到使用服务器获取。InthispostI’mgoingtoshowyoupart2:howtouseNode.js,andinparticularExpress,tohandleupload Failed to resolve loader: cache-loader 菜鸟蹦迪 前端java 根据提示下载cache-loader仍然报错解决办法:将npm版本降低即可npminstallnpm@6.14.8-g然后就跑出来了: 值得信赖的伙伴:AORO A30防爆手机助力企业应对安全与效率双重挑战 AORO_BEIDOU 人工智能5G智能手机信息与通信安全网络 在当今这个信息化、智能化飞速发展的时代,企业如何在保障安全的前提下,提高效率、降低成本,成为了一个亟待解决的问题。特别是在石油化工等高危行业,安全问题更是重中之重。那么,有没有一款设备能够帮助企业解决这些难点痛点,同时为企业创造价值呢?遨游通讯推出的AOROA30防爆手机,或许就是那个答案。这款手机精准定位细微故障,从源头上有效控制或消除安全隐患,为企业安全保驾护航。它采用了遨游通讯自研的九重防爆 【开发笔记】 Postgres-12.1数据库,基于docker-compose做主从备份 love__nana 数据库postgresql 如题,关于postgres12.1版本做主从备份一开始安装了两个数据库,照着好多教程,配置主数据库后,数据库无法启动,查看启动日志,发现是主数据库的postgresql.conf中的配置了wal_keep_segments,配置文件中有这个参数的说明,但是配置了就无法启动,原因在查找中折腾了一天,最后在大神的帮助下,基于网上现成的9.5版本的docker,改造调试了下,将12.1版本的整理出来直接 Java 内存溢出(java.lang.OutOfMemoryError)的常见情况和处理方式总结 笑锝没心没肺 JAVAjava开发语言 介绍PermGenspace的全称是PermanentGenerationspace,是指内存的永久保存区域OutOfMemoryError:PermGenspace从表面上看就是内存益出,解决方法也一定是加大内存。说说为什么会内存益出:这一部分用于存放Class和Meta的信息,Class在被Load的时候被放入PermGenspace区域,它和和存放Instance的Heap区域不同,GC(G kubeadm_k8s_v1.31高可用部署教程 techzhi kubernetes容器云原生 kubeadm_k8s_v1.31高可用部署教程实验环境部署拓扑图**部署署架构****LoadBalance****Controlplanenode****Workernode****资源分配(8台虚拟机)**集群列表前置准备关闭swap开启ipv4转发更多设置1、VerifytheMACaddressandproduct_uuidareuniqueforeverynode2、Checknetw django app中的models迁移问题根治方法 kunkun_1230 djangodjango数据库 今天想给某个app里添加一张表,但是忽略了主键冲突问题,再想改的时候就一直提示Youaretryingtoaddanon-nullablefield‘id’tosensorconfigalllogwithoutadefault;wecan’tdothat(thedatabaseneedssomethingtopopulateexistingrows).Pleaseselectafix:Provid Spring Cloud Alibaba入门教程合集-01【微服务和Spring Cloud Alibaba介绍】 图灵学院架构师 Java前段spring微服务javamicroservicesspring性能优化 1、微服务介绍1.1系统架构演变随着互联网的发展,网站应用的规模也在不断的扩大,进而导致系统架构也在不断的进行变化。从互联网早起到现在,系统架构大体经历了下面几个过程:单体应用架构—>垂直应用架构—>分布式架构—>SOA架构—>微服务架构,当然还有悄然兴起的ServiceMesh(服务网格化)。接下来我们就来了解一下每种系统架构是什么样子的,以及各有什么优缺点。1.1.1单体应用架构互联网早期,一 spring boot 是如何加载配值文件的 花花进修 springbootspringboot publicSpringApplication(ResourceLoaderresourceLoader,Class...primarySources){this.sources=newLinkedHashSet();this.bannerMode=Mode.CONSOLE;this.logStartupInfo=true;this.addCommandLineProperties=true;th Elasticsearch常用命令 墨明&棋妙 elasticsearchspringcloud全文检索搜索引擎 1、安装教程windows环境下elasticsearch安装教程(超详细)-hualess-博客园(cnblogs.com)Elasticsearch下载地址:https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.18-windows-x86_64.zip2、安装分词器,elasticsearch-anal Docker部署postgres数据库 RedEric 部署运维docker数据库容器postgresql Docker部署postgres数据库拉取镜像dockerpullpostgres:10.21-alpine启动容器dockerrun-d-p15432:5432-v/home/server/postgres/pgdata:/var/lib/postgresql/data-ePOSTGRES_PASSWORD=a123456--namepgsqlpostgres:10.21-alpine进入PgS 基于javaweb的流浪宠物管理系统的设计与实现 然然学长 java开发语言springboot毕业设计 运行环境环境说明:开发语言:java框架:springboot,vueJDK版本:JDK1.8数据库:mysql5.7+(推荐5.7,8.0也可以)数据库工具:Navicat11+开发软件:idea/eclipse(推荐idea)Maven包:Maven3.3.9+系统实现5.1个人中心通过设计的个人中心管理功能模块,管理用户可以对相关的个人信息进行管理,比如管理用户可以更新个人账号的密码信息,修 如何升级node.js版本 m0_74824865 面试学习路线阿里巴巴node.js 升级Node.js可以通过多种方式来完成,以下是四种常见的方法:方法一:使用Node.js官方安装程序访问Node.js的官方网站,下载对应你操作系统的最新版本安装程序。通常,你可以https://nodejs.org/en/download找到你需要的版本,比如:v18.20.2LTS双击打开下载的安装程序,并按照安装向导的指示进行安装。如果你已经安装了Node.js,安装程序通常会提供一个“U centos7中LNMP架构部署 m0_45318174 linux 一.环境规划IP角色192.168.19.152PHP脚本程序解析192.168.19.20Web服务器192.168.19.153数据库节点系统采用:[root@nebulalinux~]#cat/etc/redhat-releaseCentOSLinuxrelease7.6.1810(Core)软件版本:nginx:nginx/1.15.0php:PHP7.4.8mysql:8.0.16处理防 手机号码归属地的实现 dev.null 服务器运维 手机号码归属地查询一般可以通过以下几种方式实现:1.使用公开的号码归属地数据库可以使用国内的手机号码归属地数据库,如:百度号码归属地开放API阿里云号码归属地API腾讯号码归属地API你可以在本地存储一个CSV或SQLite数据库,定期更新,查询时直接匹配。2.调用第三方API许多服务商提供号码归属地查询API,例如:https://tcc.taobao.com/cc/json/mobile_te PHP,安卓,UI,java,linux视频教程合集 cocos2d-x小菜 javaUIlinuxPHPandroid ╔-----------------------------------╗┆ zookeeper admin 笔记 braveCS zookeeper Required Software 1) JDK>=1.6 2)推荐使用ensemble的ZooKeeper(至少3台),并run on separate machines 3)在Yahoo!,zk配置在特定的RHEL boxes里,2个cpu,2G内存,80G硬盘 数据和日志目录 1)数据目录里的文件是zk节点的持久化备份,包括快照和事务日 Spring配置多个连接池 easterfly spring 项目中需要同时连接多个数据库的时候,如何才能在需要用到哪个数据库就连接哪个数据库呢? Spring中有关于dataSource的配置: <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" &nb Mysql 171815164 mysql 例如,你想myuser使用mypassword从任何主机连接到mysql服务器的话。 GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'%'IDENTIFIED BY 'mypassword' WI TH GRANT OPTION; 如果你想允许用户myuser从ip为192.168.1.6的主机连接到mysql服务器,并使用mypassword作 CommonDAO(公共/基础DAO) g21121 DAO 好久没有更新博客了,最近一段时间工作比较忙,所以请见谅,无论你是爱看呢还是爱看呢还是爱看呢,总之或许对你有些帮助。 DAO(Data Access Object)是一个数据访问(顾名思义就是与数据库打交道)接口,DAO一般在业 直言有讳 永夜-极光 感悟随笔 1.转载地址:http://blog.csdn.net/jasonblog/article/details/10813313 精华: “直言有讳”是阿里巴巴提倡的一种观念,而我在此之前并没有很深刻的认识。为什么呢?就好比是读书时候做阅读理解,我喜欢我自己的解读,并不喜欢老师给的意思。在这里也是。我自己坚持的原则是互相尊重,我觉得阿里巴巴很多价值观其实是基本的做人 安装CentOS 7 和Win 7后,Win7 引导丢失 随便小屋 centos 一般安装双系统的顺序是先装Win7,然后在安装CentOS,这样CentOS可以引导WIN 7启动。但安装CentOS7后,却找不到Win7 的引导,稍微修改一点东西即可。 一、首先具有root 的权限。 即进入Terminal后输入命令su,然后输入密码即可 二、利用vim编辑器打开/boot/grub2/grub.cfg文件进行修改 v Oracle备份与恢复案例 aijuans oracle Oracle备份与恢复案例 一. 理解什么是数据库恢复当我们使用一个数据库时,总希望数据库的内容是可靠的、正确的,但由于计算机系统的故障(硬件故障、软件故障、网络故障、进程故障和系统故障)影响数据库系统的操作,影响数据库中数据的正确性,甚至破坏数据库,使数据库中全部或部分数据丢失。因此当发生上述故障后,希望能重构这个完整的数据库,该处理称为数据库恢复。恢复过程大致可以分为复原(Restore)与 JavaEE开源快速开发平台G4Studio v5.0发布 無為子 我非常高兴地宣布,今天我们最新的JavaEE开源快速开发平台G4Studio_V5.0版本已经正式发布。 访问G4Studio网站 http://www.g4it.org 2013-04-06 发布G4Studio_V5.0版本 功能新增 (1). 新增了调用Oracle存储过程返回游标,并将游标映射为Java List集合对象的标 Oracle显示根据高考分数模拟录取 百合不是茶 PL/SQL编程oracle例子模拟高考录取学习交流 题目要求: 1,创建student表和result表 2,pl/sql对学生的成绩数据进行处理 3,处理的逻辑是根据每门专业课的最低分线和总分的最低分数线自动的将录取和落选 1,创建student表,和result表 学生信息表; create table student( student_id number primary key,--学生id 优秀的领导与差劲的领导 bijian1013 领导管理团队 责任 优秀的领导:优秀的领导总是对他所负责的项目担负起责任。如果项目不幸失败了,那么他知道该受责备的人是他自己,并且敢于承认错误。 差劲的领导:差劲的领导觉得这不是他的问题,因此他会想方设法证明是他的团队不行,或是将责任归咎于团队中他不喜欢的那几个成员身上。 努力工作 优秀的领导:团队领导应该是团队成员的榜样。至少,他应该与团队中的其他成员一样努力工作。这仅仅因为他 js函数在浏览器下的兼容 Bill_chen jquery浏览器IEDWRext 做前端开发的工程师,少不了要用FF进行测试,纯js函数在不同浏览器下,名称也可能不同。对于IE6和FF,取得下一结点的函数就不尽相同: IE6:node.nextSibling,对于FF是不能识别的; FF:node.nextElementSibling,对于IE是不能识别的; 兼容解决方式:var Div = node.nextSibl 【JVM四】老年代垃圾回收:吞吐量垃圾收集器(Throughput GC) bit1129 垃圾回收 吞吐量与用户线程暂停时间 衡量垃圾回收算法优劣的指标有两个: 吞吐量越高,则算法越好 暂停时间越短,则算法越好 首先说明吞吐量和暂停时间的含义。 垃圾回收时,JVM会启动几个特定的GC线程来完成垃圾回收的任务,这些GC线程与应用的用户线程产生竞争关系,共同竞争处理器资源以及CPU的执行时间。GC线程不会对用户带来的任何价值,因此,好的GC应该占 J2EE监听器和过滤器基础 白糖_ J2EE Servlet程序由Servlet,Filter和Listener组成,其中监听器用来监听Servlet容器上下文。 监听器通常分三类:基于Servlet上下文的ServletContex监听,基于会话的HttpSession监听和基于请求的ServletRequest监听。 ServletContex监听器 ServletContex又叫application 博弈AngularJS讲义(16) - 提供者 boyitech jsAngularJSapiAngularProvider Angular框架提供了强大的依赖注入机制,这一切都是有注入器(injector)完成. 注入器会自动实例化服务组件和符合Angular API规则的特殊对象,例如控制器,指令,过滤器动画等。 那注入器怎么知道如何去创建这些特殊的对象呢? Angular提供了5种方式让注入器创建对象,其中最基础的方式就是提供者(provider), 其余四种方式(Value, Fac java-写一函数f(a,b),它带有两个字符串参数并返回一串字符,该字符串只包含在两个串中都有的并按照在a中的顺序。 bylijinnan java public class CommonSubSequence { /** * 题目:写一函数f(a,b),它带有两个字符串参数并返回一串字符,该字符串只包含在两个串中都有的并按照在a中的顺序。 * 写一个版本算法复杂度O(N^2)和一个O(N) 。 * * O(N^2):对于a中的每个字符,遍历b中的每个字符,如果相同,则拷贝到新字符串中。 * O( sqlserver 2000 无法验证产品密钥 Chen.H sqlwindowsSQL ServerMicrosoft 在 Service Pack 4 (SP 4), 是运行 Microsoft Windows Server 2003、 Microsoft Windows Storage Server 2003 或 Microsoft Windows 2000 服务器上您尝试安装 Microsoft SQL Server 2000 通过卷许可协议 (VLA) 媒体。 这样做, 收到以下错误信息CD KEY的 SQ [新概念武器]气象战争 comsci 气象战争的发动者必须是拥有发射深空航天器能力的国家或者组织.... 原因如下: 地球上的气候变化和大气层中的云层涡旋场有密切的关系,而维持一个在大气层某个层次 oracle 中 rollup、cube、grouping 使用详解 daizj oraclegroupingrollupcube oracle 中 rollup、cube、grouping 使用详解 -- 使用oracle 样例表演示 转自namesliu -- 使用oracle 的样列库,演示 rollup, cube, grouping 的用法与使用场景 --- ROLLUP , 为了理解分组的成员数量,我增加了 分组的计数 COUNT(SAL) 技术资料汇总分享 Dead_knight 技术资料汇总 分享 本人汇总的技术资料,分享出来,希望对大家有用。 http://pan.baidu.com/s/1jGr56uE 资料主要包含: Workflow->工作流相关理论、框架(OSWorkflow、JBPM、Activiti、fireflow...) Security->java安全相关资料(SSL、SSO、SpringSecurity、Shiro、JAAS...) Ser 初一下学期难记忆单词背诵第一课 dcj3sjt126com englishword could 能够 minute 分钟 Tuesday 星期二 February 二月 eighteenth 第十八 listen 听 careful 小心的,仔细的 short 短的 heavy 重的 empty 空的 certainly 当然 carry 携带;搬运 tape 磁带 basket 蓝子 bottle 瓶 juice 汁,果汁 head 头;头部 截取视图的图片, 然后分享出去 dcj3sjt126com OSObjective-C OS 7 has a new method that allows you to draw a view hierarchy into the current graphics context. This can be used to get an UIImage very fast. I implemented a category method on UIView to get the vi MySql重置密码 fanxiaolong MySql重置密码 方法一: 在my.ini的[mysqld]字段加入: skip-grant-tables 重启mysql服务,这时的mysql不需要密码即可登录数据库 然后进入mysql mysql>use mysql; mysql>更新 user set password=password('新密码') WHERE User='root'; mysq Ehcache(03)——Ehcache中储存缓存的方式 234390216 ehcacheMemoryStoreDiskStore存储驱除策略 Ehcache中储存缓存的方式 目录 1 堆内存(MemoryStore) 1.1 指定可用内存 1.2 驱除策略 1.3 元素过期 2 &nbs spring mvc中的@propertysource jackyrong spring mvc 在spring mvc中,在配置文件中的东西,可以在java代码中通过注解进行读取了: @PropertySource 在spring 3.1中开始引入 比如有配置文件 config.properties mongodb.url=1.2.3.4 mongodb.db=hello 则代码中 @PropertySource(& 重学单例模式 lanqiu17 单例Singleton模式 最近在重新学习设计模式,感觉对模式理解更加深刻。觉得有必要记下来。 第一个学的就是单例模式,单例模式估计是最好理解的模式了。它的作用就是防止外部创建实例,保证只有一个实例。 单例模式的常用实现方式有两种,就人们熟知的饱汉式与饥汉式,具体就不多说了。这里说下其他的实现方式 静态内部类方式: package test.pattern.singleton.statics; publ .NET开源核心运行时,且行且珍惜 netcome java.net开源 背景 2014年11月12日,ASP.NET之父、微软云计算与企业级产品工程部执行副总裁Scott Guthrie,在Connect全球开发者在线会议上宣布,微软将开源全部.NET核心运行时,并将.NET 扩展为可在 Linux 和 Mac OS 平台上运行。.NET核心运行时将基于MIT开源许可协议发布,其中将包括执行.NET代码所需的一切项目——CLR、JIT编译器、垃圾收集器(GC)和核心 使用oscahe缓存技术减少与数据库的频繁交互 Everyday都不同 Web高并发oscahe缓存 此前一直不知道缓存的具体实现,只知道是把数据存储在内存中,以便下次直接从内存中读取。对于缓存的使用也没有概念,觉得缓存技术是一个比较”神秘陌生“的领域。但最近要用到缓存技术,发现还是很有必要一探究竟的。 缓存技术使用背景:一般来说,对于web项目,如果我们要什么数据直接jdbc查库好了,但是在遇到高并发的情形下,不可能每一次都是去查数据库,因为这样在高并发的情形下显得不太合理—— Spring+Mybatis 手动控制事务 toknowme mybatis @Override public boolean testDelete(String jobCode) throws Exception { boolean flag = false; &nbs 菜鸟级的android程序员面试时候需要掌握的知识点 xp9802 android 熟悉Android开发架构和API调用 掌握APP适应不同型号手机屏幕开发技巧 熟悉Android下的数据存储 熟练Android Debug Bridge Tool 熟练Eclipse/ADT及相关工具 熟悉Android框架原理及Activity生命周期 熟练进行Android UI布局 熟练使用SQLite数据库; 熟悉Android下网络通信机制,S 按字母分类: ABCDEFGHIJKLMNOPQRSTUVWXYZ其他
得到的结果,只封装了第一列的id
查询满足条件的总数,注意,使用这个方法,不能调用QueryWrapper的select方法设置要查询的列了。这个方法会自动添加select count(1)
QueryWrapper
select
select count(1)
@Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like("name", "黄"); Integer count = userMapper.selectCount(wrapper); System.out.println(count); }
另外一套CRUD是Service层的,只需要编写一个接口,继承IService,并创建一个接口实现类,即可食用。(这个接口提供的CRUD方法,和Mapper接口提供的功能大同小异,比较明显的区别在于IService支持了更多的批量化操作,如saveBatch,saveOrUpdateBatch等方法。
IService
saveBatch
saveOrUpdateBatch
食用示例如下
1.首先,新建一个接口,继承IService
package com.example.mp.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.mp.po.User; public interface UserService extends IService { }
2.创建这个接口的实现类,并继承ServiceImpl,最后打上@Service注解,注册到Spring容器中,即可食用
ServiceImpl
@Service
package com.example.mp.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.mp.mappers.UserMapper; import com.example.mp.po.User; import com.example.mp.service.UserService; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl implements UserService { }
3.测试代码
package com.example.mp; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.example.mp.po.User; import com.example.mp.service.UserService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class ServiceTest { @Autowired private UserService userService; @Test public void testGetOne() { LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); wrapper.gt(User::getAge, 28); User one = userService.getOne(wrapper, false); // 第二参数指定为false,使得在查到了多行记录时,不抛出异常,而返回第一条记录 System.out.println(one); } }
4.结果
另,IService也支持链式调用,代码写起来非常简洁,查询示例如下
@Test public void testChain() { List list = userService.lambdaQuery() .gt(User::getAge, 39) .likeRight(User::getName, "王") .list(); list.forEach(System.out::println); }
更新示例如下
@Test public void testChain() { userService.lambdaUpdate() .gt(User::getAge, 39) .likeRight(User::getName, "王") .set(User::getEmail, "w39@baomidou.com") .update(); }
删除示例如下
@Test public void testChain() { userService.lambdaUpdate() .like(User::getName, "青蛙") .remove(); }
mp让我觉得极其方便的一点在于其提供了强大的条件构造器Wrapper,可以非常方便的构造WHERE条件。条件构造器主要涉及到3个类,AbstractWrapper。QueryWrapper,UpdateWrapper,它们的类关系如下
AbstractWrapper
UpdateWrapper
在AbstractWrapper中提供了非常多的方法用于构建WHERE条件,而QueryWrapper针对SELECT语句,提供了select()方法,可自定义需要查询的列,而UpdateWrapper针对UPDATE语句,提供了set()方法,用于构造set语句。条件构造器也支持lambda表达式,写起来非常舒爽。
SELECT
select()
UPDATE
set()
set
下面对AbstractWrapper中用于构建SQL语句中的WHERE条件的方法进行部分列举
eq:equals,等于
eq
allEq:all equals,全等于
allEq
ne:not equals,不等于
ne
gt:greater than ,大于 >
gt
>
ge:greater than or equals,大于等于≥
ge
≥
lt:less than,小于<
lt
<
le:less than or equals,小于等于≤
le
≤
between:相当于SQL中的BETWEEN
between
notBetween
like:模糊匹配。like("name","黄"),相当于SQL的name like '%黄%'
like
like("name","黄")
name like '%黄%'
likeRight:模糊匹配右半边。likeRight("name","黄"),相当于SQL的name like '黄%'
likeRight
likeRight("name","黄")
name like '黄%'
likeLeft:模糊匹配左半边。likeLeft("name","黄"),相当于SQL的name like '%黄'
likeLeft
likeLeft("name","黄")
name like '%黄'
notLike:notLike("name","黄"),相当于SQL的name not like '%黄%'
notLike
notLike("name","黄")
name not like '%黄%'
isNull
isNotNull
in
and:SQL连接符AND
and
or:SQL连接符OR
or
apply:用于拼接SQL,该方法可用于数据库函数,并可以动态传参
apply
.......
下面通过一些具体的案例来练习条件构造器的使用。(使用前文创建的user表)
user
// 案例先展示需要完成的SQL语句,后展示Wrapper的写法 // 1. 名字中包含佳,且年龄小于25 // SELECT * FROM user WHERE name like '%佳%' AND age < 25 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like("name", "佳").lt("age", 25); List users = userMapper.selectList(wrapper); // 下面展示SQL时,仅展示WHERE条件;展示代码时, 仅展示Wrapper构建部分 // 2. 姓名为黄姓,且年龄大于等于20,小于等于40,且email字段不为空 // name like '黄%' AND age BETWEEN 20 AND 40 AND email is not null wrapper.likeRight("name","黄").between("age", 20, 40).isNotNull("email"); // 3. 姓名为黄姓,或者年龄大于等于40,按照年龄降序排列,年龄相同则按照id升序排列 // name like '黄%' OR age >= 40 order by age desc, id asc wrapper.likeRight("name","黄").or().ge("age",40).orderByDesc("age").orderByAsc("id"); // 4.创建日期为2021年3月22日,并且直属上级的名字为李姓 // date_format(create_time,'%Y-%m-%d') = '2021-03-22' AND manager_id IN (SELECT id FROM user WHERE name like '李%') wrapper.apply("date_format(create_time, '%Y-%m-%d') = {0}", "2021-03-22") // 建议采用{index}这种方式动态传参, 可防止SQL注入 .inSql("manager_id", "SELECT id FROM user WHERE name like '李%'"); // 上面的apply, 也可以直接使用下面这种方式做字符串拼接,但当这个日期是一个外部参数时,这种方式有SQL注入的风险 wrapper.apply("date_format(create_time, '%Y-%m-%d') = '2021-03-22'"); // 5. 名字为王姓,并且(年龄小于40,或者邮箱不为空) // name like '王%' AND (age < 40 OR email is not null) wrapper.likeRight("name", "王").and(q -> q.lt("age", 40).or().isNotNull("email")); // 6. 名字为王姓,或者(年龄小于40并且年龄大于20并且邮箱不为空) // name like '王%' OR (age < 40 AND age > 20 AND email is not null) wrapper.likeRight("name", "王").or( q -> q.lt("age",40) .gt("age",20) .isNotNull("email") ); // 7. (年龄小于40或者邮箱不为空) 并且名字为王姓 // (age < 40 OR email is not null) AND name like '王%' wrapper.nested(q -> q.lt("age", 40).or().isNotNull("email")) .likeRight("name", "王"); // 8. 年龄为30,31,34,35 // age IN (30,31,34,35) wrapper.in("age", Arrays.asList(30,31,34,35)); // 或 wrapper.inSql("age","30,31,34,35"); // 9. 年龄为30,31,34,35, 返回满足条件的第一条记录 // age IN (30,31,34,35) LIMIT 1 wrapper.in("age", Arrays.asList(30,31,34,35)).last("LIMIT 1"); // 10. 只选出id, name 列 (QueryWrapper 特有) // SELECT id, name FROM user; wrapper.select("id", "name"); // 11. 选出id, name, age, email, 等同于排除 manager_id 和 create_time // 当列特别多, 而只需要排除个别列时, 采用上面的方式可能需要写很多个列, 可以采用重载的select方法,指定需要排除的列 wrapper.select(User.class, info -> { String columnName = info.getColumn(); return !"create_time".equals(columnName) && !"manager_id".equals(columnName); });
条件构造器的诸多方法中,均可以指定一个boolean类型的参数condition,用来决定该条件是否加入最后生成的WHERE语句中,比如
boolean
condition
String name = "黄"; // 假设name变量是一个外部传入的参数 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like(StringUtils.hasText(name), "name", name); // 仅当 StringUtils.hasText(name) 为 true 时, 会拼接这个like语句到WHERE中 // 其实就是对下面代码的简化 if (StringUtils.hasText(name)) { wrapper.like("name", name); }
调用构造函数创建一个Wrapper对象时,可以传入一个实体对象。后续使用这个Wrapper时,会以实体对象中的非空属性,构建WHERE条件(默认构建等值匹配的WHERE条件,这个行为可以通过实体类里各个字段上的@TableField注解中的condition属性进行改变)
示例如下
@Test public void test3() { User user = new User(); user.setName("黄主管"); user.setAge(28); QueryWrapper wrapper = new QueryWrapper<>(user); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
执行结果如下。可以看到,是根据实体对象中的非空属性,进行了等值匹配查询。
若希望针对某些属性,改变等值匹配的行为,则可以在实体类中用@TableField注解进行配置,示例如下
package com.example.mp.po; import com.baomidou.mybatisplus.annotation.SqlCondition; import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import java.time.LocalDateTime; @Data public class User { private Long id; @TableField(condition = SqlCondition.LIKE) // 配置该字段使用like进行拼接 private String name; private Integer age; private String email; private Long managerId; private LocalDateTime createTime; }
运行下面的测试代码
@Test public void test3() { User user = new User(); user.setName("黄"); QueryWrapper wrapper = new QueryWrapper<>(user); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
从下图得到的结果来看,对于实体对象中的name字段,采用了like进行拼接
name
@TableField中配置的condition属性实则是一个字符串,SqlCondition类中预定义了一些字符串以供选择
SqlCondition
package com.baomidou.mybatisplus.annotation; public class SqlCondition { //下面的字符串中, %s 是占位符, 第一个 %s 是列名, 第二个 %s 是列的值 public static final String EQUAL = "%s=#{%s}"; public static final String NOT_EQUAL = "%s<>#{%s}"; public static final String LIKE = "%s LIKE CONCAT('%%',#{%s},'%%')"; public static final String LIKE_LEFT = "%s LIKE CONCAT('%%',#{%s})"; public static final String LIKE_RIGHT = "%s LIKE CONCAT(#{%s},'%%')"; }
SqlCondition中提供的配置比较有限,当我们需要<或>等拼接方式,则需要自己定义。比如
package com.example.mp.po; import com.baomidou.mybatisplus.annotation.SqlCondition; import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import java.time.LocalDateTime; @Data public class User { private Long id; @TableField(condition = SqlCondition.LIKE) private String name; @TableField(condition = "%s > #{%s}") // 这里相当于大于, 其中 > 是字符实体 private Integer age; private String email; private Long managerId; private LocalDateTime createTime; }
测试如下
@Test public void test3() { User user = new User(); user.setName("黄"); user.setAge(30); QueryWrapper wrapper = new QueryWrapper<>(user); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
从下图得到的结果,可以看出,name属性是用like拼接的,而age属性是用>拼接的
age
allEq方法传入一个map,用来做等值匹配
map
@Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); Map param = new HashMap<>(); param.put("age", 40); param.put("name", "黄飞飞"); wrapper.allEq(param); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
当allEq方法传入的Map中有value为null的元素时,默认会设置为is null
null
is null
@Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); Map param = new HashMap<>(); param.put("age", 40); param.put("name", null); wrapper.allEq(param); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
若想忽略map中value为null的元素,可以在调用allEq时,设置参数boolean null2IsNull为false
boolean null2IsNull
@Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); Map param = new HashMap<>(); param.put("age", 40); param.put("name", null); wrapper.allEq(param, false); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
若想要在执行allEq时,过滤掉Map中的某些元素,可以调用allEq的重载方法allEq(BiPredicate filter, Map params)
allEq(BiPredicate filter, Map params)
@Test public void test3() { QueryWrapper wrapper = new QueryWrapper<>(); Map param = new HashMap<>(); param.put("age", 40); param.put("name", "黄飞飞"); wrapper.allEq((k,v) -> !"name".equals(k), param); // 过滤掉map中key为name的元素 List users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
lambda条件构造器,支持lambda表达式,可以不必像普通条件构造器一样,以字符串形式指定列名,它可以直接以实体类的方法引用来指定列。示例如下
@Test public void testLambda() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.like(User::getName, "黄").lt(User::getAge, 30); List users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
像普通的条件构造器,列名是用字符串的形式指定,无法在编译期进行列名合法性的检查,这就不如lambda条件构造器来的优雅。
另外,还有个链式lambda条件构造器,使用示例如下
@Test public void testLambda() { LambdaQueryChainWrapper chainWrapper = new LambdaQueryChainWrapper<>(userMapper); List users = chainWrapper.like(User::getName, "黄").gt(User::getAge, 30).list(); users.forEach(System.out::println); }
上面介绍的都是查询操作,现在来讲更新和删除操作。
BaseMapper中提供了2个更新方法
根据入参entity的id(主键)进行更新,对于entity中非空的属性,会出现在UPDATE语句的SET后面,即entity中非空的属性,会被更新到数据库,示例如下
entity
id
@RunWith(SpringRunner.class) @SpringBootTest public class UpdateTest { @Autowired private UserMapper userMapper; @Test public void testUpdate() { User user = new User(); user.setId(2L); user.setAge(18); userMapper.updateById(user); } }
根据实体entity和条件构造器wrapper进行更新,示例如下
@Test public void testUpdate2() { User user = new User(); user.setName("王三蛋"); LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); wrapper.between(User::getAge, 26,31).likeRight(User::getName,"吴"); userMapper.update(user, wrapper); }
额外演示一下,把实体对象传入Wrapper,即用实体对象构造WHERE条件的案例
@Test public void testUpdate3() { User whereUser = new User(); whereUser.setAge(40); whereUser.setName("王"); LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(whereUser); User user = new User(); user.setEmail("share@baomidou.com"); user.setManagerId(10L); userMapper.update(user, wrapper); }
注意到我们的User类中,对name属性和age属性进行了如下的设置
@Data public class User { private Long id; @TableField(condition = SqlCondition.LIKE) private String name; @TableField(condition = "%s > #{%s}") private Integer age; private String email; private Long managerId; private LocalDateTime createTime; }
执行结果
再额外演示一下,链式lambda条件构造器的使用
@Test public void testUpdate5() { LambdaUpdateChainWrapper wrapper = new LambdaUpdateChainWrapper<>(userMapper); wrapper.likeRight(User::getEmail, "share") .like(User::getName, "飞飞") .set(User::getEmail, "ff@baomidou.com") .update(); }
反思
由于BaseMapper提供的2个更新方法都是传入一个实体对象去执行更新,这在需要更新的列比较多时还好,若想要更新的只有那么一列,或者两列,则创建一个实体对象就显得有点麻烦。针对这种情况,UpdateWrapper提供有set方法,可以手动拼接SQL中的SET语句,此时可以不必传入实体对象,示例如下
@Test public void testUpdate4() { LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); wrapper.likeRight(User::getEmail, "share").set(User::getManagerId, 9L); userMapper.update(null, wrapper); }
BaseMapper一共提供了如下几个用于删除的方法
deleteById 根据主键id进行删除
deleteById
deleteBatchIds 根据主键id进行批量删除
deleteBatchIds
deleteByMap 根据Map进行删除(Map中的key为列名,value为值,根据列和值进行等值匹配)
deleteByMap
delete(Wrapper wrapper) 根据条件构造器Wrapper进行删除
与前面查询和更新的操作大同小异,不做赘述
当mp提供的方法还不能满足需求时,则可以自定义SQL。
注解方式
package com.example.mp.mappers; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @Author yogurtzzz * @Date 2021/3/18 11:21 **/ public interface UserMapper extends BaseMapper { @Select("select * from user") List selectRaw(); }
xml方式
SELECT * FROM user
package com.example.mp.mappers; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User; import org.apache.ibatis.annotations.Select; import java.util.List; public interface UserMapper extends BaseMapper { List selectRaw(); }
使用xml时,若xml文件与mapper接口文件不在同一目录下,则需要在application.yml中配置mapper.xml的存放路径
application.yml
mybatis-plus: mapper-locations: /mappers/*
若有多个地方存放mapper,则用数组形式进行配置
mybatis-plus: mapper-locations: - /mappers/* - /com/example/mp/*
测试代码如下
@Test public void testCustomRawSql() { List users = userMapper.selectRaw(); users.forEach(System.out::println); }
结果
也可以使用mp提供的Wrapper条件构造器,来自定义SQL
package com.example.mp.mappers; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.example.mp.po.User; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; public interface UserMapper extends BaseMapper { // SQL中不写WHERE关键字,且固定使用${ew.customSqlSegment} @Select("select * from user ${ew.customSqlSegment}") List findAll(@Param(Constants.WRAPPER)Wrapper wrapper); }
package com.example.mp.mappers; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User; import java.util.List; public interface UserMapper extends BaseMapper { List findAll(Wrapper wrapper); }
SELECT * FROM user ${ew.customSqlSegment}
BaseMapper中提供了2个方法进行分页查询,分别是selectPage和selectMapsPage,前者会将查询的结果封装成Java实体对象,后者会封装成Map。分页查询的食用示例如下
selectPage
selectMapsPage
Map
1. 创建mp的分页拦截器,注册到Spring容器中
package com.example.mp.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { /** 新版mp **/ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } /** 旧版mp 用 PaginationInterceptor **/ }
2. 执行分页查询
@Test public void testPage() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.ge(User::getAge, 28); // 设置分页信息, 查第3页, 每页2条数据 Page page = new Page<>(3, 2); // 执行分页查询 Page userPage = userMapper.selectPage(page, wrapper); System.out.println("总记录数 = " + userPage.getTotal()); System.out.println("总页数 = " + userPage.getPages()); System.out.println("当前页码 = " + userPage.getCurrent()); // 获取分页查询结果 List records = userPage.getRecords(); records.forEach(System.out::println); }
3. 结果
4. 其他
注意到,分页查询总共发出了2次SQL,一次查总记录数,一次查具体数据。若希望不查总记录数,仅查分页结果。可以通过Page的重载构造函数,指定isSearchCount为false即可
Page
isSearchCount
public Page(long current, long size, boolean isSearchCount)
在实际开发中,可能遇到多表联查的场景,此时BaseMapper中提供的单表分页查询的方法无法满足需求,需要自定义SQL,示例如下(使用单表查询的SQL进行演示,实际进行多表联查时,修改SQL语句即可)
1. 在mapper接口中定义一个函数,接收一个Page对象为参数,并编写自定义SQL
// 这里采用纯注解方式。当然,若SQL比较复杂,建议还是采用XML的方式 @Select("SELECT * FROM user ${ew.customSqlSegment}") Page selectUserPage(Page page, @Param(Constants.WRAPPER) Wrapper wrapper);
2. 执行查询
@Test public void testPage2() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.ge(User::getAge, 28).likeRight(User::getName, "王"); Page page = new Page<>(3,2); Page userPage = userMapper.selectUserPage(page, wrapper); System.out.println("总记录数 = " + userPage.getTotal()); System.out.println("总页数 = " + userPage.getPages()); userPage.getRecords().forEach(System.out::println); }
ActiveRecord模式,通过操作实体对象,直接操作数据库表。与ORM有点类似。
让实体类User继承自Model
Model
package com.example.mp.po; import com.baomidou.mybatisplus.annotation.SqlCondition; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.extension.activerecord.Model; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; @EqualsAndHashCode(callSuper = false) @Data public class User extends Model { private Long id; @TableField(condition = SqlCondition.LIKE) private String name; @TableField(condition = "%s > #{%s}") private Integer age; private String email; private Long managerId; private LocalDateTime createTime; }
直接调用实体对象上的方法
@Test public void insertAr() { User user = new User(); user.setId(15L); user.setName("我是AR猪"); user.setAge(1); user.setEmail("ar@baomidou.com"); user.setManagerId(1L); boolean success = user.insert(); // 插入 System.out.println(success); }
其他示例
// 查询 @Test public void selectAr() { User user = new User(); user.setId(15L); User result = user.selectById(); System.out.println(result); } // 更新 @Test public void updateAr() { User user = new User(); user.setId(15L); user.setName("王全蛋"); user.updateById(); } //删除 @Test public void deleteAr() { User user = new User(); user.setId(15L); user.deleteById(); }
在定义实体类时,用@TableId指定主键,而其type属性,可以指定主键策略。
mp支持多种主键策略,默认的策略是基于雪花算法的自增id。全部主键策略定义在了枚举类IdType中,IdType有如下的取值
IdType
AUTO
数据库ID自增,依赖于数据库。在插入操作生成SQL语句时,不会插入主键这一列
NONE
未设置主键类型。若在代码中没有手动设置主键,则会根据主键的全局策略自动生成(默认的主键全局策略是基于雪花算法的自增ID)
INPUT
需要手动设置主键,若不设置。插入操作生成SQL语句时,主键这一列的值会是null。oracle的序列主键需要使用这种方式
ASSIGN_ID
当没有手动设置主键,即实体类中的主键属性为空时,才会自动填充,使用雪花算法
ASSIGN_UUID
当实体类的主键属性为空时,才会自动填充,使用UUID
....(还有几种是已过时的,就不再列举)
可以针对每个实体类,使用@TableId注解指定该实体类的主键策略,这可以理解为局部策略。若希望对所有的实体类,都采用同一种主键策略,挨个在每个实体类上进行配置,则太麻烦了,此时可以用主键的全局策略。只需要在application.yml进行配置即可。比如,配置了全局采用自增主键策略
# application.yml mybatis-plus: global-config: db-config: id-type: auto
下面对不同主键策略的行为进行演示
在User上对id属性加上注解,然后将MYSQL的user表修改其主键为自增。
@EqualsAndHashCode(callSuper = false) @Data public class User extends Model { @TableId(type = IdType.AUTO) private Long id; @TableField(condition = SqlCondition.LIKE) private String name; @TableField(condition = "%s > #{%s}") private Integer age; private String email; private Long managerId; private LocalDateTime createTime; }
测试
@Test public void testAuto() { User user = new User(); user.setName("我是青蛙呱呱"); user.setAge(99); user.setEmail("frog@baomidou.com"); user.setCreateTime(LocalDateTime.now()); userMapper.insert(user); System.out.println(user.getId()); }
可以看到,代码中没有设置主键ID,发出的SQL语句中也没有设置主键ID,并且插入结束后,主键ID会被写回到实体对象。
在MYSQL的user表中,去掉主键自增。然后修改User类(若不配置@TableId注解,默认主键策略也是NONE)
@TableId(type = IdType.NONE) private Long id;
插入时,若实体类的主键ID有值,则使用之;若主键ID为空,则使用主键全局策略,来生成一个ID。
其余的策略类似,不赘述
小结
AUTO依赖于数据库的自增主键,插入时,实体对象无需设置主键,插入成功后,主键会被写回实体对象。
INPUT完全依赖于用户输入。实体对象中主键ID是什么,插入到数据库时就设置什么。若有值便设置值,若为null则设置null
其余的几个策略,都是在实体对象中主键ID为空时,才会自动生成。
NONE会跟随全局策略,ASSIGN_ID采用雪花算法,ASSIGN_UUID采用UUID
全局配置,在application.yml中进行即可;针对单个实体类的局部配置,使用@TableId即可。对于某个实体类,若它有局部主键策略,则采用之,否则,跟随全局策略。
mybatis plus有许多可配置项,可在application.yml中进行配置,如上面的全局主键策略。下面列举部分配置项
configLocation:若有单独的mybatis配置,用这个注解指定mybatis的配置文件(mybatis的全局配置文件)
configLocation
mapperLocations:mybatis mapper所对应的xml文件的位置
mapperLocations
typeAliasesPackage:mybatis的别名包扫描路径
typeAliasesPackage
.....
mapUnderscoreToCamelCase:是否开启自动驼峰命名规则映射。(默认开启)
mapUnderscoreToCamelCase
dbTpe:数据库类型。一般不用配,会根据数据库连接url自动识别
dbTpe
fieldStrategy:(已过时)字段验证策略。该配置项在最新版的mp文档中已经找不到了,被细分成了insertStrategy,updateStrategy,selectStrategy。默认值是NOT_NULL,即对于实体对象中非空的字段,才会组装到最终的SQL语句中。
fieldStrategy
selectStrategy
NOT_NULL
有如下几种可选配置
这个配置项,可在application.yml中进行全局配置,也可以在某一实体类中,对某一字段用@TableField注解进行局部配置
这个字段验证策略有什么用呢?在UPDATE操作中能够体现出来,若用一个User对象执行UPDATE操作,我们希望只对User对象中非空的属性,更新到数据库中,其他属性不做更新,则NOT_NULL可以满足需求。
而若updateStrategy配置为IGNORED,则不会进行非空判断,会将实体对象中的全部属性如实组装到SQL中,这样,执行UPDATE时,可能就将一些不想更新的字段,设置为了NULL。
IGNORED
NULL
IGNORED:忽略校验。即,不做校验。实体对象中的全部字段,无论值是什么,都如实地被组装到SQL语句中(为NULL的字段在SQL语句中就组装为NULL)。
NOT_NULL:非NULL校验。只会将非NULL的字段组装到SQL语句中
NOT_EMPTY:非空校验。当有字段是字符串类型时,只组装非空字符串;对其他类型的字段,等同于NOT_NULL
NOT_EMPTY
NEVER:不加入SQL。所有字段不加入到SQL语句
NEVER
tablePrefix:添加表名前缀
tablePrefix
mybatis-plus: global-config: db-config: table-prefix: xx_
然后将MYSQL中的表做一下修改。但Java实体类保持不变(仍然为User)。
可以看到拼接出来的SQL,在表名前面添加了前缀
mp提供一个生成器,可快速生成Entity实体类,Mapper接口,Service,Controller等全套代码。
public class GeneratorTest { @Test public void generate() { AutoGenerator generator = new AutoGenerator(); // 全局配置 GlobalConfig config = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); // 设置输出到的目录 config.setOutputDir(projectPath + "/src/main/java"); config.setAuthor("yogurt"); // 生成结束后是否打开文件夹 config.setOpen(false); // 全局配置添加到 generator 上 generator.setGlobalConfig(config); // 数据源配置 DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai"); dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver"); dataSourceConfig.setUsername("root"); dataSourceConfig.setPassword("root"); // 数据源配置添加到 generator generator.setDataSource(dataSourceConfig); // 包配置, 生成的代码放在哪个包下 PackageConfig packageConfig = new PackageConfig(); packageConfig.setParent("com.example.mp.generator"); // 包配置添加到 generator generator.setPackageInfo(packageConfig); // 策略配置 StrategyConfig strategyConfig = new StrategyConfig(); // 下划线驼峰命名转换 strategyConfig.setNaming(NamingStrategy.underline_to_camel); strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel); // 开启lombok strategyConfig.setEntityLombokModel(true); // 开启RestController strategyConfig.setRestControllerStyle(true); generator.setStrategy(strategyConfig); generator.setTemplateEngine(new FreemarkerTemplateEngine()); // 开始生成 generator.execute(); } }
运行后,可以看到生成了如下图所示的全套代码
高级功能的演示需要用到一张新的表user2
user2
DROP TABLE IF EXISTS user2; CREATE TABLE user2 ( id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键id', name VARCHAR(30) DEFAULT NULL COMMENT '姓名', age INT(11) DEFAULT NULL COMMENT '年龄', email VARCHAR(50) DEFAULT NULL COMMENT '邮箱', manager_id BIGINT(20) DEFAULT NULL COMMENT '直属上级id', create_time DATETIME DEFAULT NULL COMMENT '创建时间', update_time DATETIME DEFAULT NULL COMMENT '修改时间', version INT(11) DEFAULT '1' COMMENT '版本', deleted INT(1) DEFAULT '0' COMMENT '逻辑删除标识,0-未删除,1-已删除', CONSTRAINT manager_fk FOREIGN KEY(manager_id) REFERENCES user2(id) ) ENGINE = INNODB CHARSET=UTF8; INSERT INTO user2(id, name, age, email, manager_id, create_time) VALUES (1, '老板', 40 ,'boss@baomidou.com' ,NULL, '2021-03-28 13:12:40'), (2, '王狗蛋', 40 ,'gd@baomidou.com' ,1, '2021-03-28 13:12:40'), (3, '王鸡蛋', 40 ,'jd@baomidou.com' ,2, '2021-03-28 13:12:40'), (4, '王鸭蛋', 40 ,'yd@baomidou.com' ,2, '2021-03-28 13:12:40'), (5, '王猪蛋', 40 ,'zd@baomidou.com' ,2, '2021-03-28 13:12:40'), (6, '王软蛋', 40 ,'rd@baomidou.com' ,2, '2021-03-28 13:12:40'), (7, '王铁蛋', 40 ,'td@baomidou.com' ,2, '2021-03-28 13:12:40')
并创建对应的实体类User2
User2
package com.example.mp.po; import lombok.Data; import java.time.LocalDateTime; @Data public class User2 { private Long id; private String name; private Integer age; private String email; private Long managerId; private LocalDateTime createTime; private LocalDateTime updateTime; private Integer version; private Integer deleted; }
以及Mapper接口
package com.example.mp.mappers; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User2; public interface User2Mapper extends BaseMapper { }
首先,为什么要有逻辑删除呢?直接删掉不行吗?当然可以,但日后若想要恢复,或者需要查看这些数据,就做不到了。逻辑删除是为了方便数据恢复,和保护数据本身价值的一种方案。
日常中,我们在电脑中删除一个文件后,也仅仅是把该文件放入了回收站,日后若有需要还能进行查看或恢复。当我们确定不再需要某个文件,可以将其从回收站中彻底删除。这也是类似的道理。
mp提供的逻辑删除实现起来非常简单
只需要在application.yml中进行逻辑删除的相关配置即可
mybatis-plus: global-config: db-config: logic-delete-field: deleted # 全局逻辑删除的实体字段名 logic-delete-value: 1 # 逻辑已删除值(默认为1) logic-not-delete-value: 0 # 逻辑未删除值(默认为0) # 若逻辑已删除和未删除的值和默认值一样,则可以不配置这2项
测试代码
package com.example.mp; import com.example.mp.mappers.User2Mapper; import com.example.mp.po.User2; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; @RunWith(SpringRunner.class) @SpringBootTest public class LogicDeleteTest { @Autowired private User2Mapper mapper; @Test public void testLogicDel() { int i = mapper.deleteById(6); System.out.println("rowAffected = " + i); } }
可以看到,发出的SQL不再是DELETE,而是UPDATE
DELETE
此时我们再执行一次SELECT
@Test public void testSelect() { List users = mapper.selectList(null); }
可以看到,发出的SQL语句,会自动在WHERE后面拼接逻辑未删除的条件。查询出来的结果中,没有了id为6的王软蛋。
若想要SELECT的列,不包括逻辑删除的那一列,则可以在实体类中通过@TableField进行配置
@TableField(select = false) private Integer deleted;
可以看到下图的执行结果中,SELECT中已经不包含deleted这一列了
前面在application.yml中做的配置,是全局的。通常来说,对于多个表,我们也会统一逻辑删除字段的名称,统一逻辑已删除和未删除的值,所以全局配置即可。当然,若要对某些表进行单独配置,在实体类的对应字段上使用@TableLogic即可
@TableLogic(value = "0", delval = "1") private Integer deleted;
开启mp的逻辑删除后,会对SQL产生如下的影响
INSERT语句:没有影响
SELECT语句:追加WHERE条件,过滤掉已删除的数据
UPDATE语句:追加WHERE条件,防止更新到已删除的数据
DELETE语句:转变为UPDATE语句
注意,上述的影响,只针对mp自动注入的SQL生效。如果是自己手动添加的自定义SQL,则不会生效。比如
public interface User2Mapper extends BaseMapper { @Select("select * from user2") List selectRaw(); }
调用这个selectRaw,则mp的逻辑删除不会生效。
selectRaw
另,逻辑删除可在application.yml中进行全局配置,也可在实体类中用@TableLogic进行局部配置。
表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。比较原始的方式,是每次插入或更新时,手动进行设置。mp可以通过配置,对某些字段进行自动填充,食用示例如下
1. 在实体类中的某些字段上,通过@TableField设置自动填充
public class User2 { private Long id; private String name; private Integer age; private String email; private Long managerId; @TableField(fill = FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; @TableField(fill = FieldFill.UPDATE) // 更新时自动填充 private LocalDateTime updateTime; private Integer version; private Integer deleted; }
2. 实现自动填充处理器
package com.example.mp.component; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Component //需要注册到Spring容器中 public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 插入时自动填充 // 注意第二个参数要填写实体类中的字段名称,而不是表的列名称 strictFillStrategy(metaObject, "createTime", LocalDateTime::now); } @Override public void updateFill(MetaObject metaObject) { // 更新时自动填充 strictFillStrategy(metaObject, "updateTime", LocalDateTime::now); } }
@Test public void test() { User2 user = new User2(); user.setId(8L); user.setName("王一蛋"); user.setAge(29); user.setEmail("yd@baomidou.com"); user.setManagerId(2L); mapper.insert(user); }
根据下图结果,可以看到对createTime进行了自动填充
注意,自动填充仅在该字段为空时会生效,若该字段不为空,则直接使用已有的值。如下
@Test public void test() { User2 user = new User2(); user.setId(8L); user.setName("王一蛋"); user.setAge(29); user.setEmail("yd@baomidou.com"); user.setManagerId(2L); user.setCreateTime(LocalDateTime.of(2000,1,1,8,0,0)); mapper.insert(user); }
更新时的自动填充,测试如下
@Test public void test() { User2 user = new User2(); user.setId(8L); user.setName("王一蛋"); user.setAge(99); mapper.updateById(user); }
当出现并发操作时,需要确保各个用户对数据的操作不产生冲突,此时需要一种并发控制手段。悲观锁的方法是,在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作;而乐观锁,正如其名,它先假设不存在冲突情况,而在实际进行数据操作时,再检查是否冲突。乐观锁的一种通常实现是版本号,在MySQL中也有名为MVCC的基于版本号的并发事务控制。
在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。
在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。
乐观锁的实现如下:
取出记录时,获取当前version
更新时,带上这个version
执行更新时, set version = newVersion where version = oldVersion
如果oldVersion与数据库中的version不一致,就更新失败
这种思想和CAS(Compare And Swap)非常相似。
乐观锁的实现步骤如下
1. 配置乐观锁插件
package com.example.mp.config; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { /** 3.4.0以后的mp版本,推荐用如下的配置方式 **/ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } /** 旧版mp可以采用如下方式。注意新旧版本中,新版的类,名称带有Inner, 旧版的不带, 不要配错了 **/ /* @Bean public OptimisticLockerInterceptor opLocker() { return new OptimisticLockerInterceptor(); } */ }
2. 在实体类中表示版本的字段上添加注解@Version
@Data public class User2 { private Long id; private String name; private Integer age; private String email; private Long managerId; private LocalDateTime createTime; private LocalDateTime updateTime; @Version private Integer version; private Integer deleted; }
@Test public void testOpLocker() { int version = 1; // 假设这个version是先前查询时获得的 User2 user = new User2(); user.setId(8L); user.setEmail("version@baomidou.com"); user.setVersion(version); int i = mapper.updateById(user); }
执行之前先看一下数据库的情况
根据下图执行结果,可以看到SQL语句中添加了version相关的操作
当UPDATE返回了1,表示影响行数为1,则更新成功。反之,由于WHERE后面的version与数据库中的不一致,匹配不到任何记录,则影响行数为0,表示更新失败。更新成功后,新的version会被封装回实体对象中。
实体类中version字段,类型只支持int,long,Date,Timestamp,LocalDateTime
注意,乐观锁插件仅支持**updateById(id)与update(entity, wrapper)方法**
updateById(id)
update(entity, wrapper)
注意:如果使用wrapper,则wrapper不能复用!示例如下
@Test public void testOpLocker() { User2 user = new User2(); user.setId(8L); user.setVersion(1); user.setAge(2); // 第一次使用 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User2::getName, "王一蛋"); mapper.update(user, wrapper); // 第二次复用 user.setAge(3); mapper.update(user, wrapper); }
可以看到在第二次复用wrapper时,拼接出的SQL中,后面WHERE语句中出现了2次version,是有问题的。
该插件会输出SQL语句的执行时间,以便做SQL语句的性能分析和调优。
注:3.2.0版本之后,mp自带的性能分析插件被官方移除了,而推荐食用第三方性能分析插件
食用步骤
1. 引入maven依赖
p6spy p6spy 3.9.1
2. 修改application.yml
spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver #换成p6spy的驱动 url: jdbc:p6spy:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai #url修改 username: root password: root
3. 在src/main/resources资源目录下添加spy.properties
src/main/resources
spy.properties
#spy.properties #3.2.1以上使用 modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory # 真实JDBC driver , 多个以逗号分割,默认为空。由于上面设置了modulelist, 这里可以不用设置driverlist #driverlist=com.mysql.cj.jdbc.Driver # 自定义日志打印 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger #若要日志输出到文件, 把上面的appnder注释掉, 或者采用下面的appender, 再添加logfile配置 #不配置appender时, 默认是往文件进行输出的 #appender=com.p6spy.engine.spy.appender.FileLogger #logfile=log.log # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=2 # 执行时间设置, 只有超过这个执行时间的才进行记录, 默认值0, 单位毫秒 executionThreshold=10
随便运行一个测试用例,可以看到该SQL的执行时长被记录了下来
多租户的概念:多个用户共用一套系统,但他们的数据有需要相对的独立,保持一定的隔离性。
多租户的数据隔离一般有如下的方式:
不同租户使用不同的数据库服务器
优点是:不同租户有不同的独立数据库,有助于扩展,以及对不同租户提供更好的个性化,出现故障时恢复数据较为简单。
缺点是:增加了数据库数量,购置成本,维护成本更高
不同租户使用相同的数据库服务器,但使用不同的数据库(不同的schema)
优点是购置和维护成本低了一些,缺点是数据恢复较为困难,因为不同租户的数据都放在了一起
不同租户使用相同的数据库服务器,使用相同的数据库,共享数据表,在表中增加租户id来做区分
优点是,购置和维护成本最低,支持用户最多,缺点是隔离性最低,安全性最低
食用实例如下
添加多租户拦截器配置。添加配置后,在执行CRUD的时候,会自动在SQL语句最后拼接租户id的条件
package com.example.mp.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { @Override public Expression getTenantId() { // 返回租户id的值, 这里固定写死为1 // 一般是从当前上下文中取出一个 租户id return new LongValue(1); } /** ** 通常会将表示租户id的列名,需要排除租户id的表等信息,封装到一个配置类中(如TenantConfig) **/ @Override public String getTenantIdColumn() { // 返回表中的表示租户id的列名 return "manager_id"; } @Override public boolean ignoreTable(String tableName) { // 表名不为 user2 的表, 不拼接多租户条件 return !"user2".equals(tableName); } })); // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false return interceptor; } }
@Test public void testTenant() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.likeRight(User2::getName, "王") .select(User2::getName, User2::getAge, User2::getEmail, User2::getManagerId); user2Mapper.selectList(wrapper); }
当数据量特别大的时候,我们通常会采用分库分表。这时,可能就会有多张表,其表结构相同,但表名不同。例如order_1,order_2,order_3,查询时,我们可能需要动态设置要查的表名。mp提供了动态表名SQL解析器,食用示例如下
order_1
order_2
order_3
先在mysql中拷贝一下user2表
配置动态表名拦截器
package com.example.mp.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler; import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Random; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor(); HashMap map = new HashMap<>(); // 对于user2表,进行动态表名设置 map.put("user2", (sql, tableName) -> { String _ = "_"; int random = new Random().nextInt(2) + 1; return tableName + _ + random; // 若返回null, 则不会进行动态表名替换, 还是会使用user2 }); dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map); interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor); return interceptor; } }
@Test public void testDynamicTable() { user2Mapper.selectList(null); }
条件构造器AbstractWrapper中提供了多个方法用于构造SQL语句中的WHERE条件,而其子类QueryWrapper额外提供了select方法,可以只选取特定的列,子类UpdateWrapper额外提供了set方法,用于设置SQL中的SET语句。除了普通的Wrapper,还有基于lambda表达式的Wrapper,如LambdaQueryWrapper,LambdaUpdateWrapper,它们在构造WHERE条件时,直接以方法引用来指定WHERE条件中的列,比普通Wrapper通过字符串来指定要更加优雅。另,还有链式Wrapper,如LambdaQueryChainWrapper,它封装了BaseMapper,可以更方便地获取结果。
LambdaQueryWrapper
LambdaUpdateWrapper
LambdaQueryChainWrapper
条件构造器采用链式调用来拼接多个条件,条件之间默认以AND连接
AND
当AND或OR后面的条件需要被括号包裹时,将括号中的条件以lambda表达式形式,作为参数传入and()或or()
OR
and()
or()
特别的,当()需要放在WHERE语句的最开头时,可以使用nested()方法
()
nested()
条件表达式时当需要传入自定义的SQL语句,或者需要调用数据库函数时,可用apply()方法进行SQL拼接
apply()
条件构造器中的各个方法可以通过一个boolean类型的变量condition,来根据需要灵活拼接WHERE条件(仅当condition为true时会拼接SQL语句)
true
使用lambda条件构造器,可以通过lambda表达式,直接使用实体类中的属性进行条件构造,比普通的条件构造器更加优雅
若mp提供的方法不够用,可以通过自定义SQL(原生mybatis)的形式进行扩展开发
使用mp进行分页查询时,需要创建一个分页拦截器(Interceptor),注册到Spring容器中,随后查询时,通过传入一个分页对象(Page对象)进行查询即可。单表查询时,可以使用BaseMapper提供的selectPage或selectMapsPage方法。复杂场景下(如多表联查),使用自定义SQL。
AR模式可以直接通过操作实体类来操作数据库。让实体类继承自Model即可
热门内容:还在写大量 if 来判断?试试用一个规则执行器来替代它 CTO 说了,如果发现谁用 kill -9 关闭程序就开除 面试官问:MySQL 的自增 ID 用完了,怎么办? 王者荣耀中一个英雄是怎么被产生的? 饿了么CTO:“不能被烂用的框架不是好框架”! 最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ♡