窺探Mybatis Plus的ActiveRecord,並實現Spring Data JPA版本的AR
之前在做自己專案使用Mybatis的時候,一次偶然的機會看到了Mybatis Plus並使用了起來。不得不說,這個工具真的給開發提供了很大的便利性,推薦大家去試一下。特別是,它的ActiveRecord模式深深的吸引住了我:只要實體類繼承一個類,並重寫獲取主鍵的值的方法,就可以使用例項物件去呼叫簡單的增刪改查方法。於是乎,我決定解讀一下Mybatis Plus工具的ActiveRecord模式。
一、Mybatis Plus介紹
Mybatis Plus(簡稱 MP)是一個 Mybatis 的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。
特性
- 無侵入
- 損耗小:啟動即會自動注入基本 CURD,效能基本無損耗,直接面向物件操作
- 強大的 CRUD 操作:內建通用 Mapper、通用 Service,僅僅通過少量配置即可實現單表大部分 CRUD 操作,更有強大的條件構造器,滿足各類使用需求
- 支援 Lambda 形式呼叫:通過 Lambda 表示式,方便的編寫各類查詢條件,無需再擔心欄位寫錯
- 支援多種資料庫:支援 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer2005、SQLServer 等多種資料庫
- 支援主鍵自動生成
- 支援 XML 熱載入:Mapper 對應的 XML 支援熱載入,對於簡單的 CRUD 操作,甚至可以無 XML 啟動
- 支援 ActiveRecord 模式:支援 ActiveRecord 形式呼叫,實體類只需繼承 Model 類即可進行強大的 CRUD 操作
- 支援自定義全域性通用操作:支援全域性通用方法注入( Write once, use anywhere )
- 支援關鍵詞自動轉義:支援資料庫關鍵詞(order、key......)自動轉義,還可自定義關鍵詞
- 內建程式碼生成器
- 內建分頁外掛:基於 MyBatis 物理分頁,開發者無需關心具體操作,配置好外掛之後,寫分頁等同於普通 List 查詢
- 內建效能分析外掛:可輸出 Sql 語句以及其執行時間,建議開發測試時啟用該功能,能快速揪出慢查詢
- 內建全域性攔截外掛:提供全表 delete 、 update 操作智慧分析阻斷,也可自定義攔截規則,預防誤操作
- 內建 Sql 注入剝離器:支援 Sql 注入剝離,有效預防 Sql 注入攻擊
框架結構
二、ActiveRecord實現原理
1、什麼是ActiveRecord?
Active Record 是一種資料訪問設計模式,它可以幫助你實現資料物件Object到關係資料庫的對映。
應用Active Record 時,每一個類的例項物件唯一對應一個數據庫表的一行(一對一關係)。你只需繼承一個abstract Active Record 類就可以使用該設計模式訪問資料庫,其最大的好處是使用非常簡單。
2、Mybatis Plus的ActiveRecord
在Mybatis-Plus中提供了ActiveRecord的模式,支援 ActiveRecord 形式呼叫,實體類只需繼承 Model 類即可實現基本 CRUD 操作,簡單來說就是一個實體類繼承Model類,並通過註解與資料庫的表名進行關聯,這樣就可以通過實體類直接進行表的簡單增刪改查操作,這樣也確實極大的方便了開發人員。
在MP中,我們可以這樣使用AR模式:
(1)實體類繼承Model類
(2)重寫pkVal方法
(3)通過實體類直接進行表的簡單增刪改查操作
原理理解:
簡單來說Mybatis-plus是基於Mybatis的基礎之上進行開發的,其基本操作還是一個Mapper操作中對應一條sql語句,通過引數和返回值來處理sql語句的執行結果。那樣我們可以理解Mybatis-Plus的ActiveRecord其實就是Mybatis-Plus給我們提供一些簡單的增刪改查操作SQl語句的自動生成操作,可以參考部落格mybtais-plus學習--BaseMapper提供的方法及SQL語句生成,在Mybatis提供的BaseMapper中預設提供了一些簡單增刪改查操作,其通過自動生成sql來初始化Mybatis的一些操作,其最終實現和我們直接基於Mybatis開發是一致的。
Model原始碼:
public abstract class Model<T extends Model> implements Serializable {
private static final long serialVersionUID = 1L;
public Model() {
}
@Transactional
public boolean insert() {
return SqlHelper.retBool(this.sqlSession().insert(this.sqlStatement(SqlMethod.INSERT_ONE), this));
}
@Transactional
public boolean insertAllColumn() {
return SqlHelper.retBool(this.sqlSession().insert(this.sqlStatement(SqlMethod.INSERT_ONE_ALL_COLUMN), this));
}
@Transactional
public boolean insertOrUpdate() {
if (StringUtils.checkValNull(this.pkVal())) {
return this.insert();
} else {
return this.updateById() || this.insert();
}
}
@Transactional
public boolean deleteById(Serializable id) {
return SqlHelper.delBool(this.sqlSession().delete(this.sqlStatement(SqlMethod.DELETE_BY_ID), id));
}
@Transactional
public boolean deleteById() {
if (StringUtils.checkValNull(this.pkVal())) {
throw new MybatisPlusException("deleteById primaryKey is null.");
} else {
return this.deleteById(this.pkVal());
}
}
@Transactional
public boolean delete(String whereClause, Object... args) {
return this.delete(Condition.create().where(whereClause, args));
}
@Transactional
public boolean delete(Wrapper wrapper) {
Map<String, Object> map = new HashMap();
map.put("ew", wrapper);
return SqlHelper.delBool(this.sqlSession().delete(this.sqlStatement(SqlMethod.DELETE), map));
}
@Transactional
public boolean updateById() {
if (StringUtils.checkValNull(this.pkVal())) {
throw new MybatisPlusException("updateById primaryKey is null.");
} else {
Map<String, Object> map = new HashMap();
map.put("et", this);
return SqlHelper.retBool(this.sqlSession().update(this.sqlStatement(SqlMethod.UPDATE_BY_ID), map));
}
}
@Transactional
public boolean updateAllColumnById() {
if (StringUtils.checkValNull(this.pkVal())) {
throw new MybatisPlusException("updateAllColumnById primaryKey is null.");
} else {
Map<String, Object> map = new HashMap();
map.put("et", this);
return SqlHelper.retBool(this.sqlSession().update(this.sqlStatement(SqlMethod.UPDATE_ALL_COLUMN_BY_ID), map));
}
}
@Transactional
public boolean update(String whereClause, Object... args) {
return this.update(Condition.create().where(whereClause, args));
}
@Transactional
public boolean update(Wrapper wrapper) {
Map<String, Object> map = new HashMap();
map.put("et", this);
map.put("ew", wrapper);
return SqlHelper.retBool(this.sqlSession().update(this.sqlStatement(SqlMethod.UPDATE), map));
}
public List<T> selectAll() {
return this.sqlSession().selectList(this.sqlStatement(SqlMethod.SELECT_LIST));
}
public T selectById(Serializable id) {
return (Model)this.sqlSession().selectOne(this.sqlStatement(SqlMethod.SELECT_BY_ID), id);
}
public T selectById() {
if (StringUtils.checkValNull(this.pkVal())) {
throw new MybatisPlusException("selectById primaryKey is null.");
} else {
return this.selectById(this.pkVal());
}
}
public List<T> selectList(Wrapper wrapper) {
Map<String, Object> map = new HashMap();
map.put("ew", wrapper);
return this.sqlSession().selectList(this.sqlStatement(SqlMethod.SELECT_LIST), map);
}
public List<T> selectList(String whereClause, Object... args) {
return this.selectList(Condition.create().where(whereClause, args));
}
public T selectOne(Wrapper wrapper) {
return (Model)SqlHelper.getObject(this.selectList(wrapper));
}
public T selectOne(String whereClause, Object... args) {
return this.selectOne(Condition.create().where(whereClause, args));
}
public Page<T> selectPage(Page<T> page, Wrapper<T> wrapper) {
Map<String, Object> map = new HashMap();
wrapper = SqlHelper.fillWrapper(page, wrapper);
map.put("ew", wrapper);
List<T> tl = this.sqlSession().selectList(this.sqlStatement(SqlMethod.SELECT_PAGE), map, page);
page.setRecords(tl);
return page;
}
public Page<T> selectPage(Page<T> page, String whereClause, Object... args) {
return this.selectPage(page, Condition.create().where(whereClause, args));
}
public int selectCount(String whereClause, Object... args) {
return this.selectCount(Condition.create().where(whereClause, args));
}
public int selectCount(Wrapper wrapper) {
Map<String, Object> map = new HashMap();
map.put("ew", wrapper);
return SqlHelper.retCount((Integer)this.sqlSession().selectOne(this.sqlStatement(SqlMethod.SELECT_COUNT), map));
}
public SqlRunner sql() {
return new SqlRunner(this.getClass());
}
protected SqlSession sqlSession() {
return SqlHelper.sqlSession(this.getClass());
}
protected String sqlStatement(SqlMethod sqlMethod) {
return this.sqlStatement(sqlMethod.getMethod());
}
protected String sqlStatement(String sqlMethod) {
return SqlHelper.table(this.getClass()).getSqlStatement(sqlMethod);
}
protected abstract Serializable pkVal();
}
三、實現Spring Data JPA版本的AR
1、Spring Data JPA的基本使用
這裡不做詳細講解,有興趣的童鞋可以找相關資源進行學習。
(1)定義實體類(如果資料庫表名、欄位名跟實體類類名、屬性名不符合預設轉換規範,需要使用指定註解標明)。
(2)定義增刪改查介面。
(3)在業務程式碼注入bean並使用。
2、定義抽象父類Model
其實,實體類對資料庫的操作,本質上還是依賴實體類對應的CrudRepository介面。關鍵是,不同的實體類,所對應的CrudRepository介面的具體類也是不同的。所以,需要在實體類呼叫方法的時候,根據這個類找到對應的CrudRepository。
a.定義兩個泛型,實體類的型別及其主鍵的型別。
b.定義一個獲取主鍵值的抽象方法,強制子類覆蓋。
c.定義一個map,用於將獲取過的CrudRepository儲存,避免重複獲取影響效能。
完整程式碼
/**
* 具備增刪查功能的實體父類
* @author z_hh
* @time 2018年11月10日
*/
/*
* T為實體自身型別,ID為實體主鍵型別
*/
public abstract class Model<T, ID> {
/**
* 用於獲取容器中bean物件的上下文,由外部用Model.setApplicationContext方法傳入
*/
private static ApplicationContext applicationContext;
public static void setApplicationContext(ApplicationContext applicationContext) {
Model.applicationContext = applicationContext;
}
/**
* 維護各個實體類對應的CrudRepository物件,避免重複呼叫applicationContext.getBean方法影響效能
*/
private Map<String, CrudRepository<T, ID>> repositories = new HashMap<>();
@SuppressWarnings("unchecked")
private CrudRepository<T, ID> getRepository() {
// 1.獲取實體物件對應的CrudRepository的bean名稱,這裡根據具體的命名風格來調整
String entityClassName = this.getClass().getSimpleName(),
beanName = entityClassName.substring(0, 1).toLowerCase() + entityClassName.substring(1) + "Dao";
CrudRepository<T, ID> crudRepository = repositories.get(beanName);
// 2.如果map中沒有,從上下文環境獲取,並放進map中
if (Objects.isNull(crudRepository)) {
crudRepository = (CrudRepository<T, ID>) applicationContext.getBean(beanName);
repositories.put(beanName, crudRepository);
}
// 返回
return crudRepository;
}
/**
* 儲存當前物件
* @return 儲存後的當前物件
*/
@SuppressWarnings("unchecked")
@Transactional
public T save() {
return getRepository().save((T) this);
}
/**
* 根據當前物件的id獲取物件
* @return 查詢到的物件
*/
@SuppressWarnings("unchecked")
public T find() {
return (T) getRepository().findById((ID) this.pkVal()).orElse(null);
}
/**
* 刪除當前物件
*/
@SuppressWarnings("unchecked")
@Transactional
public void remove() {
getRepository().delete((T) this);
}
protected abstract Serializable pkVal();
}
3、實體類繼承Model並重寫pkVal方法
4、編寫Junit測試程式碼
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentTest {
@Autowired
private StudentDao studentDao;
@Autowired
private ApplicationContext applicationContext;
@Before
public void init() {
Model.setApplicationContext(applicationContext);
}
@Test
public void testCrud() {
Student student = new Student();
student.setName("zhh");
student.setSex(1);
student.setMobile("13800138000");
student.setBirthday(new Date());
student.setAddress("廣州市天河區");
// studentDao.save(student);
// 儲存
student.save();
if (Objects.nonNull(student.getId())) {
System.out.println("新增成功");
System.out.println(student.toString());
} else {
System.out.println("新增失敗");
}
// 查詢
Student student2 = new Student();
student2.setId(student.getId());
student2 = student2.find();
if (Objects.nonNull(student2)) {
System.out.println("查詢成功");
System.out.println(student2.toString());
} else {
System.out.println("查詢失敗");
}
// 刪除
student.remove();
if (Objects.isNull(student.find())) {
System.out.println("刪除成功");
} else {
System.out.println("刪除失敗");
}
}
}