Spring Boot 实战 —— MyBatis(注解版)使用方法
原文链接:
简介
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」
Spring Boot 实战 —— MyBatis(注解版)使用方法的更多相关文章
- Spring Boot中@Scheduled注解的使用方法
Spring Boot中@Scheduled注解的使用方法 一.定时任务注解为@Scheduled,使用方式举例如下 //定义一个按时间执行的定时任务,在每天16:00执行一次. @Scheduled ...
- 【spring boot】14.spring boot集成mybatis,注解方式OR映射文件方式AND pagehelper分页插件【Mybatis】pagehelper分页插件分页查询无效解决方法
spring boot集成mybatis,集成使用mybatis拖沓了好久,今天终于可以补起来了. 本篇源码中,同时使用了Spring data JPA 和 Mybatis两种方式. 在使用的过程中一 ...
- Spring Boot整合Mybatis(注解方式和XML方式)
其实对我个人而言还是不够熟悉JPA.hibernate,所以觉得这两种框架使用起来好麻烦啊. 一直用的Mybatis作为持久层框架, JPA(Hibernate)主张所有的SQL都用Java代码生成, ...
- Spring Boot的MyBatis注解:@MapperScan和@Mapper(十七)
1.Spring Boot与MyBatis融合的矛盾问题: Spring家族的使命就是为了简化而生,但是随着Spring的发展壮大,有点事与愿违了.为了坚持初心,Spring家族祭出了一大杀器---S ...
- Spring Boot集成Mybatis注解相关
mybatis3开始支持java注解,使用java注解可以替代xml配置文件,简化代码.下面来看一下怎么在spring boot中使用mybatis注解. 1 使用mybatis注解需要的配置.如下面 ...
- Spring Boot 使用Mybatis注解开发增删改查
使用逆向工程是遇到的错误 错误描述 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): c ...
- Spring Boot整合MyBatis(非注解版)
Spring Boot整合MyBatis(非注解版),开发时采用的时IDEA,JDK1.8 直接上图: 文件夹不存在,创建一个新的路径文件夹 创建完成目录结构如下: 本人第一步习惯先把需要的包结构创建 ...
- Spring Boot实战二:集成Mybatis
Spring Boot集成Mybatis非常简单,在之前搭建好的项目中加入Mybatis依赖的jar,在配置文件中加入数据库配置即可,如下图所示: 创建对应的Controller.Service.Da ...
- spring boot 实战教程
二八法则 - get more with less Java.spring经过多年的发展,各种技术纷繁芜杂,初学者往往不知道该从何下手.其实开发技术的世界也符合二八法则,80%的场景中只有20%的技术 ...
随机推荐
- javascript如何动态修改iframe的src
为什么需要动态修改iframe的src?一般情况我们使用iframe,其中的src通常是写死的,但是有些时候我们不希望它是死的src,而是一个活的src. 示例代码如下: <!DOCTYPE h ...
- 多线程 fork/join 并行计算
1. 什么是Fork/Join框架 Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架. 我们再通过 ...
- Hadoop综合大作业1
本次作业来源于:https://edu.cnblogs.com/campus/gzcc/GZCC-16SE1/homework/3363 一.课程评分标准: 分数组成: 考勤 10 平时作业 30 爬 ...
- Asp.net MVC 权限验证,以及是否允许匿名访问
public class CheckUserAttribute : ActionFilterAttribute, IAuthorizationFilter { public void OnAuthor ...
- Android HIDL学习(2) ---- HelloWorld【转】
本文转载自: 写在前面 程序员有个癖好,无论是学习什么新知识,都喜欢以HelloWorld作为一个简单的例子来开头,咱们也不例外. OK,咱这里都是干货,废话就不多说啦,学习HIDL呢咱们还是需要一些 ...
- Python将print输出内容保存到指定文件中
#!/usr/bin/python # -*- coding: utf- -*- import sys import os class Logger(object): def __init__(sel ...
- 【翻译】Flink Table Api & SQL —Streaming 概念 ——动态表
本文翻译自官网:Flink Table Api & SQL 动态表 https://ci.apache.org/projects/flink/flink-docs-release-1.9/de ...
- 安防视频互联网化的EasyDSS流媒体服务器不但能Easy安防流媒体的开发而且能更加互联网化视频协议的输出
开发EasyDSS的初衷 自从12年开始做EasyDarwin的时候,当时眼光一直都仅仅局限在安防监控视频这一块,对RTMP没有太大的重视,对于后起之秀HLS更是没有太多关注,然而经历了15直播火热的 ...
- LeetCode:字符串相加【415】
LeetCode:字符串相加[415] 题目描述 给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和. 注意: num1 和num2 的长度都小于 5100.num1 和num2 都只 ...
- Lyrics of the song 99 Bottles of Beer
99 bottles of beer on the wall, 99 bottles of beer.Take one down and pass it around, 98 bottles of b ...