原文鏈接:
簡介
MyBatis 官網 是這么介紹它自己的:
MyBatis 是一款優秀的持久層框架,它支持定制化 SQL、存儲過程以及高級映射。MyBatis 避免了幾乎所有的 JDBC 代碼和手動設置參數以及獲取結果集。MyBatis 可以使用簡單的 XML 或注解來配置和映射原生類型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 對象)為數據庫中的記錄。
示例代碼
依賴
這里僅展示和 MyBatis 相關的數據庫依賴項,完整的示例,在文末會附上項目代碼鏈接。
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
spring-boot-start
系列的包,真是給 Spring Boot 開發帶來了極大的便利,它的項目地址是:
配置
創建 users
表的 SQL:
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `users`
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`userName` varchar(32) DEFAULT NULL COMMENT '用戶名',
`passWord` varchar(32) DEFAULT NULL COMMENT '密碼',
`user_sex` varchar(32) DEFAULT NULL,
`nick_name` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `users` VALUES (1,'michael翔', '123', 'MAN', 'zx');
INSERT INTO `users` VALUES (2,'張小敬', '123', 'MAN', 'zxj');
INSERT INTO `users` VALUES (3,'李司辰', '123', 'MAN', 'lsc');
INSERT INTO `users` VALUES (4,'崔器', '123', 'MAN', 'cq');
INSERT INTO `users` VALUES (5,'姚汝能', '123', 'MAN', 'yrn');
INSERT INTO `users` VALUES (null,'檀棋', '123', ' WOMAN', 'tq');
INSERT INTO `users` (`userName`,`passWord`,`user_sex`,`nick_name`) VALUES ('michael', '123', 'MAN', 'zx');
說明:
- id 設置的是自增,當插入數據時,可以不傳或者傳
null
值,都可以; - 插入數據,可以指定
column
也可以不指定;
application-dev.properties
:
swagger.enable=true
server.port=8081
spring.datasource.url=jdbc:mysql://192.168.3.43:3306/beta?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.type-aliases-package==com.michael.springbootmybatis.model
mybatis.configuration.map-underscore-to-camel-case=true
- 配置駝峰屬性自動映射,例如實體中屬性為
userSex
,數據庫屬性為user_sex
,MyBatis 默認是不能自動轉換的。我們可以配置mybatis.configuration.map-underscore-to-camel-case
實現自動映射。如果不進行此配置,通常我們要自定義以下結果集映射:
@Results({
@Result(property = "userSex", column = "user_sex"),
@Result(property = "nickName", column = "nick_name")
})
@Select("SELECT * FROM users WHERE id = #{id}")
UserEntity getUserById(Long id);
在很多 Select
語句需要做結果映射時,自然是相當麻煩。除了上面配置「駝峰屬性自動映射」,也可以用在 @Results
中使用 id
來標識一個映射關系,然后可以用 @ResultMap
復用這個映射關系:
@Select("SELECT * FROM users")
@Results(id = "user", value = {
@Result(property = "userSex", column = "user_sex"),
@Result(property = "nickName", column = "nick_name")
})
@Select("SELECT * FROM users WHERE id = #{id}")
List<UserEntity> getAll();
@ResultMap("user")
@Select("SELECT * FROM users WHERE id = #{id}")
UserEntity getUserById(Integer id);
代碼
這里僅展示關鍵的部分的代碼,完整可看下文的示例代碼。
實體類:
@Data
@ApiModel(description = "UserEntity 實體類")
public class UserEntity implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "用戶 id", dataType = "Long")
private Long id;
@ApiModelProperty(value = "用戶名", required = true)
private String userName;
@ApiModelProperty(value = "密碼")
private String passWord;
@ApiModelProperty(value = "性別")
private UserSexEnum userSex;
@ApiModelProperty(value = "昵稱")
private String nickName;
@Override
public String toString() {
return "userName " + this.userName + ", password " + this.passWord + " , sex " + this.userSex;
}
}
dao/mapper
接口,數據庫交互(Data Access Object
)層:
public interface UserMapper {
// @Results({
// @Result(property = "userSex", column = "user_sex", javaType = UserSexEnum.class),
// @Result(property = "nickName", column = "nick_name")
// })
@Select("SELECT * FROM users")
Page<UserEntity> getAll();
// @Results({
// @Result(property = "userSex", column = "user_sex"),
// @Result(property = "nickName", column = "nick_name")
// })
@Select("SELECT * FROM users WHERE id = #{id}")
UserEntity getUserById(Long id);
@Insert("INSERT INTO users(userName, passWord, user_sex, nick_name) " +
"VALUES(#{userName}, #{passWord}, #{userSex}, #{nickName})")
@Options(useGeneratedKeys = true, keyProperty = "id")
// @SelectKey(statement = "select last_insert_id()", keyProperty = "id", before = false, resultType = Integer.class)
void insert(UserEntity user);
@Update("UPDATE users SET userName=#{userName},nick_name=#{nickName} WHERE id = #{id}")
void update(UserEntity user);
@Delete("DELETE FROM users WHERE id= #{id}")
void deleteUserById(Long id);
}
說明:
insert
這里用了一個@Options
的注解,實現了「主鍵回填」的功能,也就是說,再創建好一個user
之后,user
請求體中的id
屬性會自動賦值好;@SelectKey
注解被注釋掉了,這個注解也同樣可以實現「主鍵回填」的功能;
service 接口:
public interface UserService {
/**
* 查詢所有用戶
*
* @return
*/
Map<String,Object> getAll(int pageNum, int pageSize);
/**
* 根據用戶 ID 查詢用戶
*
* @param id 用戶 ID
* @return
*/
UserEntity getUserById(Long id);
/**
* 新增一個用戶
*
* @param user
*/
void insert(UserEntity user);
/**
* 更新用戶信息,用戶 ID 不傳,會更新失敗
*
* @param user
*/
String update(UserEntity user);
/**
* 根據用戶 ID 刪除用戶
*
* @param id
*/
String deleteById(Long id);
}
service 接口的實現類:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public Map<String,Object> getAll(int pageNum, int pageSize) {
//將參數傳給這個方法就可以實現物理分頁了,非常簡單。
PageHelper.startPage(pageNum, pageSize);
PageInfo<UserEntity> pageInfo = new PageInfo<>(userMapper.getAll());
Long total = pageInfo.getTotal();
List<UserEntity> users = pageInfo.getList();
Map<String,Object> map = new HashMap<>();
map.put("total", total);
map.put("data", users);
return map;
}
@Override
public UserEntity getUserById(Long id) {
return userMapper.getUserById(id);
}
@Override
public void insert(UserEntity user) {
userMapper.insert(user);
}
@Override
public String update(UserEntity user) {
userMapper.update(user);
return "success";
}
@Override
public String deleteById(Long id) {
userMapper.deleteUserById(id);
return "success";
}
}
controller 類:
@RestController
@RequestMapping("/api/v1/")
@Api(tags = {"用戶相關接口"}, value = "用戶模塊")
public class UserController {
@Autowired
private UserService userService;
/**
* 查詢全部用戶
*
* @return
*/
@ApiOperation(value = "獲取用戶列表", notes = "獲取全部用戶信息")
@RequestMapping(value = "/users", method = RequestMethod.GET)
public Map<String,Object> getUsers(
@RequestParam(name = "pageNum", defaultValue = "1", required = false) int pageNum,
@RequestParam(name = "pateSize", defaultValue = "2", required = false) int pageSize) {
return userService.getAll(pageNum, pageSize);
}
/**
* 根據用戶 ID 查詢用戶
*
* @param id
* @return
*/
@ApiOperation(value = "查詢單用戶", notes = "根據用戶id 查詢其信息")
@ApiImplicitParam(name = "id", value = "用戶id", paramType = "query", required = true)
@GetMapping("/user/{id}")
public UserEntity getUser(Long id) {
UserEntity user = userService.getUserById(id);
return user;
}
/**
* 存儲用戶信息
*
* @param user
*/
@ApiOperation(value = "存儲用戶信息", notes = "存儲用戶詳細信息")
@RequestMapping(value = "/user", method = RequestMethod.POST)
public String save(UserEntity user) {
userService.insert(user);
// 用到了 主鍵回填 的配置
return "Create success, user id: " + user.getId();
}
/**
* 更新用戶信息
*
* @param user
*/
@ApiOperation(value = "更新用戶信息", notes = "更新用戶的個人信息")
@PutMapping("/user/")
public void update(@RequestBody UserEntity user) {
userService.update(user);
}
/**
* 根據用戶 ID 刪除用戶
*
* @param id
*/
@ApiOperation(value = "刪除用戶", notes = "根據用戶id刪除用戶信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "用戶id", required = true, paramType = "path")
})
@RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
public void delete(@PathVariable("id") Long id) {
userService.deleteById(id);
}
}
啟動類:
@SpringBootApplication
@MapperScan("com.michael.springbootmybatis.mapper")
public class SpringBootMybatisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisApplication.class, args);
}
}
@MapperScan("com.winter.mapper")
這個注解非常的關鍵,這個對應了項目中mapper/dao
所對應的包路徑。- 如果不用上面的方式,就需要在每個
mapper/dao
類上使用@Mapper
注解;
分頁
通常,在進行查詢時,我們為了避免一次性返回所有結果,通常會進行分頁。比如查詢所有用戶的接口,實際應用中,用戶數據可能會很多,如果全部一次返回,明顯不合適。這時候,就需要進行分頁查詢。
本文我們選用插鍵 pagehelper-spring-boot-starter
要進行分頁。
添加依賴
<!-- 分頁插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
分頁配置
需要添加相應的配置:
#pagehelper分頁插件
pagehelper.helperDialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true
pagehelper.params=count=countSql
pagehelper.row-bounds-with-count=true
pageSizeZero=true
分頁插鍵參數介紹:
helperDialect
:分頁插件會自動檢測當前的數據庫鏈接,自動選擇合適的分頁方式reasonable
:分頁合理化參數,默認值為 false。當該參數設置為 true 時,pageNum<=0
時會查詢第一頁,pageNum>pages
(超過總數時),會查詢最后一頁。默認 false 時,直接根據參數進行查詢params
:為了支持startPage(Object params)
方法,增加了該參數來配置參數映射,用於從對象中根據屬性名取值, 可以配置pageNum,pageSize,count,pageSizeZero,reasonable
,不配置映射的用默認值, 默認值為pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
。supportMethodsArguments
:支持通過 Mapper 接口參數來傳遞分頁參數,默認值 false,分頁插件會從查詢方法的參數值中,自動根據上面params
配置的字段中取值,查找到合適的值時就會自動分頁pageSizeZero
:默認值為 false,當該參數設置為 true 時,如果pageSize=0
或者RowBounds.limit = 0
就會查詢出全部的結果(相當於沒有執行分頁查詢,但是返回結果仍然是 Page 類型)。我測試時,發現不設置,pageSize=0
也會返回全部;
代碼變動
mapper
中查找全部用戶的方法改成如下:
@Select("SELECT * FROM users")
Page<UserEntity> getAll();
service 接口和其實現類的方法改成:
PageInfo<UserEntity> getAll(int pageNum, int pageSize);
service 接口實現類:
@Override
public PageInfo<UserEntity> getAll(int pageNum, int pageSize) {
//將參數傳給這個方法就可以實現物理分頁了,非常簡單。
PageHelper.startPage(pageNum, pageSize);
PageInfo<UserEntity> pageInfo = new PageInfo<>(userMapper.getAll());
return pageInfo;
}
注意點:
PageHelper.startPage(pageNo,pageSize);
只對其后的第一個查詢有效;
controller 類:
@ApiOperation(value = "獲取用戶列表", notes = "獲取全部用戶信息")
@RequestMapping(value = "/users", method = RequestMethod.GET)
public PageInfo<UserEntity> getUsers(
@RequestParam(name = "pageNum", defaultValue = "1", required = false) int pageNum,
@RequestParam(name = "pateSize", defaultValue = "2", required = false) int pageSize) {
return userService.getAll(pageNum, pageSize);
}
除了上面的參數名,還習慣用下面的參數名:
offset
:和pageNum
意思一樣,指定返回記錄的開始位置;limit
:和pageSize
意思一樣,指定返回記錄的數量;
進一步
上面的分頁結果返回的內容有點多,一些屬性並不想放在返回體中。可以進一步優化。編寫工具類限定關心的屬性。
分頁查詢結果封裝類:
@Data
public class PageResult {
/**
* 當前頁碼
*/
private int pageNum;
/**
* 每頁數量
*/
private int pageSize;
/**
* 記錄總數
*/
private long totalSize;
/**
* 頁碼總數
*/
private int totalPages;
/**
* 數據模型
*/
private List<?> content;
}
分頁查詢工具類:
public class PageUitls {
/**
* 將分頁信息封裝到統一的接口
*
* @param pageInfo
* @return
*/
public static PageResult getPageResult(PageInfo<?> pageInfo) {
PageResult pageResult = new PageResult();
pageResult.setPageNum(pageInfo.getPageNum());
pageResult.setPageSize(pageInfo.getPageSize());
pageResult.setTotalSize(pageInfo.getTotal());
pageResult.setTotalPages(pageInfo.getPages());
pageResult.setContent(pageInfo.getList());
return pageResult;
}
}
接口方法:
/**
* 查詢所有用戶
*
* @return
*/
PageResult getAll(int pageNum, int pageSize);
接口實現類:
@Override
public PageResult getAll(int pageNum, int pageSize) {
//將參數傳給這個方法就可以實現物理分頁了,非常簡單。
PageHelper.startPage(pageNum, pageSize);
List<UserEntity> users = userMapper.getAll();
PageInfo<UserEntity> pageInfo = new PageInfo<>(users);
return PageUitls.getPageResult(pageInfo);
}
這樣改寫后,返回體就簡潔許多了:
{
"pageNum": 1,
"pageSize": 2,
"totalSize": 3,
"totalPages": 2,
"content": [
{
"id": 1,
"userName": "Michael翔",
"passWord": "123",
"userSex": "MAN",
"nickName": "ZX"
},
{
"id": 2,
"userName": "HQH",
"passWord": "123",
"userSex": "WOMAN",
"nickName": "QQ"
}
]
}
IN 查詢
關於 MyBatis 的 IN 查詢,也是試驗了很久,才 OK 的。 StackOverflow 上就有類似的問題 How to use Annotations with iBatis (myBatis) for an IN query?。
為了測試 MyBatis IN 查詢,我們將之前的根據 ID 查詢用戶信息的接口進行修改,讓它支持根據輸入的 ID 列表查詢多用戶信息。
controller:
@ApiOperation(value = "查詢指定 ID 的用戶", notes = "根據用戶 id 列表查詢其信息")
@ApiImplicitParam(name = "ids", value = "用戶 id 列表", paramType = "path", required = true)
@GetMapping(value = "/user/{ids}")
public PageResult getUser(@RequestParam(name = "pageNum", defaultValue = "1", required = false) int pageNum,
@RequestParam(name = "pageSize", defaultValue = "2", required = false) int pageSize,
@PathVariable String ids) {
List<String> idLst = Arrays.asList(ids.split(","));
PageResult user = userService.getUserById(pageNum, pageSize, idLst);
return user;
}
這里有個小注意點,@ApiImplicitParam
注解中的 paramType = "path"
記得修改為 path
,因為請求參數中包含路徑變量了,否則渲染 URL 時,會出問題。
mapper 類:
@Select({
"<script>",
"SELECT * ",
"FROM users WHERE id IN",
"<foreach item='id' index='index' collection='ids' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
List<UserEntity> getUserById(@Param("ids") List<String> ids);
說明:
item
標識集合中每一個元素進行迭代是的別名,很多教程中設置為item
,我這里改為id
也是 OK,而且也易於理解;index
指定一個名字,用於表示在迭代過程中,每次迭代到的位置,從 0 開始;open
表示該語句以什么開始;separator
表示在每次進行迭代之間以什么符號作為分隔符;close
表示以什么結束;collection
屬性,該屬性是必須指定的,但是在不同情況下,該屬性的值是不一樣的;@Param
的設置比較關鍵,相當於給其修飾的參數指定一個別名:- 使用
@Param
,默認會和參數名同名,或者以注解傳入的變量名為准。變量名將作為@Select
中的可用參數,比如,我這里這樣定義@Param("ids2") List<String> ids
,那么,@Select
中可用參數名將是ids2
,collection
也須定義為ids2
,否則會報錯:nested exception is org.apache.ibatis.binding.BindingException: Parameter 'list' not found. Available parameters are [ids2, param1]
; - 不使用
@Param
時,那么,此時collection
需要定義為list
,否則會報錯:nested exception is org.apache.ibatis.binding.BindingException: Parameter 'list2' not found. Available parameters are [collection, list]
;
- 使用
上面的說明參考自 mybatis查詢sql中in條件使用(foreach) ,沒有找到官方文檔支撐,待補充。
動態 SQL
待后續補充
FAQ
MyBatis 中 # 和 $ 的區別
- 簡單說
#{}
是經過預編譯的,是安全的,而${}
是未經過預編譯的,僅僅是取變量的值,是非安全的,存在 SQL 注入。${}
將傳入的數據都當成一個字符串,會對自動傳入的數據加一個雙引號。 - 使用
${}
的情況,order by
、like
語句只能用${}
,用#{}
會多個' '
導致 SQL 語句失效。此外動態拼接 SQL,模糊查詢時也要用${}
。
參考
- 純潔的微笑-Spring Boot(六):如何優雅的使用 Mybatis 本文的主要參考文章之一,入門挺好
- CSDN-larger5-[增刪改查] SpringBoot + MyBatis(注解版) 這位博主的示例,代碼結構和風格都比較規范,值得學習
- CSDN-LuisChen的博客-Spring boot Mybatis 整合(完整版)
- 博客園-Ruthless-SpringBoot+Mybatis+Pagehelper分頁 查詢結果關系映射那塊,該文章介紹的
@ResultMap
比較方便;
分頁
- CSDN-SpringBoot使用Mybatis注解開發教程-分頁-動態sql 分頁參考
- 博客園-朝雨憶輕塵-Spring Boot:實現MyBatis分頁 推薦,
PageResult
的優化,參考此文 - PageHelper-官宣-如何使用分頁插件
FAQ
歡迎關注個人公眾號 「iPlayMichael」