一、ActiveRecord模式
ActiveRecord也属于ORM(对象关系映射)层,由Rails最早提出,遵循标准的ORM模型:表映射到记录,记录映射到对象,字段映射到对象属性。配合遵循的命名和配置惯例,能够很大程度的快速实现模型的操作,而且简洁易懂。
ActiveRecord的主要思想是:
-
每一个数据库表对应创建一个类,类的每一个对象实例对应于数据库中表的一行记录;通常表的每个字段在类中都有相应的Field;
-
ActiveRecord同时负责把自己持久化,在ActiveRecord中封装了对数据库的访问,即CURD;;
-
ActiveRecord是一种领域模型(Domain Model),封装了部分业务逻辑;
二、使用ActiveRecord
使用ActiveRecord需要再实体类上继承Model
例如:
UserDemo实体类继承了Model
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class UserDemo extends Model<UserDemo> {
@TableId(value = "id")
private Long id;
//select如果为false表示不从数据库查询该字段
@TableField(select = true )
private String name;
private Integer age;
private String email;
//插入数据时自动填充数据,需要配置插件
@TableField(fill = FieldFill.INSERT)
private LocalDateTime insertTime;
//修改数据时自动填充数据,需要配置插件
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
}
同时需要有个Mapper接口继承BaseMapper并指定泛型为UserDemo
@Repository
public interface UserDemoMapper extends BaseMapper<UserDemo>{
}
测试
@Test
public void testActiveRecord(){
List<UserDemo> users = userDemo.selectAll();
users.forEach(System.out::println);
}
结果如下:成功查询到数据
但是如果只有实体类没有Mapper接口就会报错如下:
Model抽象类里面也有通用的CRUD,可以直接使用,这里就不一一演示了
三、Mybatis-Plus常用插件
3.1、插件简介
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
插件的作用就是为了增强功能,它通过拦截器拦截需要增强的方法,通过动态代理,为被拦截的方法增强功能
3.2、如何使用插件
要想自定义插件必须要实现Interceptor接口,这个接口中有三个方法需要实现。
方法 | 作用 |
---|---|
intercept | 这个方法是mybatis的核心方法,要实现自定义逻辑,基本都是改造这个方法,其中invocation参数可以通过反射要获取原始方法和对应参数信息 |
plugin | 它的作用是用来生成一个拦截对方,也就是代理对象,使得被代理的对象一定会经过intercept方法,通常都会使用mybatis提供的工具类Plugin来获取代理对象,如果有自己独特需求,可以自定义 |
setProperties | 这个方法就是用来设置插件的一些属性 |
@Intercepts注解就是用来标明拦截4个接口中的那个接口和接口中的哪些方法。如下自定义一个拦截器插件
@Intercepts({@Signature(type = Executor.class,method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(type = Executor.class,method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)}
)
public class MyInteceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("");
System.out.println("");
System.out.println("");
System.out.println("");
System.out.println("成功拦截查询!!!");
System.out.println("");
System.out.println("");
System.out.println("");
System.out.println("");
Object proceed = invocation.proceed();
return proceed;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target,this);
}
@Override
public void setProperties(Properties properties) {
}
}
//将拦截器添加到容器中
@Bean
public MyInteceptor myInteceptor(){
return new MyInteceptor();
}
对Executor接口的query方法进行了拦截,并通过动态代理生成代理类,此时,执行查询方法就会别拦截
测试:
@Test
public void testSelect(){
userMapper.selectById(2);
}
如下图,成功对查询方法进行拦截,并在控制台打印语句
3.3、执行分析插件
在MP中提供了对SQL执行的分析的插件,可用作阻断全表更新、删除的操作,注意:该插件仅适用于开发环境,不适用于生产环境
//注册SqlExplainInterceptor(高版本已经被标记过时)
@Bean
public SqlExplainInterceptor sqlExplainInterceptor(){
SqlExplainInterceptor sqlExplainInterceptor = new SqlExplainInterceptor();
List<ISqlParser> sqlParserList = new ArrayList<>();
// 攻击 SQL 阻断解析器、加入解析链
sqlParserList.add(new BlockAttackSqlParser());
sqlExplainInterceptor.setSqlParserList(sqlParserList);
return sqlExplainInterceptor;
}
//删除表中所有的数据
@Test
public void sqlExplainTest(){
userMapper.delete(null);
}
结果如下,控制台抛出异常,禁止全表删除
3.4、性能分析插件
性能分析拦截器,用于输出每条 SQL 语句及其执行时间,可以设置最大执行时间,超过时间会抛出异常。同样该插件只适合开发环境。
导入p6spy的pom依赖
<!--SQL分析插件依赖-->
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.0</version>
</dependency>
使用p6spy需要修改jdbc的URL和Dirver
spring:
datasource:
username: 'root'
password: 'root'
url: jdbc:p6spy:mysql://localhost:3306/mybatis_plus?useSSL=false&serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
同时在resources目录下创建spy.properties文件(MP官网文档复制的)
#3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 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
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
测试:
@Test
public void testSelect(){
userMapper.selectBatchIds(Arrays.asList(1,2,3));
}
结果如图:
可以通过logMessageFormat和customLogMessageFormat自定义日志的输出格式如下:
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=%(currentTime)
SQL耗时: %(executionTime) ms
连接信息: %(category)-%(connectionId)
执行语句: %(sqlSingleLine)
可以选择的变量如下
- %(connectionId):connection id
- %(currentTime):当前时间
- %(executionTime):执行耗时
- %(category):执行分组
- %(effectiveSql):提交的 SQL 换行
- %(effectiveSqlSingleLine):提交的 SQL 不换行显示
- %(sql):执行的真实 SQL 语句,已替换占位
- %(sqlSingleLine):执行的真实 SQL 语句,已替换占位不换行显示
3.5、乐观锁插件
使用乐观锁的意图是当要更新一条记录的时候,希望这条记录没有被别人更新。
乐观锁实现方式:
-
取出记录时,获取当前version
-
更新时,带上这个version
-
执行更新时, set version = newVersion where version = oldVersion
-
如果version不对,就更新失败
乐观锁配置需要2步 记得两步
- 插件配置
//高版本中已经被标记过时
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
- 注解实体字段@Version,必需要!!
@Version
private Integer version;
特别说明:
- 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
- 整数类型下
newVersion = oldVersion + 1
newVersion
会回写到entity
中- 仅支持
updateById(id)
与update(entity, wrapper)
方法 - 在
update(entity, wrapper)
方法下,wrapper
不能复用!!!
实体类如下:
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("tb_user")
public class User extends Model<User> {
private Long id;
private String userName;
private String password;
private String name;
private Integer age;
private String email;
@Version
private Integer version;
}
表结构如下:
测试:
@Test
public void testOptimisticLock(){
User user1 = userMapper.selectById(1l);
User user2 = userMapper.selectById(2l);
user1.setAge(80);
user2.setAge(90);
int result = this.userMapper.updateById(user1);
int result2 = this.userMapper.updateById(user2);
}
结果如下:
第一条修改语句成功,第二条修改语句失败,因为修改数据的时候要判断取出数据时的version和修改数据时数据库的version是否一致。第一条语句修改过后,数据库的version变为了1,而user2的version是0,此时再去修改数据,就与数据库version不一致,更新失败.
3.6、逻辑删除插件
所谓逻辑删除就是将数据标记为删除,而并非真正的物理删除(非DELETE操作),查询时需要携带状态条件,确保被标记的数据不被查询到。这样做的目的就是避免数据被真正的删除。
使用逻辑删除还是需要两个步骤:
- application.yaml添加配置(步骤一)
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
-
实体类加上@TableLogic注解(步骤二)
@Component @Data @AllArgsConstructor @NoArgsConstructor @TableName("tb_user") public class User extends Model<User> { private Long id; private String userName; private String password; private String name; private Integer age; private String email; @Version private Integer version; @TableLogic private Integer deleted; }
表字段:
测试:
@Test
public void testLogicDelete(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("id",1l);
userMapper.delete(wrapper);
userMapper.selectById(1l);
}
删除操作只是将deleted置为1,查询时将deleted作为条件查询,数据库中数据并没有被真正删除。
注意在向有逻辑删除的表插入数据:
- 字段在数据库定义默认值(推荐)
- insert 前自己 set 值
- 使用自动填充功能
四、Sql注入器
全局配置 sqlInjector
用于注入 ISqlInjector
接口的子类,实现自定义方法注入。
public interface ISqlInjector {
/**
* <p>
* 检查SQL是否注入(已经注入过不再注入)
* </p>
*
* @param builderAssistant mapper 信息
* @param mapperClass mapper 接口的 class 对象
*/
void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}
自定义自己的通用方法可以实现接口 ISqlInjector
也可以继承抽象类 AbstractSqlInjector
注入通用方法 SQL 语句
然后继承 BaseMapper
添加自定义方法,全局配置 sqlInjector
注入 MP 会自动将类所有方法注入到 mybatis
容器中。
4.1、编写MyBaseMapper
@Repository
public interface MyBaseMapper extends BaseMapper<User> {
/**
* 自定义查询所有方法
* @return
*/
List findAll();
}
4.2、自定义Sql注入器
如果直接继承AbstractSqlInjector的话,原有的BaseMapper中的方法将失效,所以我们选择继承DefaultSqlInjector进行扩展。
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
//扩充自定义的方法
methodList.add(new FindAll());
return methodList;
}
}
4.3、编写FindAll实体类
public class FindAll extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql = String.format("<script>%s SELECT %s FROM %s %s %s
</script>", sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo), sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass, "findAll", sqlSource, tableInfo);
}
}
4.4、注入到容器中
@Bean
public MySqlInjector mySqlInjector(){
return new MySqlInjector();
}
4.5、测试
@Autowired
MyBaseMapper MyBaseMapper;
@Test
public void testFindAll(){
List<User> all = MyBaseMapper.findAll();
System.out.println(all);
}
结果如下:成功查询到数据
五、自动填充功能
原理:
- 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
- 注解填充字段
@TableField(.. fill = FieldFill.INSERT)
生成器策略部分也可以配置!
- 实现MetaObjectHandler接口
@Component
public class DateHandler implements MetaObjectHandler {
/**
*插入时给insertTime自动填充
*/
@Override
public void insertFill(MetaObject metaObject) {
setFieldValByName("insertTime", LocalDateTime.now(),metaObject);
}
/**
*修改时给updateTime自动填充
*/
@Override
public void updateFill(MetaObject metaObject) {
setFieldValByName("updateTime", LocalDateTime.now(),metaObject);
}
}
实体字段:
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class UserDemo extends Model<UserDemo> {
private Long id;
private String name;
private Integer age;
private String email;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime insertTime;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
}
FieldFill可选
- DEFAULT :默认不处理
- INSERT:插入时填充
- UPDATE:更新时填充
- INSERT_UPDATE:插入和更新时填充
数据库表
插入测试:
@Test
public void testFillInsert(){
UserDemo userDemo = new UserDemo();
userDemo.setEmail("163@qq.com");
userDemo.setAge(100);
userDemo.setName("马化腾");
userDemoMapper.insert(userDemo);
}
结果:插入数据成功,insert_time字段自动填充了当前时间
更新测试:
@Test
public void testFillInsert(){
UserDemo userDemo2 = new UserDemo();
userDemo2.setEmail("jd@qq.com");
userDemo2.setAge(100);
userDemo2.setName("强子");
QueryWrapper<UserDemo> wrapper = new QueryWrapper<>();
wrapper.eq("id",14);
userDemoMapper.update(userDemo2,wrapper);
}
结果:修改成功,update_time自动填充当前时间
Mybatis-Plus还支持通用枚举,以及代码生成器,多数据源等等一系列的功能,个位如果需要可以前往官方文档https://baomidou.com/查看,中国人写的文档,看起来很轻松的。最后给大家推荐一个IDEA的插件MybatisX,可以实现Java接口与XML文件的跳转,可以为Mapper方法自动生成XML。