一、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) \n SQL耗時: %(executionTime) ms \n 連接信息: %(category)-%(connectionId) \n 執行語句: %(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\n</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。