當我們使用MyBatis的時候,需要在mapper.xml中書寫大量的SQL語句。當我們使用MyBatis Generator(MBG)作為程式碼生成器時,也會生成大量的mapper.xml檔案。其實從MBG 1.3.6版本以後,MyBatis官方已經推薦使用Dynamic SQL,使用這一新特性基本就不用寫mapper.xml檔案了,使用起來非常方便,推薦給大家!
Dynamic SQL簡介在我們使用Spring的時候,有XML和Java兩種配置方式。在使用SpringBoot時,已經推薦使用Java配置,基本不用xml配置了。使用Dynamic SQL就好比是使用Java的方式來操作MyBatis。Dynamic SQL是用於生成動態SQL語句的框架,提倡使用Java API的方式來實現SQL操作,支援複雜查詢和多表查詢。
Dynamic SQL具有如下特性:
型別安全:可以確保引數型別和資料庫欄位型別相匹配;富有表現力:語句的構建方式可以清楚地傳達其含義;使用靈活:可以使用and,or和nested條件的任意組合來構建where子句;擴充套件性強:可以同時為MyBatis3, Spring JDBC和純JDBC框架生成SQL語句;輕量級:只需新增一個小的依賴項,沒有傳遞依賴。開始使用首先我們透過一個入門示例將Dynamic SQL用起來,該示例會包含基礎的CRUD操作。
實現基本的CRUD操作這裡使用的是mall-tiny專案中許可權管理功能相關表,具體可以參考《還在從零開始搭建專案?手擼了款快速開發腳手架!》。
檢視下MBG生成的Mapper介面,比之前使用MBG時增加了很多方法,並且有了一些預設的方法實現,可見之前在mapper.xml中的實現都已經轉移到Mapper介面中去了,單表CRUD直接呼叫對應方法即可;@Mapperpublic interface UmsAdminMapper { @Generated("org.mybatis.generator.api.MyBatisGenerator") BasicColumn[] selectList = BasicColumn.columnList(id, username, password, icon, email, nickName, note, createTime, loginTime, status); @Generated("org.mybatis.generator.api.MyBatisGenerator") @SelectProvider(type=SqlProviderAdapter.class, method="select") long count(SelectStatementProvider selectStatement); @Generated("org.mybatis.generator.api.MyBatisGenerator") @DeleteProvider(type=SqlProviderAdapter.class, method="delete") int delete(DeleteStatementProvider deleteStatement); @Generated("org.mybatis.generator.api.MyBatisGenerator") @InsertProvider(type=SqlProviderAdapter.class, method="insert") @SelectKey(statement="SELECT LAST_INSERT_ID()", keyProperty="record.id", before=false, resultType=Long.class) int insert(InsertStatementProvider<UmsAdmin> insertStatement); @Generated("org.mybatis.generator.api.MyBatisGenerator") @SelectProvider(type=SqlProviderAdapter.class, method="select") @ResultMap("UmsAdminResult") Optional<UmsAdmin> selectOne(SelectStatementProvider selectStatement); @Generated("org.mybatis.generator.api.MyBatisGenerator") @SelectProvider(type=SqlProviderAdapter.class, method="select") @Results(id="UmsAdminResult", value = { @Result(column="id", property="id", jdbcType=JdbcType.BIGINT, id=true), @Result(column="username", property="username", jdbcType=JdbcType.VARCHAR), @Result(column="password", property="password", jdbcType=JdbcType.VARCHAR), @Result(column="icon", property="icon", jdbcType=JdbcType.VARCHAR), @Result(column="email", property="email", jdbcType=JdbcType.VARCHAR), @Result(column="nick_name", property="nickName", jdbcType=JdbcType.VARCHAR), @Result(column="note", property="note", jdbcType=JdbcType.VARCHAR), @Result(column="create_time", property="createTime", jdbcType=JdbcType.TIMESTAMP), @Result(column="login_time", property="loginTime", jdbcType=JdbcType.TIMESTAMP), @Result(column="status", property="status", jdbcType=JdbcType.INTEGER) }) List<UmsAdmin> selectMany(SelectStatementProvider selectStatement); @Generated("org.mybatis.generator.api.MyBatisGenerator") @UpdateProvider(type=SqlProviderAdapter.class, method="update") int update(UpdateStatementProvider updateStatement); @Generated("org.mybatis.generator.api.MyBatisGenerator") default long count(CountDSLCompleter completer) { return MyBatis3Utils.countFrom(this::count, umsAdmin, completer); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default int delete(DeleteDSLCompleter completer) { return MyBatis3Utils.deleteFrom(this::delete, umsAdmin, completer); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default int deleteByPrimaryKey(Long id_) { return delete(c -> c.where(id, isEqualTo(id_)) ); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default int insert(UmsAdmin record) { return MyBatis3Utils.insert(this::insert, record, umsAdmin, c -> c.map(username).toProperty("username") .map(password).toProperty("password") .map(icon).toProperty("icon") .map(email).toProperty("email") .map(nickName).toProperty("nickName") .map(note).toProperty("note") .map(createTime).toProperty("createTime") .map(loginTime).toProperty("loginTime") .map(status).toProperty("status") ); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default int insertSelective(UmsAdmin record) { return MyBatis3Utils.insert(this::insert, record, umsAdmin, c -> c.map(username).toPropertyWhenPresent("username", record::getUsername) .map(password).toPropertyWhenPresent("password", record::getPassword) .map(icon).toPropertyWhenPresent("icon", record::getIcon) .map(email).toPropertyWhenPresent("email", record::getEmail) .map(nickName).toPropertyWhenPresent("nickName", record::getNickName) .map(note).toPropertyWhenPresent("note", record::getNote) .map(createTime).toPropertyWhenPresent("createTime", record::getCreateTime) .map(loginTime).toPropertyWhenPresent("loginTime", record::getLoginTime) .map(status).toPropertyWhenPresent("status", record::getStatus) ); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default Optional<UmsAdmin> selectOne(SelectDSLCompleter completer) { return MyBatis3Utils.selectOne(this::selectOne, selectList, umsAdmin, completer); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default List<UmsAdmin> select(SelectDSLCompleter completer) { return MyBatis3Utils.selectList(this::selectMany, selectList, umsAdmin, completer); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default List<UmsAdmin> selectDistinct(SelectDSLCompleter completer) { return MyBatis3Utils.selectDistinct(this::selectMany, selectList, umsAdmin, completer); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default Optional<UmsAdmin> selectByPrimaryKey(Long id_) { return selectOne(c -> c.where(id, isEqualTo(id_)) ); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default int update(UpdateDSLCompleter completer) { return MyBatis3Utils.update(this::update, umsAdmin, completer); } @Generated("org.mybatis.generator.api.MyBatisGenerator") static UpdateDSL<UpdateModel> updateAllColumns(UmsAdmin record, UpdateDSL<UpdateModel> dsl) { return dsl.set(username).equalTo(record::getUsername) .set(password).equalTo(record::getPassword) .set(icon).equalTo(record::getIcon) .set(email).equalTo(record::getEmail) .set(nickName).equalTo(record::getNickName) .set(note).equalTo(record::getNote) .set(createTime).equalTo(record::getCreateTime) .set(loginTime).equalTo(record::getLoginTime) .set(status).equalTo(record::getStatus); } @Generated("org.mybatis.generator.api.MyBatisGenerator") static UpdateDSL<UpdateModel> updateSelectiveColumns(UmsAdmin record, UpdateDSL<UpdateModel> dsl) { return dsl.set(username).equalToWhenPresent(record::getUsername) .set(password).equalToWhenPresent(record::getPassword) .set(icon).equalToWhenPresent(record::getIcon) .set(email).equalToWhenPresent(record::getEmail) .set(nickName).equalToWhenPresent(record::getNickName) .set(note).equalToWhenPresent(record::getNote) .set(createTime).equalToWhenPresent(record::getCreateTime) .set(loginTime).equalToWhenPresent(record::getLoginTime) .set(status).equalToWhenPresent(record::getStatus); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default int updateByPrimaryKey(UmsAdmin record) { return update(c -> c.set(username).equalTo(record::getUsername) .set(password).equalTo(record::getPassword) .set(icon).equalTo(record::getIcon) .set(email).equalTo(record::getEmail) .set(nickName).equalTo(record::getNickName) .set(note).equalTo(record::getNote) .set(createTime).equalTo(record::getCreateTime) .set(loginTime).equalTo(record::getLoginTime) .set(status).equalTo(record::getStatus) .where(id, isEqualTo(record::getId)) ); } @Generated("org.mybatis.generator.api.MyBatisGenerator") default int updateByPrimaryKeySelective(UmsAdmin record) { return update(c -> c.set(username).equalToWhenPresent(record::getUsername) .set(password).equalToWhenPresent(record::getPassword) .set(icon).equalToWhenPresent(record::getIcon) .set(email).equalToWhenPresent(record::getEmail) .set(nickName).equalToWhenPresent(record::getNickName) .set(note).equalToWhenPresent(record::getNote) .set(createTime).equalToWhenPresent(record::getCreateTime) .set(loginTime).equalToWhenPresent(record::getLoginTime) .set(status).equalToWhenPresent(record::getStatus) .where(id, isEqualTo(record::getId)) ); }}
生成程式碼中有一些DynamicSqlSupport類,比如UmsAdminDynamicSqlSupport,主要是把資料庫表和欄位抽象成了SqlTable和SqlColumn物件,估計是為了防止我們硬編碼;public final class UmsAdminDynamicSqlSupport { @Generated("org.mybatis.generator.api.MyBatisGenerator") public static final UmsAdmin umsAdmin = new UmsAdmin(); public static final SqlColumn<Long> id = umsAdmin.id; public static final SqlColumn<String> username = umsAdmin.username; public static final SqlColumn<String> password = umsAdmin.password; public static final SqlColumn<String> icon = umsAdmin.icon; public static final SqlColumn<String> email = umsAdmin.email; public static final SqlColumn<String> nickName = umsAdmin.nickName; public static final SqlColumn<String> note = umsAdmin.note; public static final SqlColumn<Date> createTime = umsAdmin.createTime; public static final SqlColumn<Date> loginTime = umsAdmin.loginTime; public static final SqlColumn<Integer> status = umsAdmin.status; @Generated("org.mybatis.generator.api.MyBatisGenerator") public static final class UmsAdmin extends SqlTable { public final SqlColumn<Long> id = column("id", JDBCType.BIGINT); public final SqlColumn<String> username = column("username", JDBCType.VARCHAR); public final SqlColumn<String> password = column("password", JDBCType.VARCHAR); public final SqlColumn<String> icon = column("icon", JDBCType.VARCHAR); public final SqlColumn<String> email = column("email", JDBCType.VARCHAR); public final SqlColumn<String> nickName = column("nick_name", JDBCType.VARCHAR); public final SqlColumn<String> note = column("note", JDBCType.VARCHAR); public final SqlColumn<Date> createTime = column("create_time", JDBCType.TIMESTAMP); public final SqlColumn<Date> loginTime = column("login_time", JDBCType.TIMESTAMP); public final SqlColumn<Integer> status = column("status", JDBCType.INTEGER); public UmsAdmin() { super("ums_admin"); } }}
利用好MBG生成的程式碼即可完成單表的CRUD操作了,比如下面最常見的操作。/** * 後臺使用者管理Service實現類 * Created by macro on 2020/12/8. */@Servicepublic class UmsAdminServiceImpl implements UmsAdminService { @Autowired private UmsAdminMapper adminMapper; @Override public void create(UmsAdmin entity) { adminMapper.insert(entity); } @Override public void update(UmsAdmin entity) { adminMapper.updateByPrimaryKeySelective(entity); } @Override public void delete(Long id) { adminMapper.deleteByPrimaryKey(id); } @Override public UmsAdmin select(Long id) { Optional<UmsAdmin> optionalEntity = adminMapper.selectByPrimaryKey(id); return optionalEntity.orElse(null); } @Override public List<UmsAdmin> listAll(Integer pageNum, Integer pageSize) { PageHelper.startPage(pageNum, pageSize); return adminMapper.select(SelectDSLCompleter.allRows()); }}
進階使用想要用好Dynamic SQL,上面的基礎操作是不夠的,還需要一些進階的使用技巧。
SqlBuilderSqlBuilder是一個非常有用的類,使用它可以靈活地構建SQL語句的條件,一些常用的條件構建方法如下。
StatementProvider回想一下之前我們在mapper.xml中定義select標籤的方式,各個select標籤相當於Statement。而這裡的StatementProvider好比是Statement中引數和SQL語句的封裝,方便以Java的方式建立Statement。
條件查詢使用SqlBuilder類構建StatementProvider,然後呼叫Mapper介面中的方法即可。
這裡以按使用者名稱和狀態查詢後臺使用者並按建立時間降序排列為例,SQL實現如下;SELECT id, username, PASSWORD, icon, email, nick_name, note, create_time, login_time,STATUS FROM ums_admin WHERE ( username = 'macro' AND STATUS IN ( 0, 1 ) ) ORDER BY create_time DESC;
使用Dynamic SQL對應的Java程式碼實現如下,使用SqlBuilder的select方法可以指定查詢列,使用from方法可以指定查詢表,使用where方法可以構建查詢條件,使用orderBy方法可以指定排序。/** * 後臺使用者管理Service實現類 * Created by macro on 2020/12/8. */@Servicepublic class UmsAdminServiceImpl implements UmsAdminService { @Override public List<UmsAdmin> list(Integer pageNum, Integer pageSize, String username, List<Integer> statusList) { PageHelper.startPage(pageNum, pageSize); SelectStatementProvider selectStatement = SqlBuilder.select(UmsAdminMapper.selectList) .from(UmsAdminDynamicSqlSupport.umsAdmin) .where(UmsAdminDynamicSqlSupport.username, isEqualToWhenPresent(username)) .and(UmsAdminDynamicSqlSupport.status, isIn(statusList)) .orderBy(UmsAdminDynamicSqlSupport.createTime.descending()) .build() .render(RenderingStrategies.MYBATIS3); return adminMapper.selectMany(selectStatement); }}
Lambda條件查詢使用Lambda表示式實現單表條件查詢更加簡單,實現上面的條件查詢,對應Java程式碼實現如下。
/** * 後臺使用者管理Service實現類 * Created by macro on 2020/12/8. */@Servicepublic class UmsAdminServiceImpl implements UmsAdminService { @Override public List<UmsAdmin> lambdaList(Integer pageNum, Integer pageSize, String username, List<Integer> statusList) { PageHelper.startPage(pageNum, pageSize); List<UmsAdmin> list = adminMapper.select(c -> c.where(UmsAdminDynamicSqlSupport.username, isEqualToWhenPresent(username)) .and(UmsAdminDynamicSqlSupport.status, isIn(statusList)) .orderBy(UmsAdminDynamicSqlSupport.createTime.descending())); return list; }}
子查詢之前使用MBG需要在mapper.xml中手寫SQL才能實現子查詢,使用Dynamic SQL可以直接在Java程式碼中實現。
這裡以按角色ID查詢後臺使用者為例,SQL實現如下;SELECT * FROM ums_admin WHERE id IN ( SELECT admin_id FROM ums_admin_role_relation WHERE role_id = 1 )
使用Dynamic SQL對應的Java程式碼實現如下,可以發現SqlBuilder的條件構造方法isIn中還可以巢狀SqlBuilder的查詢。
/** * 後臺使用者管理Service實現類 * Created by macro on 2020/12/8. */@Servicepublic class UmsAdminServiceImpl implements UmsAdminService { @Override public List<UmsAdmin> subList(Long roleId) { SelectStatementProvider selectStatement = SqlBuilder.select(UmsAdminMapper.selectList) .from(UmsAdminDynamicSqlSupport.umsAdmin) .where(UmsAdminDynamicSqlSupport.id, isIn(SqlBuilder.select(UmsAdminRoleRelationDynamicSqlSupport.adminId) .from(UmsAdminRoleRelationDynamicSqlSupport.umsAdminRoleRelation) .where(UmsAdminRoleRelationDynamicSqlSupport.roleId, isEqualTo(roleId)))) .build() .render(RenderingStrategies.MYBATIS3); return adminMapper.selectMany(selectStatement); }}
Group和Join查詢涉及到多表查詢,之前使用MBG的時候基本只能在mapper.xml中手寫SQL實現,使用Dynamic SQL可以支援多表查詢。
這裡以按角色統計後臺使用者數量為例,SQL實現如下;SELECT ur.id AS roleId, ur.NAME AS roleName, count( ua.id ) AS count FROM ums_role ur LEFT JOIN ums_admin_role_relation uarr ON ur.id = uarr.role_id LEFT JOIN ums_admin ua ON uarr.admin_id = ua.id GROUP BY ur.id;
先在Dao中新增一個groupList方法,然後使用@Results註解定義好resultMap;
/** * Created by macro on 2020/12/9. */public interface UmsAdminDao { @SelectProvider(type = SqlProviderAdapter.class, method = "select") @Results(id = "RoleStatResult", value = { @Result(column = "roleId", property = "roleId", jdbcType = JdbcType.BIGINT, id = true), @Result(column = "roleName", property = "roleName", jdbcType = JdbcType.VARCHAR), @Result(column = "count", property = "count", jdbcType = JdbcType.INTEGER) }) List<RoleStatDto> groupList(SelectStatementProvider selectStatement);}
然後在Service中呼叫groupList方法傳入StatementProvider即可,對應的Java程式碼實現如下。/** * 後臺使用者管理Service實現類 * Created by macro on 2020/12/8. */@Servicepublic class UmsAdminServiceImpl implements UmsAdminService { @Override public List<RoleStatDto> groupList() { SelectStatementProvider selectStatement = SqlBuilder.select(UmsRoleDynamicSqlSupport.id.as("roleId"), UmsRoleDynamicSqlSupport.name.as("roleName"), count(UmsAdminDynamicSqlSupport.id).as("count")) .from(UmsRoleDynamicSqlSupport.umsRole) .leftJoin(UmsAdminRoleRelationDynamicSqlSupport.umsAdminRoleRelation) .on(UmsRoleDynamicSqlSupport.id, equalTo(UmsAdminRoleRelationDynamicSqlSupport.roleId)) .leftJoin(UmsAdminDynamicSqlSupport.umsAdmin) .on(UmsAdminRoleRelationDynamicSqlSupport.adminId, equalTo(UmsAdminDynamicSqlSupport.id)) .groupBy(UmsRoleDynamicSqlSupport.id) .build() .render(RenderingStrategies.MYBATIS3); return adminDao.groupList(selectStatement); }}
條件刪除使用Dynamic SQL實現條件刪除,直接呼叫Mapper介面中生成好的delete方法即可。
這裡以按使用者名稱刪除後臺使用者為例,SQL實現如下;DELETE FROM ums_admin WHERE username = 'andy';
使用Dynamic SQL對應Java中的實現如下。
/** * 後臺使用者管理Service實現類 * Created by macro on 2020/12/8. */@Servicepublic class UmsAdminServiceImpl implements UmsAdminService { @Override public void deleteByUsername(String username) { DeleteStatementProvider deleteStatement = SqlBuilder.deleteFrom(UmsAdminDynamicSqlSupport.umsAdmin) .where(UmsAdminDynamicSqlSupport.username, isEqualTo(username)) .build() .render(RenderingStrategies.MYBATIS3); adminMapper.delete(deleteStatement); }}
條件修改使用Dynamic SQL實現條件修改,直接呼叫Mapper介面中生成好的update方法即可。
這裡以按指定ID修改後臺使用者的狀態為例,SQL實現如下;UPDATE ums_admin SET STATUS = 1 WHERE id IN ( 1, 2 );
使用Dynamic SQL對應Java中的實現如下。
/** * 後臺使用者管理Service實現類 * Created by macro on 2020/12/8. */@Servicepublic class UmsAdminServiceImpl implements UmsAdminService { @Override public void updateByIds(List<Long> ids, Integer status) { UpdateStatementProvider updateStatement = SqlBuilder.update(UmsAdminDynamicSqlSupport.umsAdmin) .set(UmsAdminDynamicSqlSupport.status).equalTo(status) .where(UmsAdminDynamicSqlSupport.id, isIn(ids)) .build() .render(RenderingStrategies.MYBATIS3); adminMapper.update(updateStatement); }}
一對多查詢使用Dynamic SQL也可以實現一對多查詢,只是由於Java註解無法實現迴圈引用,所以一對多的resultMap只能在mapper.xml來配置,這可能是唯一需要使用mapper.xml的地方。
這裡以按ID查詢後臺使用者資訊(包含對應角色列表)為例,SQL實現如下;SELECT ua.*, ur.id AS role_id, ur.NAME AS role_name, ur.description AS role_description, ur.create_time AS role_create_time, ur.STATUS AS role_status, ur.sort AS role_sort FROM ums_admin ua LEFT JOIN ums_admin_role_relation uarr ON ua.id = uarr.admin_id LEFT JOIN ums_role ur ON uarr.role_id = ur.id WHERE ua.id = 1
然後在Dao介面中新增selectWithRoleList方法,這裡使用@ResultMap註解引用mapper.xml中定義的resultMap;/** * Created by macro on 2020/12/9. */public interface UmsAdminDao { @SelectProvider(type = SqlProviderAdapter.class, method = "select") @ResultMap("AdminRoleResult") AdminRoleDto selectWithRoleList(SelectStatementProvider selectStatement);}
在mapper.xml中新增名稱為AdminRoleResult的resultMap,這裡有個小技巧,可以直接引用在Mapper介面中定義好的resultMap;
<resultMap id="AdminRoleResult" type="com.macro.mall.tiny.domain.AdminRoleDto" extends="com.macro.mall.tiny.mbg.mapper.UmsAdminMapper.UmsAdminResult"> <collection property="roleList" resultMap="com.macro.mall.tiny.mbg.mapper.UmsRoleMapper.UmsRoleResult" columnPrefix="role_"> </collection></resultMap>
然後在Service實現類中呼叫即可,為了方便結果集對映給查詢列取了別名。/** * 後臺使用者管理Service實現類 * Created by macro on 2020/12/8. */@Servicepublic class UmsAdminServiceImpl implements UmsAdminService { @Override public AdminRoleDto selectWithRoleList(Long id) { List<BasicColumn> columnList = new ArrayList<>(CollUtil.toList(UmsAdminMapper.selectList)); columnList.add(UmsRoleDynamicSqlSupport.id.as("role_id")); columnList.add(UmsRoleDynamicSqlSupport.name.as("role_name")); columnList.add(UmsRoleDynamicSqlSupport.description.as("role_description")); columnList.add(UmsRoleDynamicSqlSupport.createTime.as("role_create_time")); columnList.add(UmsRoleDynamicSqlSupport.status.as("role_status")); columnList.add(UmsRoleDynamicSqlSupport.sort.as("role_sort")); SelectStatementProvider selectStatement = SqlBuilder.select(columnList) .from(UmsAdminDynamicSqlSupport.umsAdmin) .leftJoin(UmsAdminRoleRelationDynamicSqlSupport.umsAdminRoleRelation) .on(UmsAdminDynamicSqlSupport.id, equalTo(UmsAdminRoleRelationDynamicSqlSupport.adminId)) .leftJoin(UmsRoleDynamicSqlSupport.umsRole) .on(UmsAdminRoleRelationDynamicSqlSupport.roleId, equalTo(UmsRoleDynamicSqlSupport.id)) .where(UmsAdminDynamicSqlSupport.id, isEqualTo(id)) .build() .render(RenderingStrategies.MYBATIS3); return adminDao.selectWithRoleList(selectStatement); }}
總結當我們使用MyBatis官方程式碼生成器MBG時,配置的targetRuntime決定了使用它的使用方式。Dynamic SQL更傾向於使用Java API來實現SQL操作,傳統的方式更傾向於在mapper.xml中手寫SQL來實現SQL操作。雖然MyBatis官方推薦使用Dynamic SQL,但選擇哪種方式全看個人習慣了!