簡介
Mybatis-Plus(簡稱MP)是一個 Mybatis 的增強工具,在 Mybatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。
使用它可以簡化單表的操作, 節省開發時間, 國人寫的文檔已經非常通俗易懂了, 所以這里只是對其進行一些規范,便於多人協作開發
如果不了解mp, 請先閱讀官方文檔, 大約耗時半小時以內
mp最新的mybatis版本是3.4.6, mybatis-spring版本是1.3.2 比我們的全局配置稍微高了一些, 看了下相關的更新以及自測,發現是兼容的, 但是不排除有些操作不支持, 支持的操作已經足夠簡化開發了
現在mp最新的版本已經升級到3.X, 所以這里2.X的文檔不在維護了
名詞解釋
GlobalConfig -> 指的是mp的全局配置文件, 版本2.X和 3.X的變化還是比較大, 可以直接看源碼的注釋
BaseMapper -> BaseMapper里面維護了許多常用的方法, 例如根據主鍵查詢記錄, 根據根據主鍵修改記錄等等, 普通mapper只需要繼承這個BaseMapper即可獲得通用的方法
Wrapper -> 條件構造抽象類, 用來生成sql的條件
LogicSqlInjector -> 邏輯sql處理器, (邏輯刪除)
PaginationInterceptor -> 分頁插件, 底層是物理分頁
實體類命名
GlobalConfig 中默認表名、字段名、使用下划線命名
eg. TrainCourse 對應的表名為 train_course
成員 courseType 對應表字段名 course_type
這些可以通過注解覆蓋
類 : @TableName("對應的表名")
字段: @TableField("對應的字段名")
@Data
public class Course {
/**
* 主鍵
*/
@TableId(value = "course_id", type = IdType.AUTO)
private Long courseId;
private Integer courseType;
private String title;
/**
* 城市
*/
private String city;
// 因為mp在使用的時候如果直接使用str, 等於就是使用了魔法值, 不便於維護, 所以統一在實體類中維護
public static final String COURSE_ID = "course_id";
public static final String COURSE_TYPE = "course_type";
}
- 如果是表中不存在的字段 , 務必打上@TableField(exist = false)
- 如果是表示是否刪除的字段, 如deleted, 則也要打上@TableLogic 名稱和全局一樣, 如果和數據庫字段名不符合全局的映射, 則需要手動指定
- 數據庫中只有0,1的值, 使用Boolean類型
XML和接口
xml和接口是完全兼容之前mybatis的寫法, 所以不會影響之前的邏輯。如果xml手動定義的名稱和mp的BaseMapper中定義的一樣, 則手動指定的會覆蓋BaseMapper中的。
即順序為 自己在xml中定義的方法 > BaseMapper中的方法
如果僅僅使用到了BaseMapper中的方法, 則可以不配置XML配置文件
使用示例
在構建好實體類后, 不要在條件查詢器中直接使用魔法值, 不容易維護
// 根據主鍵id查詢
Coursecourse = mCourseMapper.selectById(id);
// 使用查詢器
Wrapper<Course> query = new EntityWrapper<>();
if (courseType != null) {
// 這里的寫法其實有點像jooq, 生成的sql為: where course_type = courseType
query.eq(Course.COURSE_TYPE, courseType);
}
query.eq(Course.NAME, name);
// deleted 使用 true 或者 false , 盡量不要使用0,1 因為0,1是魔法值
query.eq(Course.DELETED, true);
// 查詢count的數量
int result = mCourseMapper.selectCount(query);
邏輯刪除插件
一旦使用了邏輯刪除插件, 以后默認的select都會帶上 deleted = 0, 即只會查出不標記為刪除的數據, 所以不需要手動在寫
Wrapper<Test> query = new EntityWrapper<>();
query.eq(Test.DELETED, false); //這句話是多余的, 因為配置了邏輯刪除插件, 默認在select的時候會加上 deleted = 0 這個條件
// 經過測試發現, 這會帶來一個問題, 比如就是要查詢刪除的記錄, 使用下面的這句話是無效的, 所以一旦使用了邏輯刪除插件, 如果要查詢刪除了的記錄, 請手寫xml
query.eq(Test.DELETED, true); // 效果就是 WHERE deleted=0 AND (deleted = 1) 這樣明顯是查不出任何一條記錄的
2.X版本
示例配置
mp只是用自己的 MybatisSqlSessionFactoryBean 替代了 mybatis-spring的 SqlSessionFactoryBean 其余的配置都不需要改動
再 globalConfig 中, 務必加入邏輯刪除插件, 因為我們的大多數操作都是邏輯刪除。物理刪除請手動寫xml。
3.X 版本
3.X版本和2.X版本的差別還是比較大的, 主要的差別如下
升級指南可以參考 https://my.oschina.net/u/241218/blog/1838534
其中有一點, 就是下面這個方法讓我有點困惑
UpdateWrapper
方法名 | 說明 |
---|---|
set | SQL SET 字段(一個字段使用一次) |
因為發現在使用中並沒有使用到這個方法, 連BaseMapper里面的update, 根據下面的解釋, 也用不到, 猜測是要自己實現一個注入方法了, 這里沒有深究
配置示例
這里采用java config的方式來配置(個人喜好, 加上java config可以做一些環境判斷, 上面那個打印sql每次都需要手動打開注釋, 麻煩)
Spring Boot的方式非常簡單, 參考官網即可, 所以這里給出的是Spring mvc的配置方式, 使用的DataSource是阿里的druid
這里就直接貼配置文件了, 下面的包含了多數據源的配置
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig;
import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import java.util.ArrayList;
import java.util.List;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yanghuan
*/
@Configuration
public class MybatisPlusConfig {
// 環境標志, 區分dev or prod
@Autowired
private String projectStage;
/**
* 數據源a 相關信息
*/
@Value("${a.url}")
private String aUrl;
@Value("${a.username}")
private String aUsername;
@Value("${a.password}")
private String aPassword;
/**
* 數據源b 相關信息
*/
@Value("${b.url}")
private String bUrl;
@Value("${b.username}")
private String bUsername;
@Value("${b.password}")
private String bPassword;
// 創建數據源a
@Bean(initMethod = "init", destroyMethod = "close")
public DruidDataSource aDataSource(){
DruidDataSource d = new DruidDataSource();
d.setUrl(aUrl);
d.setUsername(aUsername);
d.setPassword(aPassword);
return d;
}
@Bean(initMethod = "init", destroyMethod = "close")
public DruidDataSource bDataSource(){
DruidDataSource d= new DruidDataSource();
d.setUrl(bUrl);
d.setUsername(bUsername);
d.setPassword(bPassword);
return d;
}
// 創建全局配置
@Bean
public GlobalConfig mpGlobalConfig() {
// 全局配置文件
GlobalConfig globalConfig = new GlobalConfig();
DbConfig dbConfig = new DbConfig();
// 默認為自增
dbConfig.setIdType(IdType.AUTO);
// 手動指定db 的類型, 這里是mysql
dbConfig.setDbType(DbType.MYSQL);
globalConfig.setDbConfig(dbConfig);
if (!ProjectStageUtil.isProd(projectStage)) {
// 如果是dev環境,則使用 reload xml的功能,方便調試
globalConfig.setRefresh(true);
}
// 邏輯刪除注入器
LogicSqlInjector injector = new LogicSqlInjector();
globalConfig.setSqlInjector(injector);
return globalConfig;
}
@Bean(name = "aSqlSessionFactory")
public MybatisSqlSessionFactoryBean aSqlSessionFactory(
DruidDataSource aDataSource,
GlobalConfig globalConfig) {
return getSessionFactoryBean(aDataSource, globalConfig);
}
/**
* MapperScannerConfigurer 是 BeanFactoryPostProcessor 的一個實現,如果配置類中出現 BeanFactoryPostProcessor ,會破壞默認的
* post-processing, 如果不加static, 會導致整個都提前加載, 這時候, 取不到projectStage的值
*
* @return
*/
@Bean
public static MapperScannerConfigurer aMapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.a");
// 設置為上面的 factory name
configurer.setSqlSessionFactoryBeanName("bSqlSessionFactory");
return configurer;
}
@Bean(name = "bSqlSessionFactory")
public MybatisSqlSessionFactoryBean bSqlSessionFactory(
DruidDataSource bDataSource,
GlobalConfig mpGlobalConfig) {
return getSessionFactoryBean(bDataSource, mpGlobalConfig);
}
private MybatisSqlSessionFactoryBean getSessionFactoryBean(
aDataSource aDataSource,
GlobalConfig globalConfig) {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(aDataSource);
sqlSessionFactoryBean.setGlobalConfig(globalConfig);
// 源碼里面如果有configuration, 不會注入BaseMapper里面的方法, 所以這里要這樣寫
MybatisConfiguration configuration = new MybatisConfiguration().init(globalConfig);
configuration.setMapUnderscoreToCamelCase(true);
sqlSessionFactoryBean.setConfiguration(configuration);
List<Interceptor> interceptors = new ArrayList<>();
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 設置分頁插件
interceptors.add(paginationInterceptor);
if (!ProjectStageUtil.isProd(projectStage)) {
// 如果是dev環境,打印出sql, 設置sql攔截插件, prod環境不要使用, 會影響性能
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
interceptors.add(performanceInterceptor);
}
sqlSessionFactoryBean.setPlugins(interceptors.toArray(new Interceptor[0]));
return sqlSessionFactoryBean;
}
/**
* b 的mapperscan
* @return
*/
@Bean
public static MapperScannerConfigurer bMapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.b");
// 設置為上面的 factory name
configurer.setSqlSessionFactoryBeanName("bSqlSessionFactory");
return configurer;
}
}
使用方式和之前的類似, 只不過 EntityWrapper 改為了 QueryWrapper
以及有一些新的lambda方法
- 不需要定義一個靜態變量了, 如果在成員變量上打了@TableField注解, 這里會取注解中的值, 否則就是globalConfig里面的
@Test
public void testPluralLambda() {
TableInfoHelper.initTableInfo(null, BdSystemUser.class);
QueryWrapper<BdSystemUser> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(BdSystemUser::getName,"sss");
queryWrapper.lambda().eq(BdSystemUser::getUserId,1);
BdSystemUser user = mBdSystemUserMapper.selectOne(queryWrapper);
}
- 之前2.X版本對復雜的or查詢不能兼容, 即使用 Wrapper 的or方法, 默認是不加括號的
不過對於負責的查詢, 還是推薦手寫sql, 這里展示一下如何使用mp來構建
@Test
public void test() {
Wrapper<BdSystemUser> wrapper = new QueryWrapper<BdSystemUser>().lambda().eq(BdSystemUser::getName, 123)
.or(c -> c.eq(BdSystemUser::getExternalStaff, 1).eq(BdSystemUser::getUserId, 2))
.eq(BdSystemUser::getUserId, 1);
BdSystemUser user = mBdSystemUserMapper.selectOne(wrapper);
// 對應的sql: SELECT * FROM bdsystem_user WHERE deleted = 0 AND NAME = ? OR ( externalstaff = ? AND userid = ? ) AND userid = ?
// 可以看到, or中的語句是在括號里的
}
3.可以把where 后的條件防止在一個map中
@Test
public void testCompare() {
Map<String, Object> map = new HashMap<>();
map.put("userid", 1);
map.put("ldap", "luoshu");
QueryWrapper<BdSystemUser> queryWrapper = new QueryWrapper<BdSystemUser>()
.allEq(true, map, false);
BdSystemUser user = mBdSystemUserMapper.selectOne(queryWrapper);
// SELECT userid,name,ldap,stafftype,leader,departmenttype,groupid,externalstaff,mobile,created,lastmodified,deleted FROM bdsystem_user WHERE deleted=0 AND ldap = ? AND userid = ?
// 可以看到, map中對應的值, 是where后的條件
}
更詳細的demo可以參照 demo
3.X 注意事項
- BaseMapper的selectOne() 現在在返回多條語句的時候,會拋異常, 需要手動在條件中處理, 使用wrapper.last()方法.
- 如果mp和tddl這樣的中間件進行集成, 在分頁插件的創建時候, 需要指定數據庫方言, 之前可以不指定的原因是mp通過解析jdbc url的時候可以獲取到對應的方言
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 這邊因為接了tddl, 必須手動設置方言, 否則分頁會拋異常
paginationInterceptor.setDialectType(DbType.MYSQL.getDb());