1. 程式人生 > >解讀Mybatis Plus的ActiveRecord,並實現Spring Data JPA版本的AR

解讀Mybatis Plus的ActiveRecord,並實現Spring Data JPA版本的AR

之前在做自己專案使用Mybatis的時候,一次偶然的機會看到了Mybatis Plus並使用了起來。不得不說,這個工具真的給開發提供了很大的便利性,推薦大家去試一下。特別是,它的ActiveRecord模式深深的吸引住了我:只要實體類繼承一個類,並重寫獲取主鍵的值的方法,就可以使用例項物件去呼叫簡單的增刪改查方法。於是,我決定窺探一下Mybatis Plus工具的ActiveRecord模式。

一、Mybatis Plus介紹

官網:http://mp.baomidou.com/

Mybatis Plus(簡稱 MP)是一個 Mybatis 的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。

特性

  • 無侵入:只做增強不做改變,引入它不會對現有工程產生影響,如絲般順滑
  • 損耗小:啟動即會自動注入基本 CURD,效能基本無損耗,直接面向物件操作
  • 強大的 CRUD 操作:內建通用 Mapper、通用 Service,僅僅通過少量配置即可實現單表大部分 CRUD 操作,更有強大的條件構造器,滿足各類使用需求
  • 支援 Lambda 形式呼叫:通過 Lambda 表示式,方便的編寫各類查詢條件,無需再擔心欄位寫錯
  • 支援多種資料庫:支援 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer2005、SQLServer 等多種資料庫
  • 支援主鍵自動生成:支援多達 4 種主鍵策略(內含分散式唯一 ID 生成器 - Sequence),可自由配置,完美解決主鍵問題
  • 支援 XML 熱載入:Mapper 對應的 XML 支援熱載入,對於簡單的 CRUD 操作,甚至可以無 XML 啟動
  • 支援 ActiveRecord 模式:支援 ActiveRecord 形式呼叫,實體類只需繼承 Model 類即可進行強大的 CRUD 操作
  • 支援自定義全域性通用操作:支援全域性通用方法注入( Write once, use anywhere )
  • 支援關鍵詞自動轉義:支援資料庫關鍵詞(order、key......)自動轉義,還可自定義關鍵詞
  • 內建程式碼生成器:採用程式碼或者 Maven 外掛可快速生成 Mapper 、 Model 、 Service 、 Controller 層程式碼,支援模板引擎,更有超多自定義配置等您來使用
  • 內建分頁外掛:基於 MyBatis 物理分頁,開發者無需關心具體操作,配置好外掛之後,寫分頁等同於普通 List 查詢
  • 內建效能分析外掛:可輸出 Sql 語句以及其執行時間,建議開發測試時啟用該功能,能快速揪出慢查詢
  • 內建全域性攔截外掛:提供全表 delete 、 update 操作智慧分析阻斷,也可自定義攔截規則,預防誤操作
  • 內建 Sql 注入剝離器:支援 Sql 注入剝離,有效預防 Sql 注入攻擊

框架結構

framework

二、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("刪除失敗");
		}
		
	}
}

5、結果

本文內容到此結束了,有什麼問題或者建議,歡迎在評論區進行探討!