之前在做自己项目使用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 等多种数据库
- 支持主键自动生成:支持多达 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 注入攻击
框架结构
二、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("删除失败");
}
}
}